From 4eb0e063bcd34c21b737023aa6ed5baed80658d1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 13 Jun 2021 15:00:17 +0200 Subject: Repo re-org, make gerberex tests run --- gerbonara/gerber/__init__.py | 27 + gerbonara/gerber/__main__.py | 122 + gerbonara/gerber/am_eval.py | 109 + gerbonara/gerber/am_read.py | 255 + gerbonara/gerber/am_statements.py | 1046 ++++ gerbonara/gerber/cam.py | 286 + gerbonara/gerber/common.py | 71 + gerbonara/gerber/excellon.py | 904 +++ gerbonara/gerber/excellon_report/excellon_drr.py | 25 + gerbonara/gerber/excellon_settings.py | 105 + gerbonara/gerber/excellon_statements.py | 979 ++++ gerbonara/gerber/excellon_tool.py | 190 + gerbonara/gerber/exceptions.py | 36 + gerbonara/gerber/gerber_statements.py | 1189 ++++ gerbonara/gerber/ipc356.py | 485 ++ gerbonara/gerber/layers.py | 295 + gerbonara/gerber/ncparam/allegro.py | 25 + gerbonara/gerber/operations.py | 126 + gerbonara/gerber/panelize/__init__.py | 8 + gerbonara/gerber/panelize/am_expression.py | 184 + gerbonara/gerber/panelize/am_primitive.py | 448 ++ gerbonara/gerber/panelize/common.py | 41 + gerbonara/gerber/panelize/composition.py | 192 + gerbonara/gerber/panelize/dxf.py | 796 +++ gerbonara/gerber/panelize/dxf_path.py | 412 ++ gerbonara/gerber/panelize/excellon.py | 404 ++ gerbonara/gerber/panelize/gerber_statements.py | 115 + gerbonara/gerber/panelize/rs274x.py | 331 ++ gerbonara/gerber/panelize/utility.py | 27 + gerbonara/gerber/pcb.py | 124 + gerbonara/gerber/primitives.py | 1697 ++++++ gerbonara/gerber/render/__init__.py | 31 + gerbonara/gerber/render/cairo_backend.py | 616 ++ gerbonara/gerber/render/excellon_backend.py | 188 + gerbonara/gerber/render/render.py | 246 + gerbonara/gerber/render/rs274x_backend.py | 510 ++ gerbonara/gerber/render/theme.py | 112 + gerbonara/gerber/rs274x.py | 800 +++ gerbonara/gerber/tests/__init__.py | 0 .../tests/golden/example_am_exposure_modifier.png | Bin 0 -> 10091 bytes .../tests/golden/example_coincident_hole.png | Bin 0 -> 47261 bytes .../gerber/tests/golden/example_cutin_multiple.png | Bin 0 -> 1348 bytes .../gerber/tests/golden/example_flash_circle.png | Bin 0 -> 5978 bytes .../gerber/tests/golden/example_flash_obround.png | Bin 0 -> 3443 bytes .../gerber/tests/golden/example_flash_polygon.png | Bin 0 -> 4087 bytes .../tests/golden/example_flash_rectangle.png | Bin 0 -> 1731 bytes .../tests/golden/example_fully_coincident.png | Bin 0 -> 71825 bytes .../tests/golden/example_holes_dont_clear.png | Bin 0 -> 11552 bytes .../golden/example_not_overlapping_contour.png | Bin 0 -> 71825 bytes .../golden/example_not_overlapping_touching.png | Bin 0 -> 96557 bytes .../tests/golden/example_overlapping_contour.png | Bin 0 -> 33301 bytes .../tests/golden/example_overlapping_touching.png | Bin 0 -> 33301 bytes .../gerber/tests/golden/example_simple_contour.png | Bin 0 -> 31830 bytes .../gerber/tests/golden/example_single_contour.png | Bin 0 -> 556 bytes .../tests/golden/example_single_contour_3.png | Bin 0 -> 2297 bytes .../tests/golden/example_single_quadrant.gbr | 16 + .../tests/golden/example_single_quadrant.png | Bin 0 -> 9658 bytes .../tests/golden/example_two_square_boxes.gbr | 16 + .../tests/golden/example_two_square_boxes.png | Bin 0 -> 18219 bytes gerbonara/gerber/tests/resources/board_outline.GKO | 503 ++ gerbonara/gerber/tests/resources/bottom_copper.GBL | 1811 ++++++ gerbonara/gerber/tests/resources/bottom_mask.GBS | 66 + gerbonara/gerber/tests/resources/bottom_silk.GBO | 6007 ++++++++++++++++++++ .../resources/example_am_exposure_modifier.gbr | 16 + .../tests/resources/example_coincident_hole.gbr | 24 + gerbonara/gerber/tests/resources/example_cutin.gbr | 18 + .../tests/resources/example_cutin_multiple.gbr | 28 + .../tests/resources/example_flash_circle.gbr | 10 + .../tests/resources/example_flash_obround.gbr | 10 + .../tests/resources/example_flash_polygon.gbr | 10 + .../tests/resources/example_flash_rectangle.gbr | 10 + .../tests/resources/example_fully_coincident.gbr | 23 + .../tests/resources/example_guess_by_content.g0 | 166 + .../tests/resources/example_holes_dont_clear.gbr | 13 + .../gerber/tests/resources/example_level_holes.gbr | 39 + .../resources/example_not_overlapping_contour.gbr | 20 + .../resources/example_not_overlapping_touching.gbr | 20 + .../resources/example_overlapping_contour.gbr | 20 + .../resources/example_overlapping_touching.gbr | 20 + .../tests/resources/example_simple_contour.gbr | 16 + .../tests/resources/example_single_contour_1.gbr | 15 + .../tests/resources/example_single_contour_2.gbr | 15 + .../tests/resources/example_single_contour_3.gbr | 15 + .../tests/resources/example_single_quadrant.gbr | 18 + .../tests/resources/example_two_square_boxes.gbr | 19 + gerbonara/gerber/tests/resources/ipc-d-356.ipc | 115 + .../gerber/tests/resources/multiline_read.ger | 9 + gerbonara/gerber/tests/resources/ncdrill.DRD | 51 + gerbonara/gerber/tests/resources/top_copper.GTL | 27 + gerbonara/gerber/tests/resources/top_mask.GTS | 162 + gerbonara/gerber/tests/resources/top_silk.GTO | 2099 +++++++ gerbonara/gerber/tests/test_am_statements.py | 395 ++ gerbonara/gerber/tests/test_cairo_backend.py | 279 + gerbonara/gerber/tests/test_cam.py | 151 + gerbonara/gerber/tests/test_common.py | 38 + gerbonara/gerber/tests/test_excellon.py | 366 ++ gerbonara/gerber/tests/test_excellon_statements.py | 734 +++ gerbonara/gerber/tests/test_gerber_statements.py | 959 ++++ gerbonara/gerber/tests/test_ipc356.py | 148 + gerbonara/gerber/tests/test_layers.py | 158 + gerbonara/gerber/tests/test_primitives.py | 1429 +++++ gerbonara/gerber/tests/test_rs274x.py | 55 + gerbonara/gerber/tests/test_rs274x_backend.py | 232 + gerbonara/gerber/tests/test_utils.py | 167 + gerbonara/gerber/utils.py | 458 ++ 105 files changed, 30553 insertions(+) create mode 100644 gerbonara/gerber/__init__.py create mode 100644 gerbonara/gerber/__main__.py create mode 100644 gerbonara/gerber/am_eval.py create mode 100644 gerbonara/gerber/am_read.py create mode 100644 gerbonara/gerber/am_statements.py create mode 100644 gerbonara/gerber/cam.py create mode 100644 gerbonara/gerber/common.py create mode 100755 gerbonara/gerber/excellon.py create mode 100644 gerbonara/gerber/excellon_report/excellon_drr.py create mode 100644 gerbonara/gerber/excellon_settings.py create mode 100644 gerbonara/gerber/excellon_statements.py create mode 100644 gerbonara/gerber/excellon_tool.py create mode 100644 gerbonara/gerber/exceptions.py create mode 100644 gerbonara/gerber/gerber_statements.py create mode 100644 gerbonara/gerber/ipc356.py create mode 100644 gerbonara/gerber/layers.py create mode 100644 gerbonara/gerber/ncparam/allegro.py create mode 100644 gerbonara/gerber/operations.py create mode 100644 gerbonara/gerber/panelize/__init__.py create mode 100644 gerbonara/gerber/panelize/am_expression.py create mode 100644 gerbonara/gerber/panelize/am_primitive.py create mode 100644 gerbonara/gerber/panelize/common.py create mode 100644 gerbonara/gerber/panelize/composition.py create mode 100644 gerbonara/gerber/panelize/dxf.py create mode 100644 gerbonara/gerber/panelize/dxf_path.py create mode 100644 gerbonara/gerber/panelize/excellon.py create mode 100644 gerbonara/gerber/panelize/gerber_statements.py create mode 100644 gerbonara/gerber/panelize/rs274x.py create mode 100644 gerbonara/gerber/panelize/utility.py create mode 100644 gerbonara/gerber/pcb.py create mode 100644 gerbonara/gerber/primitives.py create mode 100644 gerbonara/gerber/render/__init__.py create mode 100644 gerbonara/gerber/render/cairo_backend.py create mode 100644 gerbonara/gerber/render/excellon_backend.py create mode 100644 gerbonara/gerber/render/render.py create mode 100644 gerbonara/gerber/render/rs274x_backend.py create mode 100644 gerbonara/gerber/render/theme.py create mode 100644 gerbonara/gerber/rs274x.py create mode 100644 gerbonara/gerber/tests/__init__.py create mode 100644 gerbonara/gerber/tests/golden/example_am_exposure_modifier.png create mode 100644 gerbonara/gerber/tests/golden/example_coincident_hole.png create mode 100644 gerbonara/gerber/tests/golden/example_cutin_multiple.png create mode 100644 gerbonara/gerber/tests/golden/example_flash_circle.png create mode 100644 gerbonara/gerber/tests/golden/example_flash_obround.png create mode 100644 gerbonara/gerber/tests/golden/example_flash_polygon.png create mode 100644 gerbonara/gerber/tests/golden/example_flash_rectangle.png create mode 100644 gerbonara/gerber/tests/golden/example_fully_coincident.png create mode 100644 gerbonara/gerber/tests/golden/example_holes_dont_clear.png create mode 100644 gerbonara/gerber/tests/golden/example_not_overlapping_contour.png create mode 100644 gerbonara/gerber/tests/golden/example_not_overlapping_touching.png create mode 100644 gerbonara/gerber/tests/golden/example_overlapping_contour.png create mode 100644 gerbonara/gerber/tests/golden/example_overlapping_touching.png create mode 100644 gerbonara/gerber/tests/golden/example_simple_contour.png create mode 100644 gerbonara/gerber/tests/golden/example_single_contour.png create mode 100644 gerbonara/gerber/tests/golden/example_single_contour_3.png create mode 100644 gerbonara/gerber/tests/golden/example_single_quadrant.gbr create mode 100644 gerbonara/gerber/tests/golden/example_single_quadrant.png create mode 100644 gerbonara/gerber/tests/golden/example_two_square_boxes.gbr create mode 100644 gerbonara/gerber/tests/golden/example_two_square_boxes.png create mode 100644 gerbonara/gerber/tests/resources/board_outline.GKO create mode 100644 gerbonara/gerber/tests/resources/bottom_copper.GBL create mode 100644 gerbonara/gerber/tests/resources/bottom_mask.GBS create mode 100644 gerbonara/gerber/tests/resources/bottom_silk.GBO create mode 100644 gerbonara/gerber/tests/resources/example_am_exposure_modifier.gbr create mode 100644 gerbonara/gerber/tests/resources/example_coincident_hole.gbr create mode 100644 gerbonara/gerber/tests/resources/example_cutin.gbr create mode 100644 gerbonara/gerber/tests/resources/example_cutin_multiple.gbr create mode 100644 gerbonara/gerber/tests/resources/example_flash_circle.gbr create mode 100644 gerbonara/gerber/tests/resources/example_flash_obround.gbr create mode 100644 gerbonara/gerber/tests/resources/example_flash_polygon.gbr create mode 100644 gerbonara/gerber/tests/resources/example_flash_rectangle.gbr create mode 100644 gerbonara/gerber/tests/resources/example_fully_coincident.gbr create mode 100644 gerbonara/gerber/tests/resources/example_guess_by_content.g0 create mode 100644 gerbonara/gerber/tests/resources/example_holes_dont_clear.gbr create mode 100644 gerbonara/gerber/tests/resources/example_level_holes.gbr create mode 100644 gerbonara/gerber/tests/resources/example_not_overlapping_contour.gbr create mode 100644 gerbonara/gerber/tests/resources/example_not_overlapping_touching.gbr create mode 100644 gerbonara/gerber/tests/resources/example_overlapping_contour.gbr create mode 100644 gerbonara/gerber/tests/resources/example_overlapping_touching.gbr create mode 100644 gerbonara/gerber/tests/resources/example_simple_contour.gbr create mode 100644 gerbonara/gerber/tests/resources/example_single_contour_1.gbr create mode 100644 gerbonara/gerber/tests/resources/example_single_contour_2.gbr create mode 100644 gerbonara/gerber/tests/resources/example_single_contour_3.gbr create mode 100644 gerbonara/gerber/tests/resources/example_single_quadrant.gbr create mode 100644 gerbonara/gerber/tests/resources/example_two_square_boxes.gbr create mode 100644 gerbonara/gerber/tests/resources/ipc-d-356.ipc create mode 100644 gerbonara/gerber/tests/resources/multiline_read.ger create mode 100644 gerbonara/gerber/tests/resources/ncdrill.DRD create mode 100644 gerbonara/gerber/tests/resources/top_copper.GTL create mode 100644 gerbonara/gerber/tests/resources/top_mask.GTS create mode 100644 gerbonara/gerber/tests/resources/top_silk.GTO create mode 100644 gerbonara/gerber/tests/test_am_statements.py create mode 100644 gerbonara/gerber/tests/test_cairo_backend.py create mode 100644 gerbonara/gerber/tests/test_cam.py create mode 100644 gerbonara/gerber/tests/test_common.py create mode 100644 gerbonara/gerber/tests/test_excellon.py create mode 100644 gerbonara/gerber/tests/test_excellon_statements.py create mode 100644 gerbonara/gerber/tests/test_gerber_statements.py create mode 100644 gerbonara/gerber/tests/test_ipc356.py create mode 100644 gerbonara/gerber/tests/test_layers.py create mode 100644 gerbonara/gerber/tests/test_primitives.py create mode 100644 gerbonara/gerber/tests/test_rs274x.py create mode 100644 gerbonara/gerber/tests/test_rs274x_backend.py create mode 100644 gerbonara/gerber/tests/test_utils.py create mode 100644 gerbonara/gerber/utils.py (limited to 'gerbonara') diff --git a/gerbonara/gerber/__init__.py b/gerbonara/gerber/__init__.py new file mode 100644 index 0000000..a3d4753 --- /dev/null +++ b/gerbonara/gerber/__init__.py @@ -0,0 +1,27 @@ +#! /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. +""" +Gerbonara +========= + +gerbonara provides utilities for working with Gerber (RS-274X) and Excellon +files in python. +""" + +from .common import read, loads +from .layers import load_layer, load_layer_data +from .pcb import PCB diff --git a/gerbonara/gerber/__main__.py b/gerbonara/gerber/__main__.py new file mode 100644 index 0000000..988adff --- /dev/null +++ b/gerbonara/gerber/__main__.py @@ -0,0 +1,122 @@ +#! /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 os +import argparse +from .render import available_renderers +from .render import theme +from .pcb import PCB +from . import load_layer + + +def main(): + parser = argparse.ArgumentParser( + description='Render gerber files to image', + prog='gerber-render' + ) + parser.add_argument( + 'filenames', metavar='FILENAME', type=str, nargs='+', + help='Gerber files to render. If a directory is provided, it should ' + 'be provided alone and should contain the gerber files for a ' + 'single PCB.' + ) + parser.add_argument( + '--outfile', '-o', type=str, nargs='?', default='out', + help="Output Filename (extension will be added automatically)" + ) + parser.add_argument( + '--backend', '-b', choices=available_renderers.keys(), default='cairo', + help='Choose the backend to use to generate the output.' + ) + parser.add_argument( + '--theme', '-t', choices=theme.THEMES.keys(), default='default', + help='Select render theme.' + ) + parser.add_argument( + '--width', type=int, default=1920, help='Maximum width.' + ) + parser.add_argument( + '--height', type=int, default=1080, help='Maximum height.' + ) + parser.add_argument( + '--verbose', '-v', action='store_true', default=False, + help='Increase verbosity of the output.' + ) + # parser.add_argument( + # '--quick', '-q', action='store_true', default=False, + # help='Skip longer running rendering steps to produce lower quality' + # ' output faster. This only has an effect for the freecad backend.' + # ) + # parser.add_argument( + # '--nox', action='store_true', default=False, + # help='Run without using any GUI elements. This may produce suboptimal' + # 'output. For the freecad backend, colors, transparancy, and ' + # 'visibility cannot be set without a GUI instance.' + # ) + + args = parser.parse_args() + + renderer = available_renderers[args.backend]() + + if args.backend in ['cairo', ]: + outext = 'png' + else: + outext = None + + if os.path.exists(args.filenames[0]) and os.path.isdir(args.filenames[0]): + directory = args.filenames[0] + pcb = PCB.from_directory(directory) + + if args.backend in ['cairo', ]: + top = pcb.top_layers + bottom = pcb.bottom_layers + copper = pcb.copper_layers + + outline = pcb.outline_layer + if outline: + top = [outline] + top + bottom = [outline] + bottom + copper = [outline] + copper + pcb.drill_layers + + renderer.render_layers( + layers=top, theme=theme.THEMES[args.theme], + max_height=args.height, max_width=args.width, + filename='{0}.top.{1}'.format(args.outfile, outext) + ) + renderer.render_layers( + layers=bottom, theme=theme.THEMES[args.theme], + max_height=args.height, max_width=args.width, + filename='{0}.bottom.{1}'.format(args.outfile, outext) + ) + renderer.render_layers( + layers=copper, theme=theme.THEMES['Transparent Multilayer'], + max_height=args.height, max_width=args.width, + filename='{0}.copper.{1}'.format(args.outfile, outext)) + else: + pass + else: + filenames = args.filenames + for filename in filenames: + layer = load_layer(filename) + settings = theme.THEMES[args.theme].get(layer.layer_class, None) + renderer.render_layer(layer, settings=settings) + renderer.dump(filename='{0}.{1}'.format(args.outfile, outext)) + + +if __name__ == '__main__': + main() + diff --git a/gerbonara/gerber/am_eval.py b/gerbonara/gerber/am_eval.py new file mode 100644 index 0000000..3a7e1ed --- /dev/null +++ b/gerbonara/gerber/am_eval.py @@ -0,0 +1,109 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# copyright 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. +""" This module provides RS-274-X AM macro evaluation. +""" + + +class OpCode: + PUSH = 1 + LOAD = 2 + STORE = 3 + ADD = 4 + SUB = 5 + MUL = 6 + DIV = 7 + PRIM = 8 + + @staticmethod + def str(opcode): + if opcode == OpCode.PUSH: + return "OPCODE_PUSH" + elif opcode == OpCode.LOAD: + return "OPCODE_LOAD" + elif opcode == OpCode.STORE: + return "OPCODE_STORE" + elif opcode == OpCode.ADD: + return "OPCODE_ADD" + elif opcode == OpCode.SUB: + return "OPCODE_SUB" + elif opcode == OpCode.MUL: + return "OPCODE_MUL" + elif opcode == OpCode.DIV: + return "OPCODE_DIV" + elif opcode == OpCode.PRIM: + return "OPCODE_PRIM" + else: + return "UNKNOWN" + + +def eval_macro(instructions, parameters={}): + + if not isinstance(parameters, type({})): + p = {} + for i, val in enumerate(parameters): + p[i + 1] = val + + parameters = p + + stack = [] + + def pop(): + return stack.pop() + + def push(op): + stack.append(op) + + def top(): + return stack[-1] + + def empty(): + return len(stack) == 0 + + for opcode, argument in instructions: + if opcode == OpCode.PUSH: + push(argument) + + elif opcode == OpCode.LOAD: + push(parameters.get(argument, 0)) + + elif opcode == OpCode.STORE: + parameters[argument] = pop() + + elif opcode == OpCode.ADD: + op1 = pop() + op2 = pop() + push(op2 + op1) + + elif opcode == OpCode.SUB: + op1 = pop() + op2 = pop() + push(op2 - op2) + + elif opcode == OpCode.MUL: + op1 = pop() + op2 = pop() + push(op2 * op1) + + elif opcode == OpCode.DIV: + op1 = pop() + op2 = pop() + push(op2 / op1) + + elif opcode == OpCode.PRIM: + yield "%d,%s" % (argument, ",".join([str(x) for x in stack])) + stack = [] diff --git a/gerbonara/gerber/am_read.py b/gerbonara/gerber/am_read.py new file mode 100644 index 0000000..4aff00b --- /dev/null +++ b/gerbonara/gerber/am_read.py @@ -0,0 +1,255 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# copyright 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. +""" This module provides RS-274-X AM macro modifiers parsing. +""" + +from .am_eval import OpCode, eval_macro + +import string + + +class Token: + ADD = "+" + SUB = "-" + # compatibility as many gerber writes do use non compliant X + MULT = ("x", "X") + DIV = "/" + OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV) + LEFT_PARENS = "(" + RIGHT_PARENS = ")" + EQUALS = "=" + EOF = "EOF" + + +def token_to_opcode(token): + if token == Token.ADD: + return OpCode.ADD + elif token == Token.SUB: + return OpCode.SUB + elif token in Token.MULT: + return OpCode.MUL + elif token == Token.DIV: + return OpCode.DIV + else: + return None + + +def precedence(token): + if token == Token.ADD or token == Token.SUB: + return 1 + elif token in Token.MULT or token == Token.DIV: + return 2 + else: + return 0 + + +def is_op(token): + return token in Token.OPERATORS + + +class Scanner: + + def __init__(self, s): + self.buff = s + self.n = 0 + + def eof(self): + return self.n == len(self.buff) + + def peek(self): + if not self.eof(): + return self.buff[self.n] + + return Token.EOF + + def ungetc(self): + if self.n > 0: + self.n -= 1 + + def getc(self): + if self.eof(): + return "" + + c = self.buff[self.n] + self.n += 1 + return c + + def readint(self): + n = "" + while not self.eof() and (self.peek() in string.digits): + n += self.getc() + return int(n) + + def readfloat(self): + n = "" + while not self.eof() and (self.peek() in string.digits or self.peek() == "."): + n += self.getc() + # weird case where zero is ommited inthe last modifider, like in ',0.' + if n == ".": + return 0 + return float(n) + + def readstr(self, end="*"): + s = "" + while not self.eof() and self.peek() != end: + s += self.getc() + return s.strip() + + +def print_instructions(instructions): + for opcode, argument in instructions: + print("%s %s" % (OpCode.str(opcode), + str(argument) if argument is not None else "")) + + +def read_macro(macro): + instructions = [] + + for block in macro.split("*"): + + is_primitive = False + is_equation = False + + found_equation_left_side = False + found_primitive_code = False + + equation_left_side = 0 + primitive_code = 0 + + unary_minus_allowed = False + unary_minus = False + + if Token.EQUALS in block: + is_equation = True + else: + is_primitive = True + + scanner = Scanner(block) + + # inlined here for compactness and convenience + op_stack = [] + + def pop(): + return op_stack.pop() + + def push(op): + op_stack.append(op) + + def top(): + return op_stack[-1] + + def empty(): + return len(op_stack) == 0 + + while not scanner.eof(): + + c = scanner.getc() + + if c == ",": + found_primitive_code = True + + # add all instructions on the stack to finish last modifier + while not empty(): + instructions.append((token_to_opcode(pop()), None)) + + unary_minus_allowed = True + + elif c in Token.OPERATORS: + if c == Token.SUB and unary_minus_allowed: + unary_minus = True + unary_minus_allowed = False + continue + + while not empty() and is_op(top()) and precedence(top()) >= precedence(c): + instructions.append((token_to_opcode(pop()), None)) + + push(c) + + elif c == Token.LEFT_PARENS: + push(c) + + elif c == Token.RIGHT_PARENS: + while not empty() and top() != Token.LEFT_PARENS: + instructions.append((token_to_opcode(pop()), None)) + + if empty(): + raise ValueError("unbalanced parentheses") + + # discard "(" + pop() + + elif c.startswith("$"): + n = scanner.readint() + + if is_equation and not found_equation_left_side: + equation_left_side = n + else: + instructions.append((OpCode.LOAD, n)) + + elif c == Token.EQUALS: + found_equation_left_side = True + + elif c == "0": + if is_primitive and not found_primitive_code: + instructions.append((OpCode.PUSH, scanner.readstr("*"))) + found_primitive_code = True + else: + # decimal or integer disambiguation + if scanner.peek() not in '.' or scanner.peek() == Token.EOF: + instructions.append((OpCode.PUSH, 0)) + + elif c in "123456789.": + scanner.ungetc() + + if is_primitive and not found_primitive_code: + primitive_code = scanner.readint() + else: + n = scanner.readfloat() + if unary_minus: + unary_minus = False + n *= -1 + + instructions.append((OpCode.PUSH, n)) + else: + # whitespace or unknown char + pass + + # add all instructions on the stack to finish last modifier (if any) + while not empty(): + instructions.append((token_to_opcode(pop()), None)) + + # at end, we either have a primitive or a equation + if is_primitive and found_primitive_code: + instructions.append((OpCode.PRIM, primitive_code)) + + if is_equation: + instructions.append((OpCode.STORE, equation_left_side)) + + return instructions + +if __name__ == '__main__': + import sys + + instructions = read_macro(sys.argv[1]) + + print("insructions:") + print_instructions(instructions) + + print("eval:") + for primitive in eval_macro(instructions): + print(primitive) diff --git a/gerbonara/gerber/am_statements.py b/gerbonara/gerber/am_statements.py new file mode 100644 index 0000000..31c0ae4 --- /dev/null +++ b/gerbonara/gerber/am_statements.py @@ -0,0 +1,1046 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2015 Hamilton Kibbe and 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 math import asin +import math + +from .primitives import * +from .utils import validate_coordinates, inch, metric, rotate_point + + + +# TODO: Add support for aperture macro variables +__all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive', + 'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive', + 'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive', + 'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive'] + + +class AMPrimitive(object): + """ Aperture Macro Primitive Base Class + + Parameters + ---------- + code : int + primitive shape code + + exposure : str + on or off Primitives with exposure on create a slid part of + the macro aperture, and primitives with exposure off erase the + solid part created previously in the aperture macro definition. + .. note:: + The erasing effect is limited to the aperture definition in + which it occurs. + + Returns + ------- + primitive : :class: `gerber.am_statements.AMPrimitive` + + Raises + ------ + TypeError, ValueError + """ + + def __init__(self, code, exposure=None): + VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22, 9999) + if not isinstance(code, int): + raise TypeError('Aperture Macro Primitive code must be an integer') + elif code not in VALID_CODES: + raise ValueError('Invalid Code. Valid codes are %s.' % + ', '.join(map(str, VALID_CODES))) + if exposure is not None and exposure.lower() not in ('on', 'off'): + raise ValueError('Exposure must be either on or off') + self.code = code + self.exposure = exposure.lower() if exposure is not None else None + + def to_inch(self): + raise NotImplementedError('Subclass must implement `to-inch`') + + def to_metric(self): + raise NotImplementedError('Subclass must implement `to-metric`') + + @property + def _level_polarity(self): + if self.exposure == 'off': + return 'clear' + return 'dark' + + def to_primitive(self, units): + """ Return a Primitive instance based on the specified macro params. + """ + print('Rendering {}s is not supported yet.'.format(str(self.__class__))) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class AMCommentPrimitive(AMPrimitive): + """ Aperture Macro Comment primitive. Code 0 + + The comment primitive has no image meaning. It is used to include human- + readable comments into the AM command. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.1:** Comment, primitive code 0 + + Parameters + ---------- + code : int + Aperture Macro primitive code. 0 Indicates an AMCommentPrimitive + + comment : str + The comment as a string. + + Returns + ------- + CommentPrimitive : :class:`gerber.am_statements.AMCommentPrimitive` + An Initialized AMCommentPrimitive + + Raises + ------ + ValueError + """ + @classmethod + def from_gerber(cls, primitive): + primitive = primitive.strip() + code = int(primitive[0]) + comment = primitive[1:] + return cls(code, comment) + + def __init__(self, code, comment): + if code != 0: + raise ValueError('Not a valid Aperture Macro Comment statement') + super(AMCommentPrimitive, self).__init__(code) + self.comment = comment.strip(' *') + + def to_inch(self): + pass + + def to_metric(self): + pass + + def to_gerber(self, settings=None): + return '0 %s *' % self.comment + + def to_primitive(self, units): + """ + Returns None - has not primitive representation + """ + return None + + def __str__(self): + return '' % self.comment + + +class AMCirclePrimitive(AMPrimitive): + """ Aperture macro Circle primitive. Code 1 + + A circle primitive is defined by its center point and diameter. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.2:** Circle, primitive code 1 + + Parameters + ---------- + code : int + Circle Primitive code. Must be 1 + + exposure : string + 'on' or 'off' + + diameter : float + Circle diameter + + position : tuple (, ) + Position of the circle relative to the macro origin + + Returns + ------- + CirclePrimitive : :class:`gerber.am_statements.AMCirclePrimitive` + An initialized AMCirclePrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(',') + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + diameter = float(modifiers[2]) + position = (float(modifiers[3]), float(modifiers[4])) + return cls(code, exposure, diameter, position) + + @classmethod + def from_primitive(cls, primitive): + return cls(1, 'on', primitive.diameter, primitive.position) + + def __init__(self, code, exposure, diameter, position): + validate_coordinates(position) + if code != 1: + raise ValueError('CirclePrimitive code is 1') + super(AMCirclePrimitive, self).__init__(code, exposure) + self.diameter = diameter + self.position = position + + def to_inch(self): + self.diameter = inch(self.diameter) + self.position = tuple([inch(x) for x in self.position]) + + def to_metric(self): + self.diameter = metric(self.diameter) + self.position = tuple([metric(x) for x in self.position]) + + def to_gerber(self, settings=None): + data = dict(code=self.code, + exposure='1' if self.exposure == 'on' else 0, + diameter=self.diameter, + x=self.position[0], + y=self.position[1]) + return '{code},{exposure},{diameter},{x},{y}*'.format(**data) + + def to_primitive(self, units): + return Circle((self.position), self.diameter, units=units, level_polarity=self._level_polarity) + + +class AMVectorLinePrimitive(AMPrimitive): + """ Aperture Macro Vector Line primitive. Code 2 or 20. + + A vector line is a rectangle defined by its line width, start, and end + points. The line ends are rectangular. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.3:** Vector Line, primitive code 2 or 20. + + Parameters + ---------- + code : int + Vector Line Primitive code. Must be either 2 or 20. + + exposure : string + 'on' or 'off' + + width : float + Line width + + start : tuple (, ) + coordinate of line start point + + end : tuple (, ) + coordinate of line end point + + rotation : float + Line rotation about the origin. + + Returns + ------- + LinePrimitive : :class:`gerber.am_statements.AMVectorLinePrimitive` + An initialized AMVectorLinePrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(',') + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + width = float(modifiers[2]) + start = (float(modifiers[3]), float(modifiers[4])) + end = (float(modifiers[5]), float(modifiers[6])) + rotation = float(modifiers[7]) + return cls(code, exposure, width, start, end, rotation) + + def __init__(self, code, exposure, width, start, end, rotation): + validate_coordinates(start) + validate_coordinates(end) + if code not in (2, 20): + raise ValueError('VectorLinePrimitive codes are 2 or 20') + super(AMVectorLinePrimitive, self).__init__(code, exposure) + self.width = width + self.start = start + self.end = end + self.rotation = rotation + + def to_inch(self): + self.width = inch(self.width) + self.start = tuple([inch(x) for x in self.start]) + self.end = tuple([inch(x) for x in self.end]) + + def to_metric(self): + self.width = metric(self.width) + self.start = tuple([metric(x) for x in self.start]) + self.end = tuple([metric(x) for x in self.end]) + + def to_gerber(self, settings=None): + fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*' + data = dict(code=self.code, + exp=1 if self.exposure == 'on' else 0, + width=self.width, + startx=self.start[0], + starty=self.start[1], + endx=self.end[0], + endy=self.end[1], + rotation=self.rotation) + return fmtstr.format(**data) + + def to_primitive(self, units): + """ + Convert this to a primitive. We use the Outline to represent this (instead of Line) + because the behaviour of the end caps is different for aperture macros compared to Lines + when rotated. + """ + + # Use a line to generate our vertices easily + line = Line(self.start, self.end, Rectangle(None, self.width, self.width)) + vertices = line.vertices + + aperture = Circle((0, 0), 0) + + lines = [] + prev_point = rotate_point(vertices[-1], self.rotation, (0, 0)) + for point in vertices: + cur_point = rotate_point(point, self.rotation, (0, 0)) + + lines.append(Line(prev_point, cur_point, aperture)) + + return Outline(lines, units=units, level_polarity=self._level_polarity) + + +class AMOutlinePrimitive(AMPrimitive): + """ Aperture Macro Outline primitive. Code 4. + + An outline primitive is an area enclosed by an n-point polygon defined by + its start point and n subsequent points. The outline must be closed, i.e. + the last point must be equal to the start point. Self intersecting + outlines are not allowed. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.6:** Outline, primitive code 4. + + Parameters + ---------- + code : int + OutlinePrimitive code. Must be 6. + + exposure : string + 'on' or 'off' + + start_point : tuple (, ) + coordinate of outline start point + + points : list of tuples (, ) + coordinates of subsequent points + + rotation : float + outline rotation about the origin. + + Returns + ------- + OutlinePrimitive : :class:`gerber.am_statements.AMOutlineinePrimitive` + An initialized AMOutlinePrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + + start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6)) + points = [] + for prim in primitive.primitives: + points.append((round(prim.end[0], 6), round(prim.end[1], 6))) + + rotation = 0.0 + + return cls(4, 'on', start_point, points, rotation) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + + code = int(modifiers[0]) + exposure = "on" if float(modifiers[1]) == 1 else "off" + n = int(float(modifiers[2])) + start_point = (float(modifiers[3]), float(modifiers[4])) + points = [] + for i in range(n): + points.append((float(modifiers[5 + i * 2]), + float(modifiers[5 + i * 2 + 1]))) + rotation = float(modifiers[-1]) + return cls(code, exposure, start_point, points, rotation) + + def __init__(self, code, exposure, start_point, points, rotation): + """ Initialize AMOutlinePrimitive + """ + validate_coordinates(start_point) + for point in points: + validate_coordinates(point) + if code != 4: + raise ValueError('OutlinePrimitive code is 4') + super(AMOutlinePrimitive, self).__init__(code, exposure) + self.start_point = start_point + if points[-1] != start_point: + raise ValueError('OutlinePrimitive must be closed') + self.points = points + self.rotation = rotation + + def to_inch(self): + self.start_point = tuple([inch(x) for x in self.start_point]) + self.points = tuple([(inch(x), inch(y)) for x, y in self.points]) + + def to_metric(self): + self.start_point = tuple([metric(x) for x in self.start_point]) + self.points = tuple([(metric(x), metric(y)) for x, y in self.points]) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure="1" if self.exposure == "on" else "0", + n_points=len(self.points), + start_point="%.6g,%.6g" % self.start_point, + points=",\n".join(["%.6g,%.6g" % point for point in self.points]), + rotation=str(self.rotation) + ) + return "{code},{exposure},{n_points},{start_point},{points},{rotation}*".format(**data) + + def to_primitive(self, units): + """ + Convert this to a drawable primitive. This uses the Outline instead of Line + primitive to handle differences in end caps when rotated. + """ + + lines = [] + prev_point = rotate_point(self.start_point, self.rotation) + for point in self.points: + cur_point = rotate_point(point, self.rotation) + + lines.append(Line(prev_point, cur_point, Circle((0,0), 0))) + + prev_point = cur_point + + if lines[0].start != lines[-1].end: + raise ValueError('Outline must be closed') + + return Outline(lines, units=units, level_polarity=self._level_polarity) + + +class AMPolygonPrimitive(AMPrimitive): + """ Aperture Macro Polygon primitive. Code 5. + + A polygon primitive is a regular polygon defined by the number of + vertices, the center point, and the diameter of the circumscribed circle. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.8:** Polygon, primitive code 5. + + Parameters + ---------- + code : int + PolygonPrimitive code. Must be 5. + + exposure : string + 'on' or 'off' + + vertices : int, 3 <= vertices <= 12 + Number of vertices + + position : tuple (, ) + X and Y coordinates of polygon center + + diameter : float + diameter of circumscribed circle. + + rotation : float + polygon rotation about the origin. + + Returns + ------- + PolygonPrimitive : :class:`gerber.am_statements.AMPolygonPrimitive` + An initialized AMPolygonPrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + exposure = "on" if float(modifiers[1]) == 1 else "off" + vertices = int(float(modifiers[2])) + position = (float(modifiers[3]), float(modifiers[4])) + try: + diameter = float(modifiers[5]) + except: + diameter = 0 + + rotation = float(modifiers[6]) + return cls(code, exposure, vertices, position, diameter, rotation) + + def __init__(self, code, exposure, vertices, position, diameter, rotation): + """ Initialize AMPolygonPrimitive + """ + if code != 5: + raise ValueError('PolygonPrimitive code is 5') + super(AMPolygonPrimitive, self).__init__(code, exposure) + if vertices < 3 or vertices > 12: + raise ValueError('Number of vertices must be between 3 and 12') + self.vertices = vertices + validate_coordinates(position) + self.position = position + self.diameter = diameter + self.rotation = rotation + + def to_inch(self): + self.position = tuple([inch(x) for x in self.position]) + self.diameter = inch(self.diameter) + + def to_metric(self): + self.position = tuple([metric(x) for x in self.position]) + self.diameter = metric(self.diameter) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure="1" if self.exposure == "on" else "0", + vertices=self.vertices, + position="%.4g,%.4g" % self.position, + diameter='%.4g' % self.diameter, + rotation=str(self.rotation) + ) + fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*" + return fmt.format(**data) + + def to_primitive(self, units): + return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity) + + +class AMMoirePrimitive(AMPrimitive): + """ Aperture Macro Moire primitive. Code 6. + + The moire primitive is a cross hair centered on concentric rings (annuli). + Exposure is always on. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.9:** Moire, primitive code 6. + + Parameters + ---------- + code : int + Moire Primitive code. Must be 6. + + position : tuple (, ) + X and Y coordinates of moire center + + diameter : float + outer diameter of outer ring. + + ring_thickness : float + thickness of concentric rings. + + gap : float + gap between concentric rings. + + max_rings : float + maximum number of rings + + crosshair_thickness : float + thickness of crosshairs + + crosshair_length : float + length of crosshairs + + rotation : float + moire rotation about the origin. + + Returns + ------- + MoirePrimitive : :class:`gerber.am_statements.AMMoirePrimitive` + An initialized AMMoirePrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + position = (float(modifiers[1]), float(modifiers[2])) + diameter = float(modifiers[3]) + ring_thickness = float(modifiers[4]) + gap = float(modifiers[5]) + max_rings = int(float(modifiers[6])) + crosshair_thickness = float(modifiers[7]) + crosshair_length = float(modifiers[8]) + rotation = float(modifiers[9]) + return cls(code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation) + + def __init__(self, code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation): + """ Initialize AMoirePrimitive + """ + if code != 6: + raise ValueError('MoirePrimitive code is 6') + super(AMMoirePrimitive, self).__init__(code, 'on') + validate_coordinates(position) + self.position = position + self.diameter = diameter + self.ring_thickness = ring_thickness + self.gap = gap + self.max_rings = max_rings + self.crosshair_thickness = crosshair_thickness + self.crosshair_length = crosshair_length + self.rotation = rotation + + def to_inch(self): + self.position = tuple([inch(x) for x in self.position]) + self.diameter = inch(self.diameter) + self.ring_thickness = inch(self.ring_thickness) + self.gap = inch(self.gap) + self.crosshair_thickness = inch(self.crosshair_thickness) + self.crosshair_length = inch(self.crosshair_length) + + def to_metric(self): + self.position = tuple([metric(x) for x in self.position]) + self.diameter = metric(self.diameter) + self.ring_thickness = metric(self.ring_thickness) + self.gap = metric(self.gap) + self.crosshair_thickness = metric(self.crosshair_thickness) + self.crosshair_length = metric(self.crosshair_length) + + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + position="%.4g,%.4g" % self.position, + diameter=self.diameter, + ring_thickness=self.ring_thickness, + gap=self.gap, + max_rings=self.max_rings, + crosshair_thickness=self.crosshair_thickness, + crosshair_length=self.crosshair_length, + rotation=self.rotation + ) + fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*" + return fmt.format(**data) + + def to_primitive(self, units): + #raise NotImplementedError() + return None + + +class AMThermalPrimitive(AMPrimitive): + """ Aperture Macro Thermal primitive. Code 7. + + The thermal primitive is a ring (annulus) interrupted by four gaps. + Exposure is always on. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.10:** Thermal, primitive code 7. + + Parameters + ---------- + code : int + Thermal Primitive code. Must be 7. + + position : tuple (, ) + X and Y coordinates of thermal center + + outer_diameter : float + outer diameter of thermal. + + inner_diameter : float + inner diameter of thermal. + + gap : float + gap thickness + + rotation : float + thermal rotation about the origin. + + Returns + ------- + ThermalPrimitive : :class:`gerber.am_statements.AMThermalPrimitive` + An initialized AMThermalPrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + position = (float(modifiers[1]), float(modifiers[2])) + outer_diameter = float(modifiers[3]) + inner_diameter = float(modifiers[4]) + gap = float(modifiers[5]) + rotation = float(modifiers[6]) + return cls(code, position, outer_diameter, inner_diameter, gap, rotation) + + def __init__(self, code, position, outer_diameter, inner_diameter, gap, rotation): + if code != 7: + raise ValueError('ThermalPrimitive code is 7') + super(AMThermalPrimitive, self).__init__(code, 'on') + validate_coordinates(position) + self.position = position + self.outer_diameter = outer_diameter + self.inner_diameter = inner_diameter + self.gap = gap + self.rotation = rotation + + def to_inch(self): + self.position = tuple([inch(x) for x in self.position]) + self.outer_diameter = inch(self.outer_diameter) + self.inner_diameter = inch(self.inner_diameter) + self.gap = inch(self.gap) + + def to_metric(self): + self.position = tuple([metric(x) for x in self.position]) + self.outer_diameter = metric(self.outer_diameter) + self.inner_diameter = metric(self.inner_diameter) + self.gap = metric(self.gap) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + position="%.4g,%.4g" % self.position, + outer_diameter=self.outer_diameter, + inner_diameter=self.inner_diameter, + gap=self.gap, + rotation=self.rotation + ) + fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap},{rotation}*" + return fmt.format(**data) + + def _approximate_arc_cw(self, start_angle, end_angle, radius, center): + """ + Get an arc as a series of points + + Parameters + ---------- + start_angle : The start angle in radians + end_angle : The end angle in radians + radius`: Radius of the arc + center : The center point of the arc (x, y) tuple + + Returns + ------- + array of point tuples + """ + + # The total sweep + sweep_angle = end_angle - start_angle + num_steps = 10 + + angle_step = sweep_angle / num_steps + + radius = radius + center = center + + points = [] + + for i in range(num_steps + 1): + current_angle = start_angle + (angle_step * i) + + nextx = (center[0] + math.cos(current_angle) * radius) + nexty = (center[1] + math.sin(current_angle) * radius) + + points.append((nextx, nexty)) + + return points + + def to_primitive(self, units): + + # We start with calculating the top right section, then duplicate it + + inner_radius = self.inner_diameter / 2.0 + outer_radius = self.outer_diameter / 2.0 + + # Calculate the start angle relative to the horizontal axis + inner_offset_angle = asin(self.gap / 2.0 / inner_radius) + outer_offset_angle = asin(self.gap / 2.0 / outer_radius) + + rotation_rad = math.radians(self.rotation) + inner_start_angle = inner_offset_angle + rotation_rad + inner_end_angle = math.pi / 2 - inner_offset_angle + rotation_rad + + outer_start_angle = outer_offset_angle + rotation_rad + outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad + + outlines = [] + aperture = Circle((0, 0), 0) + + points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position) + + list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position)))) + # Add in the last point since outlines should be closed + points.append(points[0]) + + # There are four outlines at rotated sections + for rotation in [0, 90.0, 180.0, 270.0]: + + lines = [] + prev_point = rotate_point(points[0], rotation, self.position) + for point in points[1:]: + cur_point = rotate_point(point, rotation, self.position) + + lines.append(Line(prev_point, cur_point, aperture)) + + prev_point = cur_point + + outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity)) + + return outlines + + +class AMCenterLinePrimitive(AMPrimitive): + """ Aperture Macro Center Line primitive. Code 21. + + The center line primitive is a rectangle defined by its width, height, and center point. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.4:** Center Line, primitive code 21. + + Parameters + ---------- + code : int + Center Line Primitive code. Must be 21. + + exposure : str + 'on' or 'off' + + width : float + Width of rectangle + + height : float + Height of rectangle + + center : tuple (, ) + X and Y coordinates of line center + + rotation : float + rectangle rotation about its center. + + Returns + ------- + CenterLinePrimitive : :class:`gerber.am_statements.AMCenterLinePrimitive` + An initialized AMCenterLinePrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + width = primitive.width + height = primitive.height + center = primitive.position + rotation = math.degrees(primitive.rotation) + return cls(21, 'on', width, height, center, rotation) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + width = float(modifiers[2]) + height = float(modifiers[3]) + center = (float(modifiers[4]), float(modifiers[5])) + rotation = float(modifiers[6]) + return cls(code, exposure, width, height, center, rotation) + + def __init__(self, code, exposure, width, height, center, rotation): + if code != 21: + raise ValueError('CenterLinePrimitive code is 21') + super(AMCenterLinePrimitive, self).__init__(code, exposure) + self.width = width + self.height = height + validate_coordinates(center) + self.center = center + self.rotation = rotation + + def to_inch(self): + self.center = tuple([inch(x) for x in self.center]) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.center = tuple([metric(x) for x in self.center]) + self.width = metric(self.width) + self.height = metric(self.height) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure = '1' if self.exposure == 'on' else '0', + width = self.width, + height = self.height, + center="%.4g,%.4g" % self.center, + rotation=self.rotation + ) + fmt = "{code},{exposure},{width},{height},{center},{rotation}*" + return fmt.format(**data) + + def to_primitive(self, units): + + x = self.center[0] + y = self.center[1] + half_width = self.width / 2.0 + half_height = self.height / 2.0 + + points = [] + points.append((x - half_width, y + half_height)) + points.append((x - half_width, y - half_height)) + points.append((x + half_width, y - half_height)) + points.append((x + half_width, y + half_height)) + + aperture = Circle((0, 0), 0) + + lines = [] + prev_point = rotate_point(points[3], self.rotation, self.center) + for point in points: + cur_point = rotate_point(point, self.rotation, self.center) + + lines.append(Line(prev_point, cur_point, aperture)) + + return Outline(lines, units=units, level_polarity=self._level_polarity) + + +class AMLowerLeftLinePrimitive(AMPrimitive): + """ Aperture Macro Lower Left Line primitive. Code 22. + + The lower left line primitive is a rectangle defined by its width, height, and the lower left point. + + .. seealso:: + `The Gerber File Format Specification `_ + **Section 4.12.3.5:** Lower Left Line, primitive code 22. + + Parameters + ---------- + code : int + Center Line Primitive code. Must be 22. + + exposure : str + 'on' or 'off' + + width : float + Width of rectangle + + height : float + Height of rectangle + + lower_left : tuple (, ) + X and Y coordinates of lower left corner + + rotation : float + rectangle rotation about its origin. + + Returns + ------- + LowerLeftLinePrimitive : :class:`gerber.am_statements.AMLowerLeftLinePrimitive` + An initialized AMLowerLeftLinePrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + width = float(modifiers[2]) + height = float(modifiers[3]) + lower_left = (float(modifiers[4]), float(modifiers[5])) + rotation = float(modifiers[6]) + return cls(code, exposure, width, height, lower_left, rotation) + + def __init__(self, code, exposure, width, height, lower_left, rotation): + if code != 22: + raise ValueError('LowerLeftLinePrimitive code is 22') + super (AMLowerLeftLinePrimitive, self).__init__(code, exposure) + self.width = width + self.height = height + validate_coordinates(lower_left) + self.lower_left = lower_left + self.rotation = rotation + + def to_inch(self): + self.lower_left = tuple([inch(x) for x in self.lower_left]) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.lower_left = tuple([metric(x) for x in self.lower_left]) + self.width = metric(self.width) + self.height = metric(self.height) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure = '1' if self.exposure == 'on' else '0', + width = self.width, + height = self.height, + lower_left="%.4g,%.4g" % self.lower_left, + rotation=self.rotation + ) + fmt = "{code},{exposure},{width},{height},{lower_left},{rotation}*" + return fmt.format(**data) + + +class AMUnsupportPrimitive(AMPrimitive): + @classmethod + def from_gerber(cls, primitive): + return cls(primitive) + + def __init__(self, primitive): + super(AMUnsupportPrimitive, self).__init__(9999) + self.primitive = primitive + + def to_inch(self): + pass + + def to_metric(self): + pass + + def to_gerber(self, settings=None): + return self.primitive diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py new file mode 100644 index 0000000..4f20283 --- /dev/null +++ b/gerbonara/gerber/cam.py @@ -0,0 +1,286 @@ +#! /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 + + Parameters + ---------- + notation: string + notation format. either 'absolute' or 'incremental' + + units : string + Measurement units. 'inch' or 'metric' + + zero_suppression: string + 'leading' to suppress leading zeros, 'trailing' to suppress trailing zeros. + This is the convention used in Gerber files. + + format : tuple (int, int) + Decimal format + + zeros : string + 'leading' to include leading zeros, 'trailing to include trailing zeros. + This is the convention used in Excellon files + + Notes + ----- + Either `zeros` or `zero_suppression` should be specified, there is no need to + specify both. `zero_suppression` will take on the opposite value of `zeros` + and vice versa + """ + + def __init__(self, notation='absolute', units='inch', + zero_suppression=None, format=(2, 5), zeros=None, + angle_units='degrees'): + if notation not in ['absolute', 'incremental']: + raise ValueError('Notation must be either absolute or incremental') + self.notation = notation + + if units not in ['inch', 'metric']: + raise ValueError('Units must be either inch or metric') + self.units = units + + if zero_suppression is None and zeros is None: + self.zero_suppression = 'trailing' + + elif zero_suppression == zeros: + raise ValueError('Zeros and Zero Suppression must be different. \ + Best practice is to specify only one.') + + elif zero_suppression is not None: + if zero_suppression not in ['leading', 'trailing']: + # This is a common problem in Eagle files, so just suppress it + self.zero_suppression = 'leading' + else: + self.zero_suppression = zero_suppression + + elif zeros is not None: + if zeros not in ['leading', 'trailing']: + raise ValueError('Zeros must be either leading or trailling') + self.zeros = zeros + + if len(format) != 2: + raise ValueError('Format must be a tuple(n=2) of integers') + self.format = format + + if angle_units not in ('degrees', 'radians'): + raise ValueError('Angle units may be degrees or radians') + self.angle_units = angle_units + + @property + def zero_suppression(self): + return self._zero_suppression + + @zero_suppression.setter + def zero_suppression(self, value): + self._zero_suppression = value + self._zeros = 'leading' if value == 'trailing' else 'trailing' + + @property + def zeros(self): + return self._zeros + + @zeros.setter + def zeros(self, value): + + self._zeros = value + self._zero_suppression = 'leading' if value == 'trailing' else 'trailing' + + def __getitem__(self, key): + if key == 'notation': + return self.notation + elif key == 'units': + return self.units + elif key == 'zero_suppression': + return self.zero_suppression + elif key == 'zeros': + return self.zeros + elif key == 'format': + return self.format + elif key == 'angle_units': + return self.angle_units + else: + raise KeyError() + + 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 == 'zeros': + if value not in ['leading', 'trailing']: + raise ValueError('Zeros must be either leading or trailling') + self.zeros = value + + elif key == 'format': + if len(value) != 2: + raise ValueError('Format must be a tuple(n=2) of integers') + self.format = value + + elif key == 'angle_units': + if value not in ('degrees', 'radians'): + raise ValueError('Angle units may be degrees or radians') + self.angle_units = value + + else: + raise KeyError('%s is not a valid key' % key) + + def __eq__(self, other): + return (self.notation == other.notation and + self.units == other.units and + self.zero_suppression == other.zero_suppression and + self.format == other.format and + self.angle_units == other.angle_units) + + def __str__(self): + return ('' % + (self.units, self.notation, self.zero_suppression, self.format, self.angle_units)) + + +class CamFile(object): + """ Base class for Gerber/Excellon files. + + Provides a common set of settings parameters. + + Parameters + ---------- + settings : FileSettings + The current file configuration. + + primitives : iterable + List of primitives in the file. + + 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, primitives=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.zeros = settings['zeros'] + self.format = settings['format'] + else: + self.notation = 'absolute' + self.units = 'inch' + self.zero_suppression = 'trailing' + self.zeros = 'leading' + self.format = (2, 5) + self.statements = statements if statements is not None else [] + if primitives is not None: + self.primitives = primitives + 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) + + @property + def bounds(self): + """ File boundaries + """ + pass + + @property + def bounding_box(self): + pass + + def to_inch(self): + pass + + def to_metric(self): + pass + + def render(self, ctx=None, invert=False, 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` + """ + if ctx is None: + from .render import GerberCairoContext + ctx = GerberCairoContext() + ctx.set_bounds(self.bounding_box) + ctx.paint_background() + ctx.invert = invert + ctx.new_render_layer() + for p in self.primitives: + ctx.render(p) + ctx.flatten() + + if filename is not None: + ctx.dump(filename) diff --git a/gerbonara/gerber/common.py b/gerbonara/gerber/common.py new file mode 100644 index 0000000..f496809 --- /dev/null +++ b/gerbonara/gerber/common.py @@ -0,0 +1,71 @@ +#! /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 . import rs274x +from . import excellon +from . import ipc356 +from .exceptions import ParseError +from .utils import detect_file_format + + +def read(filename): + """ Read a gerber or excellon file and return a representative object. + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + file : CncFile subclass + CncFile object representing the file, either GerberFile, ExcellonFile, + or IPCNetlist. Returns None if file is not of the proper type. + """ + with open(filename, 'rU') as f: + data = f.read() + return loads(data, filename) + + +def loads(data, filename=None): + """ Read gerber or excellon file contents from a string and return a + representative object. + + Parameters + ---------- + data : string + Source file contents as a string. + + filename : string, optional + String containing the filename of the data source. + + Returns + ------- + file : CncFile subclass + CncFile object representing the data, either GerberFile, ExcellonFile, + or IPCNetlist. Returns None if data is not of the proper type. + """ + + fmt = detect_file_format(data) + if fmt == 'rs274x': + return rs274x.loads(data, filename=filename) + elif fmt == 'excellon': + return excellon.loads(data, filename=filename) + elif fmt == 'ipc_d_356': + return ipc356.loads(data, filename=filename) + else: + raise ParseError('Unable to detect file format') diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py new file mode 100755 index 0000000..5ab062a --- /dev/null +++ b/gerbonara/gerber/excellon.py @@ -0,0 +1,904 @@ +#!/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. + +""" +Excellon File module +==================== +**Excellon file classes** + +This module provides Excellon file classes and parsing utilities +""" + +import math +import operator + +from .cam import CamFile, FileSettings +from .excellon_statements import * +from .excellon_tool import ExcellonToolDefinitionParser +from .primitives import Drill, Slot +from .utils import inch, metric + + +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + + + +def read(filename): + """ Read data from filename and return an ExcellonFile + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.excellon.ExcellonFile` + An ExcellonFile created from the specified file. + + """ + # File object should use settings from source file by default. + with open(filename, 'rU') as f: + data = f.read() + settings = FileSettings(**detect_excellon_format(data)) + return ExcellonParser(settings).parse(filename) + +def loads(data, filename=None, settings=None, tools=None): + """ Read data from string and return an ExcellonFile + Parameters + ---------- + data : string + string containing Excellon file contents + + filename : string, optional + string containing the filename of the data source + + tools: dict (optional) + externally defined tools + + Returns + ------- + file : :class:`gerber.excellon.ExcellonFile` + An ExcellonFile created from the specified file. + + """ + # File object should use settings from source file by default. + if not settings: + settings = FileSettings(**detect_excellon_format(data)) + return ExcellonParser(settings, tools).parse_raw(data, filename) + + +class DrillHit(object): + """Drill feature that is a single drill hole. + + Attributes + ---------- + tool : ExcellonTool + Tool to drill the hole. Defines the size of the hole that is generated. + position : tuple(float, float) + Center position of the drill. + + """ + def __init__(self, tool, position): + self.tool = tool + self.position = position + + def to_inch(self): + if self.tool.settings.units == 'metric': + self.tool.to_inch() + self.position = tuple(map(inch, self.position)) + + def to_metric(self): + if self.tool.settings.units == 'inch': + self.tool.to_metric() + self.position = tuple(map(metric, self.position)) + + @property + def bounding_box(self): + position = self.position + radius = self.tool.diameter / 2. + + min_x = position[0] - radius + max_x = position[0] + radius + min_y = position[1] - radius + max_y = position[1] + radius + return ((min_x, max_x), (min_y, max_y)) + + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(operator.add, self.position, (x_offset, y_offset))) + + def __str__(self): + return 'Hit (%f, %f) {%s}' % (self.position[0], self.position[1], self.tool) + +class DrillSlot(object): + """ + A slot is created between two points. The way the slot is created depends on the statement used to create it + """ + + TYPE_ROUT = 1 + TYPE_G85 = 2 + + def __init__(self, tool, start, end, slot_type): + self.tool = tool + self.start = start + self.end = end + self.slot_type = slot_type + + def to_inch(self): + if self.tool.settings.units == 'metric': + self.tool.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + + def to_metric(self): + if self.tool.settings.units == 'inch': + self.tool.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + + @property + def bounding_box(self): + start = self.start + end = self.end + radius = self.tool.diameter / 2. + min_x = min(start[0], end[0]) - radius + max_x = max(start[0], end[0]) + radius + min_y = min(start[1], end[1]) - radius + max_y = max(start[1], end[1]) + radius + return ((min_x, max_x), (min_y, max_y)) + + def offset(self, x_offset=0, y_offset=0): + self.start = tuple(map(operator.add, self.start, (x_offset, y_offset))) + self.end = tuple(map(operator.add, self.end, (x_offset, y_offset))) + + +class ExcellonFile(CamFile): + """ A class representing a single excellon file + + The ExcellonFile class represents a single excellon file. + + http://www.excellon.com/manuals/program.htm + (archived version at https://web.archive.org/web/20150920001043/http://www.excellon.com/manuals/program.htm) + + Parameters + ---------- + tools : list + 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, statements, tools, hits, settings, filename=None): + super(ExcellonFile, self).__init__(statements=statements, + settings=settings, + filename=filename) + self.tools = tools + self.hits = hits + + @property + def primitives(self): + """ + Gets the primitives. Note that unlike Gerber, this generates new objects + """ + primitives = [] + for hit in self.hits: + if isinstance(hit, DrillHit): + primitives.append(Drill(hit.position, hit.tool.diameter, + units=self.settings.units)) + elif isinstance(hit, DrillSlot): + primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, + units=self.settings.units)) + else: + raise ValueError('Unknown hit type') + return primitives + + @property + def bounding_box(self): + xmin = ymin = 100000000000 + xmax = ymax = -100000000000 + for hit in self.hits: + bbox = hit.bounding_box + xmin = min(bbox[0][0], xmin) + xmax = max(bbox[0][1], xmax) + ymin = min(bbox[1][0], ymin) + ymax = max(bbox[1][1], ymax) + return ((xmin, xmax), (ymin, ymax)) + + def report(self, filename=None): + """ Print or save drill report + """ + if self.settings.units == 'inch': + toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}in.\n' % self.settings.format + else: + toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}mm\n' % self.settings.format + rprt = '=====================\nExcellon Drill Report\n=====================\n' + if self.filename is not None: + rprt += 'NC Drill File: %s\n\n' % self.filename + rprt += 'Drill File Info:\n----------------\n' + rprt += (' Data Mode %s\n' % 'Absolute' + if self.settings.notation == 'absolute' else 'Incremental') + rprt += (' Units %s\n' % 'Inches' + if self.settings.units == 'inch' else 'Millimeters') + rprt += '\nTool List:\n----------\n\n' + rprt += ' Code Size Hits Path Length\n' + rprt += ' --------------------------------------\n' + for tool in iter(self.tools.values()): + rprt += toolfmt.format(tool.number, tool.diameter, + tool.hit_count, self.path_length(tool.number)) + if filename is not None: + with open(filename, 'w') as f: + f.write(rprt) + return rprt + + def write(self, filename=None): + filename = filename if filename is not None else self.filename + with open(filename, 'w') as f: + + # Copy the header verbatim + for statement in self.statements: + if not isinstance(statement, ToolSelectionStmt): + f.write(statement.to_excellon(self.settings) + '\n') + else: + break + + # Write out coordinates for drill hits by tool + for tool in iter(self.tools.values()): + f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n') + for hit in self.hits: + if hit.tool.number == tool.number: + f.write(CoordinateStmt( + *hit.position).to_excellon(self.settings) + '\n') + f.write(EndOfProgramStmt().to_excellon() + '\n') + + def to_inch(self): + """ + Convert units to inches + """ + if self.units != 'inch': + for statement in self.statements: + statement.to_inch() + for tool in iter(self.tools.values()): + tool.to_inch() + #for primitive in self.primitives: + # primitive.to_inch() + #for hit in self.hits: + # hit.to_inch() + self.units = 'inch' + + def to_metric(self): + """ Convert units to metric + """ + if self.units != 'metric': + for statement in self.statements: + statement.to_metric() + for tool in iter(self.tools.values()): + tool.to_metric() + #for primitive in self.primitives: + # print("Converting to metric: {}".format(primitive)) + # primitive.to_metric() + # print(primitive) + for hit in self.hits: + hit.to_metric() + self.units = 'metric' + + def offset(self, x_offset=0, y_offset=0): + for statement in self.statements: + statement.offset(x_offset, y_offset) + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) + for hit in self. hits: + hit.offset(x_offset, y_offset) + + def path_length(self, tool_number=None): + """ Return the path length for a given tool + """ + lengths = {} + positions = {} + for hit in self.hits: + tool = hit.tool + num = tool.number + positions[num] = ((0, 0) if positions.get(num) is None + else positions[num]) + lengths[num] = 0.0 if lengths.get(num) is None else lengths[num] + lengths[num] = lengths[ + num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position))) + positions[num] = hit.position + + if tool_number is None: + return lengths + else: + return lengths.get(tool_number) + + def hit_count(self, tool_number=None): + counts = {} + for tool in iter(self.tools.values()): + counts[tool.number] = tool.hit_count + if tool_number is None: + return counts + else: + return counts.get(tool_number) + + def update_tool(self, tool_number, **kwargs): + """ Change parameters of a tool + """ + if kwargs.get('feed_rate') is not None: + self.tools[tool_number].feed_rate = kwargs.get('feed_rate') + if kwargs.get('retract_rate') is not None: + self.tools[tool_number].retract_rate = kwargs.get('retract_rate') + if kwargs.get('rpm') is not None: + self.tools[tool_number].rpm = kwargs.get('rpm') + if kwargs.get('diameter') is not None: + self.tools[tool_number].diameter = kwargs.get('diameter') + if kwargs.get('max_hit_count') is not None: + self.tools[tool_number].max_hit_count = kwargs.get('max_hit_count') + if kwargs.get('depth_offset') is not None: + self.tools[tool_number].depth_offset = kwargs.get('depth_offset') + # Update drill hits + newtool = self.tools[tool_number] + for hit in self.hits: + if hit.tool.number == newtool.number: + hit.tool = newtool + + +class ExcellonParser(object): + """ Excellon File Parser + + Parameters + ---------- + settings : FileSettings or dict-like + Excellon file settings to use when interpreting the excellon file. + """ + def __init__(self, settings=None, ext_tools=None): + self.notation = 'absolute' + self.units = 'inch' + self.zeros = 'leading' + self.format = (2, 4) + self.state = 'INIT' + self.statements = [] + self.tools = {} + self.ext_tools = ext_tools or {} + self.comment_tools = {} + self.hits = [] + self.active_tool = None + self.pos = [0., 0.] + self.drill_down = False + self._previous_line = '' + # Default for plated is None, which means we don't know + self.plated = ExcellonTool.PLATED_UNKNOWN + if settings is not None: + self.units = settings.units + self.zeros = settings.zeros + self.notation = settings.notation + self.format = settings.format + + @property + def coordinates(self): + return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)] + + @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, 'rU') as f: + data = f.read() + return self.parse_raw(data, filename) + + def parse_raw(self, data, filename=None): + for line in StringIO(data): + self._parse_line(line.strip()) + for stmt in self.statements: + stmt.units = self.units + return ExcellonFile(self.statements, self.tools, self.hits, + self._settings(), filename) + + def _parse_line(self, line): + # skip empty lines + # Prepend previous line's data... + line = '{}{}'.format(self._previous_line, line) + self._previous_line = '' + + # Skip empty lines + if not line.strip(): + return + + if line[0] == ';': + comment_stmt = CommentStmt.from_excellon(line) + self.statements.append(comment_stmt) + + # get format from altium comment + if "FILE_FORMAT" in comment_stmt.comment: + detected_format = tuple( + [int(x) for x in comment_stmt.comment.split('=')[1].split(":")]) + if detected_format: + self.format = detected_format + + if "TYPE=PLATED" in comment_stmt.comment: + self.plated = ExcellonTool.PLATED_YES + + if "TYPE=NON_PLATED" in comment_stmt.comment: + self.plated = ExcellonTool.PLATED_NO + + if "HEADER:" in comment_stmt.comment: + self.state = "HEADER" + + if " Holesize " in comment_stmt.comment: + self.state = "HEADER" + + # Parse this as a hole definition + tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment) + if len(tools) == 1: + tool = tools[tools.keys()[0]] + self._add_comment_tool(tool) + + elif line[:3] == 'M48': + self.statements.append(HeaderBeginStmt()) + self.state = 'HEADER' + + elif line[0] == '%': + self.statements.append(RewindStopStmt()) + if self.state == 'HEADER': + self.state = 'DRILL' + elif self.state == 'INIT': + self.state = 'HEADER' + + elif line[:3] == 'M00' and self.state == 'DRILL': + if self.active_tool: + cur_tool_number = self.active_tool.number + next_tool = self._get_tool(cur_tool_number + 1) + + self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool)) + self.active_tool = next_tool + else: + raise Exception('Invalid state exception') + + elif line[:3] == 'M95': + self.statements.append(HeaderEndStmt()) + if self.state == 'HEADER': + self.state = 'DRILL' + + elif line[:3] == 'M15': + self.statements.append(ZAxisRoutPositionStmt()) + self.drill_down = True + + elif line[:3] == 'M16': + self.statements.append(RetractWithClampingStmt()) + self.drill_down = False + + elif line[:3] == 'M17': + self.statements.append(RetractWithoutClampingStmt()) + self.drill_down = False + + elif line[:3] == 'M30': + stmt = EndOfProgramStmt.from_excellon(line, self._settings()) + self.statements.append(stmt) + + elif line[:3] == 'G00': + # Coordinates may be on the next line + if line.strip() == 'G00': + self._previous_line = line + return + + self.statements.append(RouteModeStmt()) + self.state = 'ROUT' + + stmt = CoordinateStmt.from_excellon(line[3:], self._settings()) + stmt.mode = self.state + + x = stmt.x + y = stmt.y + self.statements.append(stmt) + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + + elif line[:3] == 'G01': + + # Coordinates might be on the next line... + if line.strip() == 'G01': + self._previous_line = line + return + + self.statements.append(RouteModeStmt()) + self.state = 'LINEAR' + + stmt = CoordinateStmt.from_excellon(line[3:], self._settings()) + stmt.mode = self.state + + # The start position is where we were before the rout command + start = (self.pos[0], self.pos[1]) + + x = stmt.x + y = stmt.y + self.statements.append(stmt) + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + + # Our ending position + end = (self.pos[0], self.pos[1]) + + if self.drill_down: + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT)) + self.active_tool._hit() + + elif line[:3] == 'G05': + self.statements.append(DrillModeStmt()) + self.drill_down = False + self.state = 'DRILL' + + elif 'INCH' in line or 'METRIC' in line: + stmt = UnitStmt.from_excellon(line) + self.units = stmt.units + self.zeros = stmt.zeros + if stmt.format: + self.format = stmt.format + 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) + + 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) + self.format = stmt.format_tuple + + elif line[:3] == 'G40': + self.statements.append(CutterCompensationOffStmt()) + + elif line[:3] == 'G41': + self.statements.append(CutterCompensationLeftStmt()) + + elif line[:3] == 'G42': + self.statements.append(CutterCompensationRightStmt()) + + elif line[:3] == 'G90': + self.statements.append(AbsoluteModeStmt()) + self.notation = 'absolute' + + elif line[0] == 'F': + infeed_rate_stmt = ZAxisInfeedRateStmt.from_excellon(line) + self.statements.append(infeed_rate_stmt) + + elif line[0] == 'T' and self.state == 'HEADER': + if not ',OFF' in line and not ',ON' in line: + tool = ExcellonTool.from_excellon(line, self._settings(), None, self.plated) + self._merge_properties(tool) + self.tools[tool.number] = tool + self.statements.append(tool) + else: + self.statements.append(UnknownStmt.from_excellon(line)) + + elif line[0] == 'T' and self.state != 'HEADER': + stmt = ToolSelectionStmt.from_excellon(line) + self.statements.append(stmt) + + # T0 is used as END marker, just ignore + if stmt.tool != 0: + tool = self._get_tool(stmt.tool) + + if not tool: + # FIXME: for weird files with no tools defined, original calc from gerb + if self._settings().units == "inch": + diameter = (16 + 8 * stmt.tool) / 1000.0 + else: + diameter = metric((16 + 8 * stmt.tool) / 1000.0) + + tool = ExcellonTool( + self._settings(), number=stmt.tool, diameter=diameter) + self.tools[tool.number] = tool + + # FIXME: need to add this tool definition inside header to + # make sure it is properly written + for i, s in enumerate(self.statements): + if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool): + self.statements.insert(i, tool) + break + + self.active_tool = tool + + elif line[0] == 'R' and self.state != 'HEADER': + stmt = RepeatHoleStmt.from_excellon(line, self._settings()) + self.statements.append(stmt) + for i in range(stmt.count): + self.pos[0] += stmt.xdelta if stmt.xdelta is not None else 0 + self.pos[1] += stmt.ydelta if stmt.ydelta is not None else 0 + self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) + self.active_tool._hit() + + elif line[0] in ['X', 'Y']: + if 'G85' in line: + stmt = SlotStmt.from_excellon(line, self._settings()) + + # I don't know if this is actually correct, but it makes sense + # that this is where the tool would end + x = stmt.x_end + y = stmt.y_end + + self.statements.append(stmt) + + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + + if self.state == 'DRILL' or self.state == 'HEADER': + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end), DrillSlot.TYPE_G85)) + self.active_tool._hit() + else: + stmt = CoordinateStmt.from_excellon(line, self._settings()) + + # We need this in case we are in rout mode + start = (self.pos[0], self.pos[1]) + + x = stmt.x + y = stmt.y + self.statements.append(stmt) + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + + if self.state == 'LINEAR' and self.drill_down: + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT)) + + elif self.state == 'DRILL' or self.state == 'HEADER': + # Yes, drills in the header doesn't follow the specification, but it there are many + # files like this + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) + self.active_tool._hit() + + else: + self.statements.append(UnknownStmt.from_excellon(line)) + + def _settings(self): + return FileSettings(units=self.units, format=self.format, + zeros=self.zeros, notation=self.notation) + + def _add_comment_tool(self, tool): + """ + Add a tool that was defined in the comments to this file. + + If we have already found this tool, then we will merge this comment tool definition into + the information for the tool + """ + + existing = self.tools.get(tool.number) + if existing and existing.plated == None: + existing.plated = tool.plated + + self.comment_tools[tool.number] = tool + + def _merge_properties(self, tool): + """ + When we have externally defined tools, merge the properties of that tool into this one + + For now, this is only plated + """ + + if tool.plated == ExcellonTool.PLATED_UNKNOWN: + ext_tool = self.ext_tools.get(tool.number) + + if ext_tool: + tool.plated = ext_tool.plated + + def _get_tool(self, toolid): + + tool = self.tools.get(toolid) + if not tool: + tool = self.comment_tools.get(toolid) + if tool: + tool.settings = self._settings() + self.tools[toolid] = tool + + if not tool: + tool = self.ext_tools.get(toolid) + if tool: + tool.settings = self._settings() + self.tools[toolid] = tool + + return tool + +def detect_excellon_format(data=None, filename=None): + """ Detect excellon file decimal format and zero-suppression settings. + + Parameters + ---------- + data : string + String containing contents of Excellon file. + + 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 + zeros_options = ('leading', 'trailing', ) + format_options = ((2, 4), (2, 5), (3, 3),) + + if data is None and filename is None: + raise ValueError('Either data or filename arguments must be provided') + if data is None: + with open(filename, 'rU') as f: + data = f.read() + + # Check for obvious clues: + p = ExcellonParser() + p.parse_raw(data) + + # Get zero_suppression from a unit statement + zero_statements = [stmt.zeros 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, 'zeros': detected_zeros} + + # Only look at remaining options + if detected_format is not None: + format_options = (detected_format,) + if detected_zeros is not None: + zeros_options = (detected_zeros,) + + # Brute force all remaining options, and pick the best looking one... + for zeros in zeros_options: + for fmt in format_options: + key = (fmt, zeros) + settings = FileSettings(zeros=zeros, format=fmt) + try: + p = ExcellonParser(settings) + ef = p.parse_raw(data) + size = tuple([t[0] - t[1] for t in ef.bounding_box]) + hole_area = 0.0 + for hit in p.hits: + tool = hit.tool + hole_area += math.pow(math.pi * tool.diameter / 2., 2) + results[key] = (size, p.hole_count, hole_area) + except: + pass + + # See if any of the dimensions are left with only a single option + formats = set(key[0] for key in iter(results.keys())) + zeros = set(key[1] for key in iter(results.keys())) + if len(formats) == 1: + detected_format = formats.pop() + if len(zeros) == 1: + 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, 'zeros': 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 iter(scores.keys()): + if scores[key] == minscore: + return {'format': key[0], 'zeros': 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] + if board_area == 0: + return 0 + + hole_percentage = hole_area / board_area + hole_score = (hole_percentage - 0.25) ** 2 + size_score = (board_area - 8) ** 2 + return hole_score * size_score diff --git a/gerbonara/gerber/excellon_report/excellon_drr.py b/gerbonara/gerber/excellon_report/excellon_drr.py new file mode 100644 index 0000000..ab9e857 --- /dev/null +++ b/gerbonara/gerber/excellon_report/excellon_drr.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Garret Fick + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Excellon DRR File module +==================== +**Excellon file classes** + +Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information +""" + diff --git a/gerbonara/gerber/excellon_settings.py b/gerbonara/gerber/excellon_settings.py new file mode 100644 index 0000000..4dbe0ca --- /dev/null +++ b/gerbonara/gerber/excellon_settings.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from argparse import PARSER + +# Copyright 2015 Garret Fick + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Excellon Settings Definition File module +==================== +**Excellon file classes** + +This module provides Excellon file classes and parsing utilities +""" + +import re +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .cam import FileSettings + +def loads(data): + """ Read settings file information and return an FileSettings + Parameters + ---------- + data : string + string containing Excellon settings file contents + + Returns + ------- + file settings: FileSettings + + """ + + return ExcellonSettingsParser().parse_raw(data) + +def map_coordinates(value): + if value == 'ABSOLUTE': + return 'absolute' + return 'relative' + +def map_units(value): + if value == 'ENGLISH': + return 'inch' + return 'metric' + +def map_boolean(value): + return value == 'YES' + +SETTINGS_KEYS = { + 'INTEGER-PLACES': (int, 'format-int'), + 'DECIMAL-PLACES': (int, 'format-dec'), + 'COORDINATES': (map_coordinates, 'notation'), + 'OUTPUT-UNITS': (map_units, 'units'), + } + +class ExcellonSettingsParser(object): + """Excellon Settings PARSER + + Parameters + ---------- + None + """ + + def __init__(self): + self.values = {} + self.settings = None + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + # Create the FileSettings object + self.settings = FileSettings( + notation=self.values['notation'], + units=self.values['units'], + format=(self.values['format-int'], self.values['format-dec']) + ) + + return self.settings + + def _parse(self, line): + + line_items = line.split() + if len(line_items) == 2: + + item_type_info = SETTINGS_KEYS.get(line_items[0]) + if item_type_info: + # Convert the value to the expected type + item_value = item_type_info[0](line_items[1]) + + self.values[item_type_info[1]] = item_value \ No newline at end of file diff --git a/gerbonara/gerber/excellon_statements.py b/gerbonara/gerber/excellon_statements.py new file mode 100644 index 0000000..2c50ef9 --- /dev/null +++ b/gerbonara/gerber/excellon_statements.py @@ -0,0 +1,979 @@ +#!/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. +""" +Excellon Statements +==================== +**Excellon file statement classes** + +""" + +import re +import uuid +import itertools +from .utils import (parse_gerber_value, write_gerber_value, decimal_string, + inch, metric) + + +__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt', + 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', + 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt', + 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt', + 'MeasuringModeStmt', 'RouteModeStmt', 'LinearModeStmt', 'DrillModeStmt', + 'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt', + 'ExcellonStatement', 'ZAxisRoutPositionStmt', + 'RetractWithClampingStmt', 'RetractWithoutClampingStmt', + 'CutterCompensationOffStmt', 'CutterCompensationLeftStmt', + 'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt', + 'NextToolSelectionStmt', 'SlotStmt'] + + +class ExcellonStatement(object): + """ Excellon Statement abstract base class + """ + + @classmethod + def from_excellon(cls, line): + raise NotImplementedError('from_excellon must be implemented in a ' + 'subclass') + + def __init__(self, unit='inch', id=None): + self.units = unit + self.id = uuid.uuid4().int if id is None else id + + def to_excellon(self, settings=None): + raise NotImplementedError('to_excellon must be implemented in a ' + 'subclass') + + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + + def offset(self, x_offset=0, y_offset=0): + pass + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class ExcellonTool(ExcellonStatement): + """ Excellon Tool class + + 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. + """ + + PLATED_UNKNOWN = None + PLATED_YES = 'plated' + PLATED_NO = 'nonplated' + PLATED_OPTIONAL = 'optional' + + @classmethod + def from_tool(cls, tool): + args = {} + + args['depth_offset'] = tool.depth_offset + args['diameter'] = tool.diameter + args['feed_rate'] = tool.feed_rate + args['max_hit_count'] = tool.max_hit_count + args['number'] = tool.number + args['plated'] = tool.plated + args['retract_rate'] = tool.retract_rate + args['rpm'] = tool.rpm + + return cls(None, **args) + + @classmethod + def from_excellon(cls, line, settings, id=None, plated=None): + """ 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 = pairwise(re.split('([BCFHSTZ])', line)[1:]) + args = {} + args['id'] = id + 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) + elif cmd == 'C': + args['diameter'] = parse_gerber_value(val, nformat, zero_suppression) + elif cmd == 'F': + args['feed_rate'] = parse_gerber_value(val, nformat, zero_suppression) + elif cmd == 'H': + args['max_hit_count'] = parse_gerber_value(val, nformat, zero_suppression) + elif cmd == 'S': + 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, nformat, zero_suppression) + + if plated != ExcellonTool.PLATED_UNKNOWN: + # Sometimees we can can parse the plating status + args['plated'] = plated + 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): + if kwargs.get('id') is not None: + super(ExcellonTool, self).__init__(id=kwargs.get('id')) + self.settings = settings + self.number = kwargs.get('number') + self.feed_rate = kwargs.get('feed_rate') + 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.plated = kwargs.get('plated') + + self.hit_count = 0 + + def to_excellon(self, settings=None): + if self.settings and not settings: + settings = self.settings + fmt = settings.format + zs = settings.zero_suppression + stmt = 'T%02d' % self.number + if self.retract_rate is not None: + stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs) + if self.feed_rate is not None: + stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs) + if self.max_hit_count is not None: + stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs) + 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.diameter is not None: + stmt += 'C%s' % decimal_string(self.diameter, fmt[1], True) + if self.depth_offset is not None: + stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs) + return stmt + + def to_inch(self): + if self.settings.units != 'inch': + self.settings.units = 'inch' + if self.diameter is not None: + self.diameter = inch(self.diameter) + + def to_metric(self): + if self.settings.units != 'metric': + self.settings.units = 'metric' + if self.diameter is not None: + self.diameter = metric(self.diameter) + + def _hit(self): + self.hit_count += 1 + + def equivalent(self, other): + """ + Is the other tool equal to this, ignoring the tool number, and other file specified properties + """ + + if type(self) != type(other): + return False + + return (self.diameter == other.diameter + and self.feed_rate == other.feed_rate + and self.retract_rate == other.retract_rate + and self.rpm == other.rpm + and self.depth_offset == other.depth_offset + and self.max_hit_count == other.max_hit_count + and self.plated == other.plated + and self.settings.units == other.settings.units) + + def __repr__(self): + unit = 'in.' if self.settings.units == 'inch' else 'mm' + fmtstr = '' % self.settings.format + return fmtstr % (self.number, self.diameter, unit) + + +class ToolSelectionStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + """ 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[1:] + compensation_index = None + + # up to 3 characters for tool number (Frizting uses that) + if len(line) <= 3: + tool = int(line) + else: + tool = int(line[:2]) + compensation_index = int(line[2:]) + + return cls(tool, compensation_index, **kwargs) + + def __init__(self, tool, compensation_index=None, **kwargs): + super(ToolSelectionStmt, self).__init__(**kwargs) + tool = int(tool) + compensation_index = (int(compensation_index) if compensation_index + is not None else None) + self.tool = tool + self.compensation_index = compensation_index + + def to_excellon(self, settings=None): + stmt = 'T%02d' % self.tool + if self.compensation_index is not None: + stmt += '%02d' % self.compensation_index + return stmt + +class NextToolSelectionStmt(ExcellonStatement): + + # TODO the statement exists outside of the context of the file, + # so it is imposible to know that it is really the next tool + + def __init__(self, cur_tool, next_tool, **kwargs): + """ + Select the next tool in the wheel. + Parameters + ---------- + cur_tool : the tool that is currently selected + next_tool : the that that is now selected + """ + super(NextToolSelectionStmt, self).__init__(**kwargs) + + self.cur_tool = cur_tool + self.next_tool = next_tool + + def to_excellon(self, settings=None): + stmt = 'M00' + return stmt + +class ZAxisInfeedRateStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + """ Create a ZAxisInfeedRate from an excellon file line. + + Parameters + ---------- + line : string + Line from an Excellon file + + Returns + ------- + z_axis_infeed_rate : ToolSelectionStmt + ToolSelectionStmt representation of `line.` + """ + rate = int(line[1:]) + + return cls(rate, **kwargs) + + def __init__(self, rate, **kwargs): + super(ZAxisInfeedRateStmt, self).__init__(**kwargs) + self.rate = rate + + def to_excellon(self, settings=None): + return 'F%02d' % self.rate + + +class CoordinateStmt(ExcellonStatement): + + @classmethod + def from_point(cls, point, mode=None): + + stmt = cls(point[0], point[1]) + if mode: + stmt.mode = mode + return stmt + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + x_coord = None + y_coord = None + if line[0] == 'X': + splitline = line.strip('X').split('Y') + x_coord = parse_gerber_value(splitline[0], settings.format, + settings.zero_suppression) + if len(splitline) == 2: + y_coord = parse_gerber_value(splitline[1], settings.format, + settings.zero_suppression) + else: + y_coord = parse_gerber_value(line.strip(' Y'), settings.format, + settings.zero_suppression) + c = cls(x_coord, y_coord, **kwargs) + c.units = settings.units + return c + + def __init__(self, x=None, y=None, **kwargs): + super(CoordinateStmt, self).__init__(**kwargs) + self.x = x + self.y = y + self.mode = None + + def to_excellon(self, settings): + stmt = '' + if self.mode == "ROUT": + stmt += "G00" + if self.mode == "LINEAR": + stmt += "G01" + if self.x is not None: + stmt += 'X%s' % write_gerber_value(self.x, settings.format, + settings.zero_suppression) + if self.y is not None: + stmt += 'Y%s' % write_gerber_value(self.y, settings.format, + settings.zero_suppression) + return stmt + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x is not None: + self.x = inch(self.x) + if self.y is not None: + self.y = inch(self.y) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x is not None: + self.x = metric(self.x) + if self.y is not None: + self.y = metric(self.y) + + def offset(self, x_offset=0, y_offset=0): + if self.x is not None: + self.x += x_offset + if self.y is not None: + self.y += y_offset + + def __str__(self): + coord_str = '' + if self.x is not None: + coord_str += 'X: %g ' % self.x + if self.y is not None: + coord_str += 'Y: %g ' % self.y + + return '' % coord_str + + +class RepeatHoleStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + match = re.compile(r'R(?P[0-9]*)X?(?P[+\-]?\d*\.?\d*)?Y?' + '(?P[+\-]?\d*\.?\d*)?').match(line) + stmt = match.groupdict() + count = int(stmt['rcount']) + xdelta = (parse_gerber_value(stmt['xdelta'], settings.format, + settings.zero_suppression) + if stmt['xdelta'] is not '' else None) + ydelta = (parse_gerber_value(stmt['ydelta'], settings.format, + settings.zero_suppression) + if stmt['ydelta'] is not '' else None) + c = cls(count, xdelta, ydelta, **kwargs) + c.units = settings.units + return c + + def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs): + super(RepeatHoleStmt, self).__init__(**kwargs) + self.count = count + self.xdelta = xdelta + self.ydelta = ydelta + + def to_excellon(self, settings): + stmt = 'R%d' % self.count + if self.xdelta is not None and self.xdelta != 0.0: + stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format, + settings.zero_suppression) + if self.ydelta is not None and self.ydelta != 0.0: + stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format, + settings.zero_suppression) + return stmt + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.xdelta is not None: + self.xdelta = inch(self.xdelta) + if self.ydelta is not None: + self.ydelta = inch(self.ydelta) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.xdelta is not None: + self.xdelta = metric(self.xdelta) + if self.ydelta is not None: + self.ydelta = metric(self.ydelta) + + def __str__(self): + return '' % ( + self.count, + self.xdelta if self.xdelta is not None else 0, + self.ydelta if self.ydelta is not None else 0) + + +class CommentStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + return cls(line.lstrip(';')) + + def __init__(self, comment, **kwargs): + super(CommentStmt, self).__init__(**kwargs) + self.comment = comment + + def to_excellon(self, settings=None): + return ';%s' % self.comment + + +class HeaderBeginStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(HeaderBeginStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M48' + + +class HeaderEndStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(HeaderEndStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M95' + + +class RewindStopStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RewindStopStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return '%' + + +class ZAxisRoutPositionStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(ZAxisRoutPositionStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M15' + + +class RetractWithClampingStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RetractWithClampingStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M16' + + +class RetractWithoutClampingStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RetractWithoutClampingStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M17' + + +class CutterCompensationOffStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationOffStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G40' + + +class CutterCompensationLeftStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationLeftStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G41' + + +class CutterCompensationRightStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationRightStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G42' + + +class EndOfProgramStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + match = re.compile(r'M30X?(?P\d*\.?\d*)?Y?' + '(?P\d*\.?\d*)?').match(line) + stmt = match.groupdict() + x = (parse_gerber_value(stmt['x'], settings.format, + settings.zero_suppression) + if stmt['x'] is not '' else None) + y = (parse_gerber_value(stmt['y'], settings.format, + settings.zero_suppression) + if stmt['y'] is not '' else None) + c = cls(x, y, **kwargs) + c.units = settings.units + return c + + def __init__(self, x=None, y=None, **kwargs): + super(EndOfProgramStmt, self).__init__(**kwargs) + self.x = x + self.y = y + + def to_excellon(self, settings=None): + 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) + return stmt + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x is not None: + self.x = inch(self.x) + if self.y is not None: + self.y = inch(self.y) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x is not None: + self.x = metric(self.x) + if self.y is not None: + self.y = metric(self.y) + + def offset(self, x_offset=0, y_offset=0): + if self.x is not None: + self.x += x_offset + if self.y is not None: + self.y += y_offset + + +class UnitStmt(ExcellonStatement): + + @classmethod + def from_settings(cls, settings): + """Create the unit statement from the FileSettings""" + + return cls(settings.units, settings.zeros) + + @classmethod + def from_excellon(cls, line, **kwargs): + units = 'inch' if 'INCH' in line else 'metric' + zeros = 'leading' if 'LZ' in line else 'trailing' + if '0000.00' in line: + format = (4, 2) + elif '000.000' in line: + format = (3, 3) + elif '00.0000' in line: + format = (2, 4) + else: + format = None + return cls(units, zeros, format, **kwargs) + + def __init__(self, units='inch', zeros='leading', format=None, **kwargs): + super(UnitStmt, self).__init__(**kwargs) + self.units = units.lower() + self.zeros = zeros + self.format = format + + def to_excellon(self, settings=None): + # TODO This won't export the invalid format statement if it exists + stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC', + 'LZ' if self.zeros == 'leading' + else 'TZ') + return stmt + + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + + +class IncrementalModeStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs) + + def __init__(self, mode='off', **kwargs): + super(IncrementalModeStmt, self).__init__(**kwargs) + if mode.lower() not in ['on', 'off']: + raise ValueError('Mode may be "on" or "off"') + self.mode = mode + + def to_excellon(self, settings=None): + return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON') + + +class VersionStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + version = int(line.split(',')[1]) + return cls(version, **kwargs) + + def __init__(self, version=1, **kwargs): + super(VersionStmt, self).__init__(**kwargs) + version = int(version) + if version not in [1, 2]: + raise ValueError('Valid versions are 1 or 2') + self.version = version + + def to_excellon(self, settings=None): + return 'VER,%d' % self.version + + +class FormatStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + fmt = int(line.split(',')[1]) + return cls(fmt, **kwargs) + + def __init__(self, format=1, **kwargs): + super(FormatStmt, self).__init__(**kwargs) + format = int(format) + if format not in [1, 2]: + raise ValueError('Valid formats are 1 or 2') + self.format = format + + def to_excellon(self, settings=None): + return 'FMAT,%d' % self.format + + @property + def format_tuple(self): + return (self.format, 6 - self.format) + + +class LinkToolStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + linked = [int(tool) for tool in line.split('/')] + return cls(linked, **kwargs) + + def __init__(self, linked_tools, **kwargs): + super(LinkToolStmt, self).__init__(**kwargs) + self.linked_tools = [int(x) for x in linked_tools] + + def to_excellon(self, settings=None): + return '/'.join([str(x) for x in self.linked_tools]) + + +class MeasuringModeStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + if not ('M71' in line or 'M72' in line): + raise ValueError('Not a measuring mode statement') + return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs) + + def __init__(self, units='inch', **kwargs): + super(MeasuringModeStmt, self).__init__(**kwargs) + units = units.lower() + if units not in ['inch', 'metric']: + raise ValueError('units must be "inch" or "metric"') + self.units = units + + def to_excellon(self, settings=None): + return 'M72' if self.units == 'inch' else 'M71' + + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + + +class RouteModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RouteModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G00' + + +class LinearModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(LinearModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G01' + + +class DrillModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(DrillModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G05' + + +class AbsoluteModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(AbsoluteModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G90' + + +class UnknownStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + return cls(line, **kwargs) + + def __init__(self, stmt, **kwargs): + super(UnknownStmt, self).__init__(**kwargs) + self.stmt = stmt + + def to_excellon(self, settings=None): + return self.stmt + + def __str__(self): + return "" % self.stmt + + +class SlotStmt(ExcellonStatement): + """ + G85 statement. Defines a slot created by multiple drills between two specified points. + + Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn + """ + + @classmethod + def from_points(cls, start, end): + + return cls(start[0], start[1], end[0], end[1]) + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + # Split the line based on the G85 separator + sub_coords = line.split('G85') + (x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings) + (x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings) + + # Some files seem to specify only one of the coordinates + if x_end_coord == None: + x_end_coord = x_start_coord + if y_end_coord == None: + y_end_coord = y_start_coord + + c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs) + c.units = settings.units + return c + + @staticmethod + def parse_sub_coords(line, settings): + + x_coord = None + y_coord = None + + if line[0] == 'X': + splitline = line.strip('X').split('Y') + x_coord = parse_gerber_value(splitline[0], settings.format, + settings.zero_suppression) + if len(splitline) == 2: + y_coord = parse_gerber_value(splitline[1], settings.format, + settings.zero_suppression) + else: + y_coord = parse_gerber_value(line.strip(' Y'), settings.format, + settings.zero_suppression) + + return (x_coord, y_coord) + + + def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs): + super(SlotStmt, self).__init__(**kwargs) + self.x_start = x_start + self.y_start = y_start + self.x_end = x_end + self.y_end = y_end + self.mode = None + + def to_excellon(self, settings): + stmt = '' + + if self.x_start is not None: + stmt += 'X%s' % write_gerber_value(self.x_start, settings.format, + settings.zero_suppression) + if self.y_start is not None: + stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format, + settings.zero_suppression) + + stmt += 'G85' + + if self.x_end is not None: + stmt += 'X%s' % write_gerber_value(self.x_end, settings.format, + settings.zero_suppression) + if self.y_end is not None: + stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format, + settings.zero_suppression) + + return stmt + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x_start is not None: + self.x_start = inch(self.x_start) + if self.y_start is not None: + self.y_start = inch(self.y_start) + if self.x_end is not None: + self.x_end = inch(self.x_end) + if self.y_end is not None: + self.y_end = inch(self.y_end) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x_start is not None: + self.x_start = metric(self.x_start) + if self.y_start is not None: + self.y_start = metric(self.y_start) + if self.x_end is not None: + self.x_end = metric(self.x_end) + if self.y_end is not None: + self.y_end = metric(self.y_end) + + def offset(self, x_offset=0, y_offset=0): + if self.x_start is not None: + self.x_start += x_offset + if self.y_start is not None: + self.y_start += y_offset + if self.x_end is not None: + self.x_end += x_offset + if self.y_end is not None: + self.y_end += y_offset + + def __str__(self): + start_str = '' + if self.x_start is not None: + start_str += 'X: %g ' % self.x_start + if self.y_start is not None: + start_str += 'Y: %g ' % self.y_start + + end_str = '' + if self.x_end is not None: + end_str += 'X: %g ' % self.x_end + if self.y_end is not None: + end_str += 'Y: %g ' % self.y_end + + return '' % (start_str, end_str) + +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)] + """ + a, b = itertools.tee(iterator) + itr = zip(itertools.islice(a, 0, None, 2), itertools.islice(b, 1, None, 2)) + for elem in itr: + yield elem diff --git a/gerbonara/gerber/excellon_tool.py b/gerbonara/gerber/excellon_tool.py new file mode 100644 index 0000000..a9ac450 --- /dev/null +++ b/gerbonara/gerber/excellon_tool.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Garret Fick + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Excellon Tool Definition File module +==================== +**Excellon file classes** + +This module provides Excellon file classes and parsing utilities +""" + +import re +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .excellon_statements import ExcellonTool + +def loads(data, settings=None): + """ Read tool file information and return a map of tools + Parameters + ---------- + data : string + string containing Excellon Tool Definition file contents + + Returns + ------- + dict tool name: ExcellonTool + + """ + return ExcellonToolDefinitionParser(settings).parse_raw(data) + +class ExcellonToolDefinitionParser(object): + """ Excellon File Parser + + Parameters + ---------- + None + """ + + allegro_tool = re.compile(r'(?P[0-9/.]+)\s+(?PP|N)\s+T(?P[0-9]{2})\s+(?P[0-9/.]+)\s+(?P[0-9/.]+)') + allegro_comment_mils = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') + allegro2_comment_mils = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') + allegro_comment_mm = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+') + allegro2_comment_mm = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+') + + matchers = [ + (allegro_tool, 'mils'), + (allegro_comment_mils, 'mils'), + (allegro2_comment_mils, 'mils'), + (allegro_comment_mm, 'mm'), + (allegro2_comment_mm, 'mm'), + ] + + def __init__(self, settings=None): + self.tools = {} + self.settings = settings + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + return self.tools + + def _parse(self, line): + + for matcher in ExcellonToolDefinitionParser.matchers: + m = matcher[0].match(line) + if m: + unit = matcher[1] + + size = float(m.group('size')) + platedstr = m.group('plated') + toolid = int(m.group('toolid')) + xtol = float(m.group('xtol')) + ytol = float(m.group('ytol')) + + size = self._convert_length(size, unit) + xtol = self._convert_length(xtol, unit) + ytol = self._convert_length(ytol, unit) + + if platedstr == 'PLATED': + plated = ExcellonTool.PLATED_YES + elif platedstr == 'NON_PLATED': + plated = ExcellonTool.PLATED_NO + elif platedstr == 'OPTIONAL': + plated = ExcellonTool.PLATED_OPTIONAL + else: + plated = ExcellonTool.PLATED_UNKNOWN + + tool = ExcellonTool(None, number=toolid, diameter=size, + plated=plated) + + self.tools[tool.number] = tool + + break + + def _convert_length(self, value, unit): + + # Convert the value to mm + if unit == 'mils': + value /= 39.3700787402 + + # Now convert to the settings unit + if self.settings.units == 'inch': + return value / 25.4 + else: + # Already in mm + return value + +def loads_rep(data, settings=None): + """ Read tool report information generated by PADS and return a map of tools + Parameters + ---------- + data : string + string containing Excellon Report file contents + + Returns + ------- + dict tool name: ExcellonTool + + """ + return ExcellonReportParser(settings).parse_raw(data) + +class ExcellonReportParser(object): + + # We sometimes get files with different encoding, so we can't actually + # match the text - the best we can do it detect the table header + header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===') + + def __init__(self, settings=None): + self.tools = {} + self.settings = settings + + self.found_header = False + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + return self.tools + + def _parse(self, line): + + # skip empty lines and "comments" + if not line.strip(): + return + + if not self.found_header: + # Try to find the heaader, since we need that to be sure we + # understand the contents correctly. + if ExcellonReportParser.header.match(line): + self.found_header = True + + elif line[0] != '=': + # Already found the header, so we know to to map the contents + parts = line.split() + if len(parts) == 6: + toolid = int(parts[0]) + size = float(parts[1]) + if parts[2] == 'x': + plated = ExcellonTool.PLATED_YES + elif parts[2] == '-': + plated = ExcellonTool.PLATED_NO + else: + plated = ExcellonTool.PLATED_UNKNOWN + feedrate = int(parts[3]) + speed = int(parts[4]) + qty = int(parts[5]) + + tool = ExcellonTool(None, number=toolid, diameter=size, + plated=plated, feed_rate=feedrate, + rpm=speed) + + self.tools[tool.number] = tool diff --git a/gerbonara/gerber/exceptions.py b/gerbonara/gerber/exceptions.py new file mode 100644 index 0000000..65ae905 --- /dev/null +++ b/gerbonara/gerber/exceptions.py @@ -0,0 +1,36 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 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. + + +class ParseError(Exception): + pass + + +class GerberParseError(ParseError): + pass + + +class ExcellonParseError(ParseError): + pass + + +class ExcellonFileError(IOError): + pass + + +class GerberFileError(IOError): + pass diff --git a/gerbonara/gerber/gerber_statements.py b/gerbonara/gerber/gerber_statements.py new file mode 100644 index 0000000..28f5e81 --- /dev/null +++ b/gerbonara/gerber/gerber_statements.py @@ -0,0 +1,1189 @@ +#!/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 (RS-274X) Statements +=========================== +**Gerber RS-274X file statement classes** + +""" +from .utils import (parse_gerber_value, write_gerber_value, decimal_string, + inch, metric) + +from .am_statements import * +from .am_read import read_macro +from .am_eval import eval_macro +from .primitives import AMGroup + + +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, units='inch'): + self.type = stype + self.units = units + + 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 + + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + + def offset(self, x_offset=0, y_offset=0): + pass + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class ParamStmt(Statement): + """ Gerber parameter statement Base class + + 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 + + +class FSParamStmt(ParamStmt): + """ FS - Gerber Format Specification Statement + """ + + @classmethod + def from_settings(cls, settings): + + return cls('FS', settings.zero_suppression, settings.notation, settings.format) + + @classmethod + def from_dict(cls, stmt_dict): + """ + """ + param = stmt_dict.get('param') + + if stmt_dict.get('zero') == 'L': + zeros = 'leading' + elif stmt_dict.get('zero') == 'T': + zeros = 'trailing' + else: + zeros = 'none' + + notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' + fmt = tuple(map(int, stmt_dict.get('x'))) + return cls(param, zeros, notation, fmt) + + 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', 'trailing' or 'none' (all zeros are present) + + 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, settings=None): + if settings: + zero_suppression = 'L' if settings.zero_suppression == 'leading' else 'T' + notation = 'A' if settings.notation == 'absolute' else 'I' + fmt = ''.join(map(str, settings.format)) + else: + zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' + notation = 'A' if self.notation == 'absolute' else 'I' + fmt = ''.join(map(str, self.format)) + + return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, fmt, fmt) + + def __str__(self): + return ('' % + (self.format[0], self.format[1], self.zero_suppression, self.notation)) + + +class MOParamStmt(ParamStmt): + """ MO - Gerber Mode (measurement units) Statement. + """ + + @classmethod + def from_units(cls, units): + return cls(None, units) + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + if stmt_dict.get('mo') is None: + mo = None + elif stmt_dict.get('mo').lower() not in ('in', 'mm'): + raise ValueError('Mode may be mm or in') + elif stmt_dict.get('mo').lower() == 'in': + mo = 'inch' + else: + mo = '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, settings=None): + mode = 'MM' if self.mode == 'metric' else 'IN' + return '%MO{0}*%'.format(mode) + + def to_inch(self): + self.mode = 'inch' + + def to_metric(self): + self.mode = 'metric' + + def __str__(self): + mode_str = 'millimeters' if self.mode == 'metric' else 'inches' + return ('' % mode_str) + + +class LPParamStmt(ParamStmt): + """ LP - Gerber Level Polarity statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict['param'] + 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=None): + lp = 'C' if self.lp == 'clear' else 'D' + return '%LP{0}*%'.format(lp) + + def __str__(self): + return '' % self.lp + + +class ADParamStmt(ParamStmt): + """ AD - Gerber Aperture Definition Statement + """ + + @classmethod + def rect(cls, dcode, width, height, hole_diameter=None, hole_width=None, hole_height=None): + '''Create a rectangular aperture definition statement''' + if hole_diameter is not None and hole_diameter > 0: + return cls('AD', dcode, 'R', ([width, height, hole_diameter],)) + elif (hole_width is not None and hole_width > 0 + and hole_height is not None and hole_height > 0): + return cls('AD', dcode, 'R', ([width, height, hole_width, hole_height],)) + return cls('AD', dcode, 'R', ([width, height],)) + + @classmethod + def circle(cls, dcode, diameter, hole_diameter=None, hole_width=None, hole_height=None): + '''Create a circular aperture definition statement''' + if hole_diameter is not None and hole_diameter > 0: + return cls('AD', dcode, 'C', ([diameter, hole_diameter],)) + elif (hole_width is not None and hole_width > 0 + and hole_height is not None and hole_height > 0): + return cls('AD', dcode, 'C', ([diameter, hole_width, hole_height],)) + return cls('AD', dcode, 'C', ([diameter],)) + + @classmethod + def obround(cls, dcode, width, height, hole_diameter=None, hole_width=None, hole_height=None): + '''Create an obround aperture definition statement''' + if hole_diameter is not None and hole_diameter > 0: + return cls('AD', dcode, 'O', ([width, height, hole_diameter],)) + elif (hole_width is not None and hole_width > 0 + and hole_height is not None and hole_height > 0): + return cls('AD', dcode, 'O', ([width, height, hole_width, hole_height],)) + return cls('AD', dcode, 'O', ([width, height],)) + + @classmethod + def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter=None, hole_width=None, hole_height=None): + '''Create a polygon aperture definition statement''' + if hole_diameter is not None and hole_diameter > 0: + return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],)) + elif (hole_width is not None and hole_width > 0 + and hole_height is not None and hole_height > 0): + return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_width, hole_height],)) + return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation],)) + + + @classmethod + def macro(cls, dcode, name): + return cls('AD', dcode, name, '') + + @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') + 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 : ADParamStmt + Initialized ADParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.d = d + self.shape = shape + if isinstance(modifiers, tuple): + self.modifiers = modifiers + elif modifiers: + self.modifiers = [tuple([float(x) for x in m.split("X") if len(x)]) + for m in modifiers.split(",") if len(m)] + else: + self.modifiers = [tuple()] + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + self.modifiers = [tuple([inch(x) for x in modifier]) + for modifier in self.modifiers] + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + self.modifiers = [tuple([metric(x) for x in modifier]) + for modifier in self.modifiers] + + def to_gerber(self, settings=None): + if any(self.modifiers): + return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, ','.join(['X'.join(["%.4g" % x for x in modifier]) for modifier in self.modifiers])) + else: + return '%ADD{0}{1}*%'.format(self.d, self.shape) + + def __str__(self): + if self.shape == 'C': + shape = 'circle' + elif self.shape == 'R': + shape = 'rectangle' + elif self.shape == 'O': + shape = 'obround' + 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 + + self.instructions = self.read(macro) + self.primitives = [] + + def read(self, macro): + return read_macro(macro) + + def build(self, modifiers=[[]]): + self.primitives = [] + + for primitive in eval_macro(self.instructions, modifiers[0]): + if primitive[0] == '0': + self.primitives.append(AMCommentPrimitive.from_gerber(primitive)) + elif primitive[0] == '1': + self.primitives.append(AMCirclePrimitive.from_gerber(primitive)) + elif primitive[0:2] in ('2,', '20'): + self.primitives.append(AMVectorLinePrimitive.from_gerber(primitive)) + elif primitive[0:2] == '21': + self.primitives.append(AMCenterLinePrimitive.from_gerber(primitive)) + elif primitive[0:2] == '22': + self.primitives.append(AMLowerLeftLinePrimitive.from_gerber(primitive)) + elif primitive[0] == '4': + self.primitives.append(AMOutlinePrimitive.from_gerber(primitive)) + elif primitive[0] == '5': + self.primitives.append(AMPolygonPrimitive.from_gerber(primitive)) + elif primitive[0] == '6': + self.primitives.append(AMMoirePrimitive.from_gerber(primitive)) + elif primitive[0] == '7': + self.primitives.append( + AMThermalPrimitive.from_gerber(primitive)) + else: + self.primitives.append( + AMUnsupportPrimitive.from_gerber(primitive)) + + return AMGroup(self.primitives, stmt=self, units=self.units) + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + for primitive in self.primitives: + primitive.to_metric() + + def to_gerber(self, settings=None): + return '%AM{0}*{1}%'.format(self.name, "".join([primitive.to_gerber() for primitive in self.primitives])) + + def __str__(self): + return '' % (self.name, self.macro) + + +class ASParamStmt(ParamStmt): + """ AS - Axis Select. (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + mode = stmt_dict.get('mode') + return cls(param, mode) + + def __init__(self, param, mode): + """ Initialize ASParamStmt class + + Parameters + ---------- + param : string + Parameter string. + + mode : string + Axis select. May be either 'AXBY' or 'AYBX' + + Returns + ------- + ParamStmt : ASParamStmt + Initialized ASParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.mode = mode + + def to_gerber(self, settings=None): + return '%AS{0}*%'.format(self.mode) + + def __str__(self): + return ('' % self.mode) + + +class INParamStmt(ParamStmt): + """ IN - Image Name Statement (Deprecated) + """ + @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, settings=None): + return '%IN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + + +class IPParamStmt(ParamStmt): + """ IP - Gerber Image Polarity Statement. (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative' + return cls(param, ip) + + def __init__(self, param, ip): + """ Initialize IPParamStmt class + + Parameters + ---------- + param : string + Parameter string. + + ip : string + Image polarity. May be either'positive' or 'negative' + + Returns + ------- + ParamStmt : IPParamStmt + Initialized IPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.ip = ip + + def to_gerber(self, settings=None): + ip = 'POS' if self.ip == 'positive' else 'NEG' + return '%IP{0}*%'.format(ip) + + def __str__(self): + return ('' % self.ip) + + +class IRParamStmt(ParamStmt): + """ IR - Image Rotation Param (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + angle = int(stmt_dict['angle']) + return cls(stmt_dict['param'], angle) + + def __init__(self, param, angle): + """ Initialize IRParamStmt class + + Parameters + ---------- + param : string + Parameter code + + angle : int + Image angle + + Returns + ------- + ParamStmt : IRParamStmt + Initialized IRParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.angle = angle + + def to_gerber(self, settings=None): + return '%IR{0}*%'.format(self.angle) + + def __str__(self): + return '' % self.angle + + +class MIParamStmt(ParamStmt): + """ MI - Image Mirror Param (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + a = int(stmt_dict.get('a', 0)) + b = int(stmt_dict.get('b', 0)) + return cls(param, a, b) + + def __init__(self, param, a, b): + """ Initialize MIParamStmt class + + Parameters + ---------- + param : string + Parameter code + + a : int + Mirror for A output devices axis (0=disabled, 1=mirrored) + + b : int + Mirror for B output devices axis (0=disabled, 1=mirrored) + + Returns + ------- + ParamStmt : MIParamStmt + Initialized MIParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + def to_gerber(self, settings=None): + ret = "%MI" + if self.a is not None: + ret += "A{0}".format(self.a) + if self.b is not None: + ret += "B{0}".format(self.b) + ret += "*%" + return ret + + def __str__(self): + return '' % (self.a, self.b) + + +class OFParamStmt(ParamStmt): + """ OF - Gerber Offset statement (Deprecated) + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + a = float(stmt_dict.get('a', 0)) + b = float(stmt_dict.get('b', 0)) + return cls(param, a, b) + + def __init__(self, param, a, b): + """ Initialize OFParamStmt class + + Parameters + ---------- + param : string + Parameter + + a : float + Offset along the output device A axis + + b : float + Offset along the output device B axis + + Returns + ------- + ParamStmt : OFParamStmt + Initialized OFParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + def to_gerber(self, settings=None): + ret = '%OF' + if self.a is not None: + ret += 'A' + decimal_string(self.a, precision=5) + if self.b is not None: + ret += 'B' + decimal_string(self.b, precision=5) + return ret + '*%' + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.a is not None: + self.a = inch(self.a) + if self.b is not None: + self.b = inch(self.b) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.a is not None: + self.a = metric(self.a) + if self.b is not None: + self.b = metric(self.b) + + def offset(self, x_offset=0, y_offset=0): + if self.a is not None: + self.a += x_offset + if self.b is not None: + self.b += y_offset + + def __str__(self): + offset_str = '' + if self.a is not None: + offset_str += ('X: %f ' % self.a) + if self.b is not None: + offset_str += ('Y: %f ' % self.b) + return ('' % offset_str) + + +class SFParamStmt(ParamStmt): + """ SF - Scale Factor Param (Deprecated) + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + a = float(stmt_dict.get('a', 1)) + b = float(stmt_dict.get('b', 1)) + return cls(param, a, b) + + def __init__(self, param, a, b): + """ Initialize OFParamStmt class + + Parameters + ---------- + param : string + Parameter + + a : float + Scale factor for the output device A axis + + b : float + Scale factor for the output device B axis + + Returns + ------- + ParamStmt : SFParamStmt + Initialized SFParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + def to_gerber(self, settings=None): + ret = '%SF' + if self.a is not None: + ret += 'A' + decimal_string(self.a, precision=5) + if self.b is not None: + ret += 'B' + decimal_string(self.b, precision=5) + return ret + '*%' + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.a is not None: + self.a = inch(self.a) + if self.b is not None: + self.b = inch(self.b) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.a is not None: + self.a = metric(self.a) + if self.b is not None: + self.b = metric(self.b) + + def offset(self, x_offset=0, y_offset=0): + if self.a is not None: + self.a += x_offset + if self.b is not None: + self.b += y_offset + + def __str__(self): + scale_factor = '' + if self.a is not None: + scale_factor += ('X: %g ' % self.a) + if self.b is not None: + scale_factor += ('Y: %g' % self.b) + return ('' % scale_factor) + + +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, settings=None): + return '%LN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + + +class DeprecatedStmt(Statement): + """ Unimportant deprecated statement, will be parsed but not emitted. + """ + @classmethod + def from_gerber(cls, line): + return cls(line) + + def __init__(self, line): + """ Initialize DeprecatedStmt class + + Parameters + ---------- + line : string + Deprecated statement text + + Returns + ------- + DeprecatedStmt + Initialized DeprecatedStmt class. + + """ + Statement.__init__(self, "DEPRECATED") + self.line = line + + def to_gerber(self, settings=None): + return self.line + + def __str__(self): + return '' % self.line + + +class CoordStmt(Statement): + """ Coordinate Data Block + """ + + OP_DRAW = 'D01' + OP_MOVE = 'D02' + OP_FLASH = 'D03' + + FUNC_LINEAR = 'G01' + FUNC_ARC_CW = 'G02' + FUNC_ARC_CCW = 'G03' + + @classmethod + def from_dict(cls, stmt_dict, settings): + function = stmt_dict['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'), settings.format, + settings.zero_suppression) + if y is not None: + y = parse_gerber_value(stmt_dict.get('y'), settings.format, + settings.zero_suppression) + if i is not None: + i = parse_gerber_value(stmt_dict.get('i'), settings.format, + settings.zero_suppression) + if j is not None: + j = parse_gerber_value(stmt_dict.get('j'), settings.format, + settings.zero_suppression) + return cls(function, x, y, i, j, op, settings) + + @classmethod + def move(cls, func, point): + if point: + return cls(func, point[0], point[1], None, None, CoordStmt.OP_MOVE, None) + # No point specified, so just write the function. This is normally for ending a region (D02*) + return cls(func, None, None, None, None, CoordStmt.OP_MOVE, None) + + @classmethod + def line(cls, func, point): + return cls(func, point[0], point[1], None, None, CoordStmt.OP_DRAW, None) + + @classmethod + def mode(cls, func): + return cls(func, None, None, None, None, None, None) + + @classmethod + def arc(cls, func, point, center): + return cls(func, point[0], point[1], center[0], center[1], CoordStmt.OP_DRAW, None) + + @classmethod + def flash(cls, point): + if point: + return cls(None, point[0], point[1], None, None, CoordStmt.OP_FLASH, None) + else: + return cls(None, None, None, None, None, CoordStmt.OP_FLASH, None) + + def __init__(self, function, x, y, i, j, op, settings): + """ Initialize CoordStmt class + + 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.function = function + self.x = x + self.y = y + self.i = i + self.j = j + self.op = op + + def to_gerber(self, settings=None): + ret = '' + if self.function: + ret += self.function + if self.x is not None: + ret += 'X{0}'.format(write_gerber_value(self.x, settings.format, + settings.zero_suppression)) + if self.y is not None: + ret += 'Y{0}'.format(write_gerber_value(self.y, settings.format, + settings.zero_suppression)) + if self.i is not None: + ret += 'I{0}'.format(write_gerber_value(self.i, settings.format, + settings.zero_suppression)) + if self.j is not None: + ret += 'J{0}'.format(write_gerber_value(self.j, settings.format, + settings.zero_suppression)) + if self.op: + ret += self.op + return ret + '*' + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x is not None: + self.x = inch(self.x) + if self.y is not None: + self.y = inch(self.y) + if self.i is not None: + self.i = inch(self.i) + if self.j is not None: + self.j = inch(self.j) + if self.function == "G71": + self.function = "G70" + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x is not None: + self.x = metric(self.x) + if self.y is not None: + self.y = metric(self.y) + if self.i is not None: + self.i = metric(self.i) + if self.j is not None: + self.j = metric(self.j) + if self.function == "G70": + self.function = "G71" + + def offset(self, x_offset=0, y_offset=0): + if self.x is not None: + self.x += x_offset + if self.y is not None: + self.y += y_offset + if self.i is not None: + self.i += x_offset + if self.j is not None: + self.j += y_offset + + def __str__(self): + coord_str = '' + if self.function: + coord_str += 'Fn: %s ' % self.function + if self.x is not None: + coord_str += 'X: %g ' % self.x + if self.y is not None: + coord_str += 'Y: %g ' % self.y + if self.i is not None: + coord_str += 'I: %g ' % self.i + if self.j is not None: + coord_str += 'J: %g ' % self.j + if self.op: + if self.op == 'D01': + op = 'Lights On' + 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 + + @property + def only_function(self): + """ + Returns if the statement only set the function. + """ + + # TODO I would like to refactor this so that the function is handled separately and then + # TODO this isn't required + return self.function != None and self.op == None and self.x == None and self.y == None and self.i == None and self.j == None + + +class ApertureStmt(Statement): + """ Aperture Statement + """ + + def __init__(self, d, deprecated=None): + Statement.__init__(self, "APERTURE") + self.d = int(d) + self.deprecated = True if deprecated is not None and deprecated is not False else False + + def to_gerber(self, settings=None): + if self.deprecated: + return 'G54D{0}*'.format(self.d) + else: + return 'D{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 if comment is not None else "" + + def to_gerber(self, settings=None): + 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, settings=None): + return 'M02*' + + def __str__(self): + return '' + + +class QuadrantModeStmt(Statement): + + @classmethod + def single(cls): + return cls('single-quadrant') + + @classmethod + def multi(cls): + return cls('multi-quadrant') + + @classmethod + def from_gerber(cls, line): + 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__('QuadrantMode') + 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, settings=None): + return 'G74*' if self.mode == 'single-quadrant' else 'G75*' + + +class RegionModeStmt(Statement): + + @classmethod + def from_gerber(cls, line): + if 'G36' not in line and 'G37' not in line: + raise ValueError('%s is not a valid region mode statement' % line) + return (cls('on') if line[:3] == 'G36' else cls('off')) + + @classmethod + def on(cls): + return cls('on') + + @classmethod + def off(cls): + return cls('off') + + def __init__(self, mode): + super(RegionModeStmt, self).__init__('RegionMode') + mode = mode.lower() + if mode not in ['on', 'off']: + raise ValueError('Valid modes are "on" or "off"') + self.mode = mode + + def to_gerber(self, settings=None): + return 'G36*' if self.mode == 'on' else 'G37*' + + +class UnknownStmt(Statement): + """ Unknown Statement + """ + + def __init__(self, line): + Statement.__init__(self, "UNKNOWN") + self.line = line + + def to_gerber(self, settings=None): + return self.line + + def __str__(self): + return '' % self.line diff --git a/gerbonara/gerber/ipc356.py b/gerbonara/gerber/ipc356.py new file mode 100644 index 0000000..9337a99 --- /dev/null +++ b/gerbonara/gerber/ipc356.py @@ -0,0 +1,485 @@ +#! /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 math +import re +from .cam import CamFile, FileSettings +from .primitives import TestRecord + +# Net Name Variables +_NNAME = re.compile(r'^NNAME\d+$') + +# Board Edge Coordinates +_COORD = re.compile(r'X?(?P[\d\s]*)?Y?(?P[\d\s]*)?') + +_SM_FIELD = { + '0': 'none', + '1': 'primary side', + '2': 'secondary side', + '3': 'both'} + + +def read(filename): + """ Read data from filename and return an IPCNetlist + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.ipc356.IPCNetlist` + An IPCNetlist object created from the specified file. + + """ + # File object should use settings from source file by default. + return IPCNetlist.from_file(filename) + + +def loads(data, filename=None): + """ Generate an IPCNetlist object from IPC-D-356 data in memory + + Parameters + ---------- + data : string + string containing netlist file contents + + filename : string, optional + string containing the filename of the data source + + Returns + ------- + file : :class:`gerber.ipc356.IPCNetlist` + An IPCNetlist created from the specified file. + """ + return IPCNetlistParser().parse_raw(data, filename) + + +class IPCNetlist(CamFile): + + @classmethod + def from_file(cls, filename): + parser = IPCNetlistParser() + return parser.parse(filename) + + def __init__(self, statements, settings, primitives=None, filename=None): + self.statements = statements + self.units = settings.units + self.angle_units = settings.angle_units + self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name, + rec.access) for rec in self.test_records] + self.filename = filename + + @property + def settings(self): + return FileSettings(units=self.units, angle_units=self.angle_units) + + @property + def comments(self): + return [record for record in self.statements + if isinstance(record, IPC356_Comment)] + + @property + def parameters(self): + return [record for record in self.statements + if isinstance(record, IPC356_Parameter)] + + @property + def test_records(self): + return [record for record in self.statements + if isinstance(record, IPC356_TestRecord)] + + @property + def nets(self): + nets = [] + for net in list(set([rec.net_name for rec in self.test_records + if rec.net_name is not None])): + adjacent_nets = set() + for record in self.adjacency_records: + if record.net == net: + adjacent_nets = adjacent_nets.update(record.adjacent_nets) + elif net in record.adjacent_nets: + adjacent_nets.add(record.net) + nets.append(IPC356_Net(net, adjacent_nets)) + return nets + + @property + def components(self): + return list(set([rec.id for rec in self.test_records + if rec.id is not None and rec.id != 'VIA'])) + + @property + def vias(self): + return [rec.id for rec in self.test_records if rec.id == 'VIA'] + + @property + def outlines(self): + return [stmt for stmt in self.statements + if isinstance(stmt, IPC356_Outline)] + + @property + def adjacency_records(self): + return [record for record in self.statements + if isinstance(record, IPC356_Adjacency)] + + def render(self, ctx, layer='both', filename=None): + for p in self.primitives: + if layer == 'both' and p.layer in ('top', 'bottom', 'both'): + ctx.render(p) + elif layer == 'top' and p.layer in ('top', 'both'): + ctx.render(p) + elif layer == 'bottom' and p.layer in ('bottom', 'both'): + ctx.render(p) + if filename is not None: + ctx.dump(filename) + + +class IPCNetlistParser(object): + # TODO: Allow multi-line statements (e.g. Altium board edge) + + def __init__(self): + self.units = 'inch' + self.angle_units = 'degrees' + self.statements = [] + self.nnames = {} + + @property + def settings(self): + return FileSettings(units=self.units, angle_units=self.angle_units) + + def parse(self, filename): + with open(filename, 'rU') as f: + data = f.read() + return self.parse_raw(data, filename) + + def parse_raw(self, data, filename=None): + oldline = '' + for line in data.splitlines(): + # Check for existing multiline data... + if oldline != '': + if len(line) and line[0] == '0': + oldline = oldline.rstrip('\r\n') + line[3:].rstrip() + else: + self._parse_line(oldline) + oldline = line + else: + oldline = line + self._parse_line(oldline) + + return IPCNetlist(self.statements, self.settings, filename=filename) + + def _parse_line(self, line): + if not len(line): + return + if line[0] == 'C': + # Comment + self.statements.append(IPC356_Comment.from_line(line)) + + elif line[0] == 'P': + # Parameter + p = IPC356_Parameter.from_line(line) + if p.parameter == 'UNITS': + if p.value in ('CUST', 'CUST 0'): + self.units = 'inch' + self.angle_units = 'degrees' + elif p.value == 'CUST 1': + self.units = 'metric' + self.angle_units = 'degrees' + elif p.value == 'CUST 2': + self.units = 'inch' + self.angle_units = 'radians' + self.statements.append(p) + if _NNAME.match(p.parameter): + # Add to list of net name variables + self.nnames[p.parameter] = p.value + + elif line[0] == '9': + self.statements.append(IPC356_EndOfFile()) + + elif line[0:3] in ('317', '327', '367'): + # Test Record + record = IPC356_TestRecord.from_line(line, self.settings) + + # Substitute net name variables + net = record.net_name + if (_NNAME.match(net) and net in self.nnames.keys()): + record.net_name = self.nnames[record.net_name] + self.statements.append(record) + + elif line[0:3] == '378': + # Conductor + self.statements.append( + IPC356_Conductor.from_line( + line, self.settings)) + + elif line[0:3] == '379': + # Net Adjacency + self.statements.append(IPC356_Adjacency.from_line(line)) + + elif line[0:3] == '389': + # Outline + self.statements.append( + IPC356_Outline.from_line( + line, self.settings)) + + +class IPC356_Comment(object): + + @classmethod + def from_line(cls, line): + if line[0] != 'C': + raise ValueError('Not a valid comment statment') + comment = line[2:].strip() + return cls(comment) + + def __init__(self, comment): + self.comment = comment + + def __repr__(self): + return '' % self.comment + + +class IPC356_Parameter(object): + + @classmethod + def from_line(cls, line): + if line[0] != 'P': + raise ValueError('Not a valid parameter statment') + splitline = line[2:].split() + parameter = splitline[0].strip() + value = ' '.join(splitline[1:]).strip() + return cls(parameter, value) + + def __init__(self, parameter, value): + self.parameter = parameter + self.value = value + + def __repr__(self): + return '' % (self.parameter, self.value) + + +class IPC356_TestRecord(object): + + @classmethod + def from_line(cls, line, settings): + offset = 0 + units = settings.units + angle = settings.angle_units + feature_types = {'1': 'through-hole', '2': 'smt', + '3': 'tooling-feature', '4': 'tooling-hole', + '6': 'non-plated-tooling-hole'} + access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5', + 'layer6', 'layer7', 'bottom'] + record = {} + line = line.strip() + if line[0] != '3': + raise ValueError('Not a valid test record statment') + record['feature_type'] = feature_types[line[1]] + + end = len(line) - 1 if len(line) < 18 else 17 + record['net_name'] = line[3:end].strip() + + if len(line) >= 27 and line[26] != '-': + offset = line[26:].find('-') + offset = 0 if offset == -1 else offset + end = len(line) - 1 if len(line) < (27 + offset) else (26 + offset) + record['id'] = line[20:end].strip() + + end = len(line) - 1 if len(line) < (32 + offset) else (31 + offset) + record['pin'] = (line[27 + offset:end].strip() if line[27 + offset:end].strip() != '' + else None) + + record['location'] = 'middle' if line[31 + offset] == 'M' else 'end' + if line[32 + offset] == 'D': + end = len(line) - 1 if len(line) < (38 + offset) else (37 + offset) + dia = int(line[33 + offset:end].strip()) + record['hole_diameter'] = (dia * 0.0001 if units == 'inch' + else dia * 0.001) + if len(line) >= (38 + offset): + record['plated'] = (line[37 + offset] == 'P') + + if len(line) >= (40 + offset): + end = len(line) - 1 if len(line) < (42 + offset) else (41 + offset) + record['access'] = access[int(line[39 + offset:end])] + + if len(line) >= (43 + offset): + end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset) + coord = int(line[42 + offset:end].strip()) + record['x_coord'] = (coord * 0.0001 if units == 'inch' + else coord * 0.001) + + if len(line) >= (51 + offset): + end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset) + coord = int(line[50 + offset:end].strip()) + record['y_coord'] = (coord * 0.0001 if units == 'inch' + else coord * 0.001) + + if len(line) >= (59 + offset): + end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset) + dim = line[58 + offset:end].strip() + if dim != '': + record['rect_x'] = (int(dim) * 0.0001 if units == 'inch' + else int(dim) * 0.001) + + if len(line) >= (64 + offset): + end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset) + dim = line[63 + offset:end].strip() + if dim != '': + record['rect_y'] = (int(dim) * 0.0001 if units == 'inch' + else int(dim) * 0.001) + + if len(line) >= (69 + offset): + end = len(line) - 1 if len(line) < (72 + offset) else (71 + offset) + rot = line[68 + offset:end].strip() + if rot != '': + record['rect_rotation'] = (int(rot) if angle == 'degrees' + else math.degrees(rot)) + + if len(line) >= (74 + offset): + end = 74 + offset + sm_info = line[73 + offset:end].strip() + record['soldermask_info'] = _SM_FIELD.get(sm_info) + + if len(line) >= (76 + offset): + end = len(line) - 1 if len(line) < (80 + offset) else 79 + offset + record['optional_info'] = line[75 + offset:end] + + return cls(**record) + + def __init__(self, **kwargs): + for key in kwargs: + setattr(self, key, kwargs[key]) + + def __repr__(self): + return '' % (self.net_name, + self.feature_type) + + +class IPC356_Outline(object): + + @classmethod + def from_line(cls, line, settings): + type = line[3:17].strip() + scale = 0.0001 if settings.units == 'inch' else 0.001 + points = [] + x = 0 + y = 0 + coord_strings = line.strip().split()[1:] + for coord in coord_strings: + coord_dict = _COORD.match(coord).groupdict() + x = int(coord_dict['x']) if coord_dict['x'] is not '' else x + y = int(coord_dict['y']) if coord_dict['y'] is not '' else y + points.append((x * scale, y * scale)) + return cls(type, points) + + def __init__(self, type, points): + self.type = type + self.points = points + + def __repr__(self): + return '' % self.type + + +class IPC356_Conductor(object): + + @classmethod + def from_line(cls, line, settings): + if line[0:3] != '378': + raise ValueError('Not a valid IPC-D-356 Conductor statement') + + scale = 0.0001 if settings.units == 'inch' else 0.001 + net_name = line[3:17].strip() + layer = int(line[19:21]) + + # Parse out aperture definiting + raw_aperture = line[22:].split()[0] + aperture_dict = _COORD.match(raw_aperture).groupdict() + x = 0 + y = 0 + x = int(aperture_dict['x']) * \ + scale if aperture_dict['x'] is not '' else None + y = int(aperture_dict['y']) * \ + scale if aperture_dict['y'] is not '' else None + aperture = (x, y) + + # Parse out conductor shapes + shapes = [] + coord_list = ' '.join(line[22:].split()[1:]) + raw_shapes = coord_list.split('*') + for rshape in raw_shapes: + x = 0 + y = 0 + shape = [] + coords = rshape.split() + for coord in coords: + coord_dict = _COORD.match(coord).groupdict() + x = int(coord_dict['x']) if coord_dict['x'] is not '' else x + y = int(coord_dict['y']) if coord_dict['y'] is not '' else y + shape.append((x * scale, y * scale)) + shapes.append(tuple(shape)) + return cls(net_name, layer, aperture, tuple(shapes)) + + def __init__(self, net_name, layer, aperture, shapes): + self.net_name = net_name + self.layer = layer + self.aperture = aperture + self.shapes = shapes + + def __repr__(self): + return '' % self.net_name + + +class IPC356_Adjacency(object): + + @classmethod + def from_line(cls, line): + if line[0:3] != '379': + raise ValueError('Not a valid IPC-D-356 Conductor statement') + nets = line[3:].strip().split() + + return cls(nets[0], nets[1:]) + + def __init__(self, net, adjacent_nets): + self.net = net + self.adjacent_nets = adjacent_nets + + def __repr__(self): + return '' % self.net + + +class IPC356_EndOfFile(object): + + def __init__(self): + pass + + def to_netlist(self): + return '999' + + def __repr__(self): + return '' + + +class IPC356_Net(object): + + def __init__(self, name, adjacent_nets): + self.name = name + self.adjacent_nets = set( + adjacent_nets) if adjacent_nets is not None else set() + + def __repr__(self): + return '' % self.name diff --git a/gerbonara/gerber/layers.py b/gerbonara/gerber/layers.py new file mode 100644 index 0000000..69e1c0d --- /dev/null +++ b/gerbonara/gerber/layers.py @@ -0,0 +1,295 @@ +#! /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 os +import re +from collections import namedtuple + +from . import common +from .excellon import ExcellonFile +from .ipc356 import IPCNetlist + + +Hint = namedtuple('Hint', 'layer ext name regex content') + +hints = [ + Hint(layer='top', + ext=['gtl', 'cmp', 'top', ], + name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', 'F.Cu', ], + regex='', + content=[] + ), + Hint(layer='bottom', + ext=['gbl', 'sld', 'bot', 'sol', 'bottom', ], + name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', 'B.Cu', ], + regex='', + content=[] + ), + Hint(layer='internal', + ext=['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', + 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', ], + name=['art', 'internal', 'pgp', 'pwr', 'gnd', 'ground', + 'gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', + 'In1.Cu', 'In2.Cu', 'In3.Cu', 'In4.Cu', + 'group3', 'group4', 'group5', 'group6', 'group7', 'group8', ], + regex='', + content=[] + ), + Hint(layer='topsilk', + ext=['gto', 'sst', 'plc', 'ts', 'skt', 'topsilk', ], + name=['sst01', 'topsilk', 'silk', 'slk', 'sst', 'F.SilkS'], + regex='', + content=[] + ), + Hint(layer='bottomsilk', + ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', ], + name=['bsilk', 'ssb', 'botsilk', 'bottomsilk', 'B.SilkS'], + regex='', + content=[] + ), + Hint(layer='topmask', + ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ], + name=['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask', + 'mst', 'F.Mask', ], + regex='', + content=[] + ), + Hint(layer='bottommask', + ext=['gbs', 'sts', 'bmk', 'smb', 'br', 'bottommask', ], + name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'bottommask', + 'msb', 'B.Mask', ], + regex='', + content=[] + ), + Hint(layer='toppaste', + ext=['gtp', 'tm', 'toppaste', ], + name=['sp01', 'toppaste', 'pst', 'F.Paste'], + regex='', + content=[] + ), + Hint(layer='bottompaste', + ext=['gbp', 'bm', 'bottompaste', ], + name=['sp02', 'botpaste', 'bottompaste', 'psb', 'B.Paste', ], + regex='', + content=[] + ), + Hint(layer='outline', + ext=['gko', 'outline', ], + name=['BDR', 'border', 'out', 'outline', 'Edge.Cuts', ], + regex='', + content=[] + ), + Hint(layer='ipc_netlist', + ext=['ipc'], + name=[], + regex='', + content=[] + ), + Hint(layer='drawing', + ext=['fab'], + name=['assembly drawing', 'assembly', 'fabrication', + 'fab drawing', 'fab'], + regex='', + content=[] + ), +] + + +def layer_signatures(layer_class): + for hint in hints: + if hint.layer == layer_class: + return hint.ext + hint.name + return [] + + +def load_layer(filename): + return PCBLayer.from_cam(common.read(filename)) + + +def load_layer_data(data, filename=None): + return PCBLayer.from_cam(common.loads(data, filename)) + + +def guess_layer_class(filename): + try: + layer = guess_layer_class_by_content(filename) + if layer: + return layer + except: + pass + + try: + directory, filename = os.path.split(filename) + name, ext = os.path.splitext(filename.lower()) + for hint in hints: + if hint.regex: + if re.findall(hint.regex, filename, re.IGNORECASE): + return hint.layer + + patterns = [r'^(\w*[.-])*{}([.-]\w*)?$'.format(x) for x in hint.name] + if ext[1:] in hint.ext or any(re.findall(p, name, re.IGNORECASE) for p in patterns): + return hint.layer + except: + pass + return 'unknown' + + +def guess_layer_class_by_content(filename): + try: + file = open(filename, 'r') + for line in file: + for hint in hints: + if len(hint.content) > 0: + patterns = [r'^(.*){}(.*)$'.format(x) for x in hint.content] + if any(re.findall(p, line, re.IGNORECASE) for p in patterns): + return hint.layer + except: + pass + + return False + + +def sort_layers(layers, from_top=True): + layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top', + 'internal', 'bottom', 'bottommask', 'bottomsilk', + 'bottompaste'] + append_after = ['drill', 'drawing'] + + output = [] + drill_layers = [layer for layer in layers if layer.layer_class == 'drill'] + internal_layers = list(sorted([layer for layer in layers + if layer.layer_class == 'internal'])) + + for layer_class in layer_order: + if layer_class == 'internal': + output += internal_layers + elif layer_class == 'drill': + output += drill_layers + else: + for layer in layers: + if layer.layer_class == layer_class: + output.append(layer) + if not from_top: + output = list(reversed(output)) + + for layer_class in append_after: + for layer in layers: + if layer.layer_class == layer_class: + output.append(layer) + return output + + +class PCBLayer(object): + """ Base class for PCB Layers + + Parameters + ---------- + source : CAMFile + CAMFile representing the layer + + + Attributes + ---------- + filename : string + Source Filename + + """ + @classmethod + def from_cam(cls, camfile): + filename = camfile.filename + layer_class = guess_layer_class(filename) + if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'): + return DrillLayer.from_cam(camfile) + elif layer_class == 'internal': + return InternalLayer.from_cam(camfile) + if isinstance(camfile, IPCNetlist): + layer_class = 'ipc_netlist' + return cls(filename, layer_class, camfile) + + def __init__(self, filename=None, layer_class=None, cam_source=None, **kwargs): + super(PCBLayer, self).__init__(**kwargs) + self.filename = filename + self.layer_class = layer_class + self.cam_source = cam_source + self.surface = None + self.primitives = cam_source.primitives if cam_source is not None else [] + + @property + def bounds(self): + if self.cam_source is not None: + return self.cam_source.bounds + else: + return None + + def __repr__(self): + return ''.format(self.layer_class) + + +class DrillLayer(PCBLayer): + @classmethod + def from_cam(cls, camfile): + return cls(camfile.filename, camfile) + + def __init__(self, filename=None, cam_source=None, layers=None, **kwargs): + super(DrillLayer, self).__init__(filename, 'drill', cam_source, **kwargs) + self.layers = layers if layers is not None else ['top', 'bottom'] + + +class InternalLayer(PCBLayer): + + @classmethod + def from_cam(cls, camfile): + filename = camfile.filename + try: + order = int(re.search(r'\d+', filename).group()) + except AttributeError: + order = 0 + return cls(filename, camfile, order) + + def __init__(self, filename=None, cam_source=None, order=0, **kwargs): + super(InternalLayer, self).__init__(filename, 'internal', cam_source, **kwargs) + self.order = order + + def __eq__(self, other): + if not hasattr(other, 'order'): + raise TypeError() + return (self.order == other.order) + + def __ne__(self, other): + if not hasattr(other, 'order'): + raise TypeError() + return (self.order != other.order) + + def __gt__(self, other): + if not hasattr(other, 'order'): + raise TypeError() + return (self.order > other.order) + + def __lt__(self, other): + if not hasattr(other, 'order'): + raise TypeError() + return (self.order < other.order) + + def __ge__(self, other): + if not hasattr(other, 'order'): + raise TypeError() + return (self.order >= other.order) + + def __le__(self, other): + if not hasattr(other, 'order'): + raise TypeError() + return (self.order <= other.order) diff --git a/gerbonara/gerber/ncparam/allegro.py b/gerbonara/gerber/ncparam/allegro.py new file mode 100644 index 0000000..a67bcf1 --- /dev/null +++ b/gerbonara/gerber/ncparam/allegro.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Garret Fick + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Allegro File module +==================== +**Excellon file classes** + +Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information +""" + diff --git a/gerbonara/gerber/operations.py b/gerbonara/gerber/operations.py new file mode 100644 index 0000000..d06876e --- /dev/null +++ b/gerbonara/gerber/operations.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2015 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 Operations +=================== +**Transformations and other operations performed on Gerber and Excellon files** + +""" +import copy + + +def to_inch(cam_file): + """ Convert Gerber or Excellon file units to imperial + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to convert + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + A deep copy of the source file with units converted to imperial. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.to_inch() + return cam_file + + +def to_metric(cam_file): + """ Convert Gerber or Excellon file units to metric + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to convert + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + A deep copy of the source file with units converted to metric. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.to_metric() + return cam_file + + +def offset(cam_file, x_offset, y_offset): + """ Offset a Cam file by a specified amount in the X and Y directions. + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to offset + + x_offset : float + Amount to offset the file in the X direction + + y_offset : float + Amount to offset the file in the Y direction + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + An offset deep copy of the source file. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.offset(x_offset, y_offset) + return cam_file + + +def scale(cam_file, x_scale, y_scale): + """ Scale a Cam file by a specified amount in the X and Y directions. + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to scale + + x_scale : float + X-axis scale factor + + y_scale : float + Y-axis scale factor + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + An scaled deep copy of the source file. + """ + # TODO + pass + + +def rotate(cam_file, angle): + """ Rotate a Cam file a specified amount about the origin. + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to rotate + + angle : float + Angle to rotate the file in degrees. + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + An rotated deep copy of the source file. + """ + # TODO + pass diff --git a/gerbonara/gerber/panelize/__init__.py b/gerbonara/gerber/panelize/__init__.py new file mode 100644 index 0000000..a3de2c1 --- /dev/null +++ b/gerbonara/gerber/panelize/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from .common import read, loads, rectangle +from .composition import GerberComposition, DrillComposition +from .dxf import DxfFile diff --git a/gerbonara/gerber/panelize/am_expression.py b/gerbonara/gerber/panelize/am_expression.py new file mode 100644 index 0000000..9e19f71 --- /dev/null +++ b/gerbonara/gerber/panelize/am_expression.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..utils import * +from ..am_eval import OpCode +from ..am_statements import * + +class AMExpression(object): + CONSTANT = 1 + VARIABLE = 2 + OPERATOR = 3 + + def __init__(self, kind): + self.kind = kind + + @property + def value(self): + return self + + def optimize(self): + pass + + def to_inch(self): + return AMOperatorExpression(AMOperatorExpression.DIV, self, + AMConstantExpression(MILLIMETERS_PER_INCH)) + + def to_metric(self): + return AMOperatorExpression(AMOperatorExpression.MUL, self, + AMConstantExpression(MILLIMETERS_PER_INCH)) + + def to_gerber(self, settings=None): + pass + + def to_instructions(self): + pass + +class AMConstantExpression(AMExpression): + def __init__(self, value): + super(AMConstantExpression, self).__init__(AMExpression.CONSTANT) + self._value = value + + @property + def value(self): + return self._value + + def optimize(self): + return self + + def to_gerber(self, settings=None): + if isinstance(self._value, str): + return self._value + gerber = '%.6g' % self._value + return '%.6f' % self._value if 'e' in gerber else gerber + + def to_instructions(self): + return [(OpCode.PUSH, self._value)] + +class AMVariableExpression(AMExpression): + def __init__(self, number): + super(AMVariableExpression, self).__init__(AMExpression.VARIABLE) + self.number = number + + def optimize(self): + return self + + def to_gerber(self, settings=None): + return '$%d' % self.number + + def to_instructions(self): + return (OpCode.LOAD, self.number) + +class AMOperatorExpression(AMExpression): + ADD = '+' + SUB = '-' + MUL = 'X' + DIV = '/' + + def __init__(self, op, lvalue, rvalue): + super(AMOperatorExpression, self).__init__(AMExpression.OPERATOR) + self.op = op + self.lvalue = lvalue + self.rvalue = rvalue + + def optimize(self): + self.lvalue = self.lvalue.optimize() + self.rvalue = self.rvalue.optimize() + + if isinstance(self.lvalue, AMConstantExpression) and isinstance(self.rvalue, AMConstantExpression): + lvalue = float(self.lvalue.value) + rvalue = float(self.rvalue.value) + value = lvalue + rvalue if self.op == self.ADD else \ + lvalue - rvalue if self.op == self.SUB else \ + lvalue * rvalue if self.op == self.MUL else \ + lvalue / rvalue if self.op == self.DIV else None + return AMConstantExpression(value) + elif self.op == self.ADD: + if self.rvalue.value == 0: + return self.lvalue + elif self.lvalue.value == 0: + return self.rvalue + elif self.op == self.SUB: + if self.rvalue.value == 0: + return self.lvalue + elif self.lvalue.value == 0 and isinstance(self.rvalue, AMConstantExpression): + return AMConstantExpression(-self.rvalue.value) + elif self.op == self.MUL: + if self.rvalue.value == 1: + return self.lvalue + elif self.lvalue.value == 1: + return self.rvalue + elif self.lvalue == 0 or self.rvalue == 0: + return AMConstantExpression(0) + elif self.op == self.DIV: + if self.rvalue.value == 1: + return self.lvalue + elif self.lvalue.value == 0: + return AMConstantExpression(0) + + return self + + def to_gerber(self, settings=None): + return '(%s)%s(%s)' % (self.lvalue.to_gerber(settings), self.op, self.rvalue.to_gerber(settings)) + + def to_instructions(self): + for i in self.lvalue.to_instructions(): + yield i + for i in self.rvalue.to_instructions(): + yield i + op = OpCode.ADD if self.op == self.ADD else\ + OpCode.SUB if self.op == self.SUB else\ + OpCode.MUL if self.op == self.MUL else\ + OpCode.DIV + yield (op, None) + +def eval_macro(instructions): + stack = [] + + def pop(): + return stack.pop() + + def push(op): + stack.append(op) + + def top(): + return stack[-1] + + def empty(): + return len(stack) == 0 + + for opcode, argument in instructions: + if opcode == OpCode.PUSH: + push(AMConstantExpression(argument)) + + elif opcode == OpCode.LOAD: + push(AMVariableExpression(argument)) + + elif opcode == OpCode.STORE: + yield (-argument, [pop()]) + + elif opcode == OpCode.ADD: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.ADD, op2, op1)) + + elif opcode == OpCode.SUB: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.SUB, op2, op1)) + + elif opcode == OpCode.MUL: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.MUL, op2, op1)) + + elif opcode == OpCode.DIV: + op1 = pop() + op2 = pop() + push(AMOperatorExpression(AMOperatorExpression.DIV, op2, op1)) + + elif opcode == OpCode.PRIM: + yield (argument, stack) + stack = [] diff --git a/gerbonara/gerber/panelize/am_primitive.py b/gerbonara/gerber/panelize/am_primitive.py new file mode 100644 index 0000000..123f030 --- /dev/null +++ b/gerbonara/gerber/panelize/am_primitive.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..utils import * +from ..am_statements import * +from ..am_eval import OpCode + +from .am_expression import eval_macro, AMConstantExpression, AMOperatorExpression + +class AMPrimitiveDef(AMPrimitive): + def __init__(self, code, exposure=None, rotation=None): + super(AMPrimitiveDef, self).__init__(code, exposure) + if not rotation: + rotation = AMConstantExpression(0) + self.rotation = rotation + + def rotate(self, angle, center=None): + self.rotation = AMOperatorExpression(AMOperatorExpression.ADD, + self.rotation, + AMConstantExpression(float(angle))) + self.rotation = self.rotation.optimize() + + def to_inch(self): + pass + + def to_metric(self): + pass + + def to_gerber(self, settings=None): + pass + + def to_instructions(self): + pass + +class AMCommentPrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + return cls(code, modifiers[0]) + + def __init__(self, code, comment): + super(AMCommentPrimitiveDef, self).__init__(code) + self.comment = comment + + def to_gerber(self, settings=None): + return '%d %s*' % (self.code, self.comment.to_gerber()) + + def to_instructions(self): + return [(OpCode.PUSH, self.comment), (OpCode.PRIM, self.code)] + +class AMCirclePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + exposure = 'on' if modifiers[0].value == 1 else 'off' + diameter = modifiers[1] + center_x = modifiers[2] + center_y = modifiers[3] + rotation = modifiers[4] if len(modifiers)>4 else AMConstantExpression(float(0)) + return cls(code, exposure, diameter, center_x, center_y, rotation) + + def __init__(self, code, exposure, diameter, center_x, center_y, rotation): + super(AMCirclePrimitiveDef, self).__init__(code, exposure, rotation) + self.diameter = diameter + self.center_x = center_x + self.center_y = center_y + + def to_inch(self): + self.diameter = self.diameter.to_inch().optimize() + self.center_x = self.center_x.to_inch().optimize() + self.center_y = self.center_y.to_inch().optimize() + + def to_metric(self): + self.diameter = self.diameter.to_metric().optimize() + self.center_x = self.center_x.to_metric().optimize() + self.center_y = self.center_y.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + diameter = self.diameter.to_gerber(settings), + x = self.center_x.to_gerber(settings), + y = self.center_y.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{diameter},{x},{y},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + for modifier in [self.diameter, self.center_x, self.center_y, self.rotation]: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMVectorLinePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' if modifiers[0].value == 1 else 'off' + width = modifiers[1] + start_x = modifiers[2] + start_y = modifiers[3] + end_x = modifiers[4] + end_y = modifiers[5] + rotation = modifiers[6] + return cls(code, exposure, width, start_x, start_y, end_x, end_y, rotation) + + def __init__(self, code, exposure, width, start_x, start_y, end_x, end_y, rotation): + super(AMVectorLinePrimitiveDef, self).__init__(code, exposure, rotation) + self.width = width + self.start_x = start_x + self.start_y = start_y + self.end_x = end_x + self.end_y = end_y + + def to_inch(self): + self.width = self.width.to_inch().optimize() + self.start_x = self.start_x.to_inch().optimize() + self.start_y = self.start_y.to_inch().optimize() + self.end_x = self.end_x.to_inch().optimize() + self.end_y = self.end_y.to_inch().optimize() + + def to_metric(self): + self.width = self.width.to_metric().optimize() + self.start_x = self.start_x.to_metric().optimize() + self.start_y = self.start_y.to_metric().optimize() + self.end_x = self.end_x.to_metric().optimize() + self.end_y = self.end_y.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + width = self.width.to_gerber(settings), + start_x = self.start_x.to_gerber(settings), + start_y = self.start_y.to_gerber(settings), + end_x = self.end_x.to_gerber(settings), + end_y = self.end_y.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{width},{start_x},{start_y},{end_x},{end_y},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + modifiers = [self.width, self.start_x, self.start_y, self.end_x, self.end_y, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMCenterLinePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' if modifiers[0].value == 1 else 'off' + width = modifiers[1] + height = modifiers[2] + x = modifiers[3] + y = modifiers[4] + rotation = modifiers[5] + return cls(code, exposure, width, height, x, y, rotation) + + def __init__(self, code, exposure, width, height, x, y, rotation): + super(AMCenterLinePrimitiveDef, self).__init__(code, exposure, rotation) + self.width = width + self.height = height + self.x = x + self.y = y + + def to_inch(self): + self.width = self.width.to_inch().optimize() + self.height = self.height.to_inch().optimize() + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + + def to_metric(self): + self.width = self.width.to_metric().optimize() + self.height = self.height.to_metric().optimize() + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + width = self.width.to_gerber(settings), + height = self.height.to_gerber(settings), + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{width},{height},{x},{y},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + modifiers = [self.width, self.height, self.x, self.y, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMOutlinePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + num_points = int(modifiers[1].value + 1) + code = code + exposure = 'on' if modifiers[0].value == 1 else 'off' + addrs = modifiers[2:num_points * 2 + 2] + rotation = modifiers[2 + num_points * 2] + return cls(code, exposure, addrs, rotation) + + def __init__(self, code, exposure, addrs, rotation): + super(AMOutlinePrimitiveDef, self).__init__(code, exposure, rotation) + self.addrs = addrs + + def to_inch(self): + self.addrs = [i.to_inch().optimize() for i in self.addrs] + + def to_metric(self): + self.addrs = [i.to_metric().optimize() for i in self.addrs] + + def to_gerber(self, settings=None): + def strs(): + yield '%d,%d,%d' % (self.code, + 1 if self.exposure == 'on' else 0, + len(self.addrs) / 2 - 1) + for i in self.addrs: + yield i.to_gerber(settings) + yield self.rotation.to_gerber(settings) + + return '%s*' % ','.join(strs()) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + yield (OpCode.PUSH, int(len(self.addrs) / 2 - 1)) + for modifier in self.addrs: + for i in modifier.to_instructions(): + yield i + for i in self.rotation.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMPolygonPrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' if modifiers[0].value == 1 else 'off' + vertices = modifiers[1] + x = modifiers[2] + y = modifiers[3] + diameter = modifiers[4] + rotation = modifiers[5] + return cls(code, exposure, vertices, x, y, diameter, rotation) + + def __init__(self, code, exposure, vertices, x, y, diameter, rotation): + super(AMPolygonPrimitiveDef, self).__init__(code, exposure, rotation) + self.vertices = vertices + self.x = x + self.y = y + self.diameter = diameter + + def to_inch(self): + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + self.diameter = self.diameter.to_inch().optimize() + + def to_metric(self): + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + self.diameter = self.diameter.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + exposure = 1 if self.exposure == 'on' else 0, + vertices = self.vertices.to_gerber(settings), + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + diameter = self.diameter.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{exposure},{vertices},{x},{y},{diameter},{rotation}*'.format(**data) + + def to_instructions(self): + yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0) + modifiers = [self.vertices, self.x, self.y, self.diameter, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMMoirePrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' + x = modifiers[0] + y = modifiers[1] + diameter = modifiers[2] + ring_thickness = modifiers[3] + gap = modifiers[4] + max_rings = modifiers[5] + crosshair_thickness = modifiers[6] + crosshair_length = modifiers[7] + rotation = modifiers[8] + return cls(code, exposure, x, y, diameter, ring_thickness, gap, + max_rings, crosshair_thickness, crosshair_length, rotation) + + def __init__(self, code, exposure, x, y, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation): + super(AMMoirePrimitiveDef, self).__init__(code, exposure, rotation) + self.x = x + self.y = y + self.diameter = diameter + self.ring_thickness = ring_thickness + self.gap = gap + self.max_rings = max_rings + self.crosshair_thickness = crosshair_thickness + self.crosshair_length = crosshair_length + + def to_inch(self): + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + self.diameter = self.diameter.to_inch().optimize() + self.ring_thickness = self.ring_thickness.to_inch().optimize() + self.gap = self.gap.to_inch().optimize() + self.crosshair_thickness = self.crosshair_thickness.to_inch().optimize() + self.crosshair_length = self.crosshair_length.to_inch().optimize() + + def to_metric(self): + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + self.diameter = self.diameter.to_metric().optimize() + self.ring_thickness = self.ring_thickness.to_metric().optimize() + self.gap = self.gap.to_metric().optimize() + self.crosshair_thickness = self.crosshair_thickness.to_metric().optimize() + self.crosshair_length = self.crosshair_length.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + diameter = self.diameter.to_gerber(settings), + ring_thickness = self.ring_thickness.to_gerber(settings), + gap = self.gap.to_gerber(settings), + max_rings = self.max_rings.to_gerber(settings), + crosshair_thickness = self.crosshair_thickness.to_gerber(settings), + crosshair_length = self.crosshair_length.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{x},{y},{diameter},{ring_thickness},{gap},{max_rings},'\ + '{crosshair_thickness},{crosshair_length},{rotation}*'.format(**data) + + def to_instructions(self): + modifiers = [self.x, self.y, self.diameter, + self.ring_thickness, self.gap, self.max_rings, + self.crosshair_thickness, self.crosshair_length, + self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMThermalPrimitiveDef(AMPrimitiveDef): + @classmethod + def from_modifiers(cls, code, modifiers): + code = code + exposure = 'on' + x = modifiers[0] + y = modifiers[1] + outer_diameter = modifiers[2] + inner_diameter = modifiers[3] + gap = modifiers[4] + rotation = modifiers[5] + return cls(code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation) + + def __init__(self, code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation): + super(AMThermalPrimitiveDef, self).__init__(code, exposure, rotation) + self.x = x + self.y = y + self.outer_diameter = outer_diameter + self.inner_diameter = inner_diameter + self.gap = gap + + def to_inch(self): + self.x = self.x.to_inch().optimize() + self.y = self.y.to_inch().optimize() + self.outer_diameter = self.outer_diameter.to_inch().optimize() + self.inner_diameter = self.inner_diameter.to_inch().optimize() + self.gap = self.gap.to_inch().optimize() + + def to_metric(self): + self.x = self.x.to_metric().optimize() + self.y = self.y.to_metric().optimize() + self.outer_diameter = self.outer_diameter.to_metric().optimize() + self.inner_diameter = self.inner_diameter.to_metric().optimize() + self.gap = self.gap.to_metric().optimize() + + def to_gerber(self, settings=None): + data = dict(code = self.code, + x = self.x.to_gerber(settings), + y = self.y.to_gerber(settings), + outer_diameter = self.outer_diameter.to_gerber(settings), + inner_diameter = self.inner_diameter.to_gerber(settings), + gap = self.gap.to_gerber(settings), + rotation = self.rotation.to_gerber(settings)) + return '{code},{x},{y},{outer_diameter},{inner_diameter},'\ + '{gap},{rotation}*'.format(**data) + + def to_instructions(self): + modifiers = [self.x, self.y, self.outer_diameter, + self.inner_diameter, self.gap, self.rotation] + for modifier in modifiers: + for i in modifier.to_instructions(): + yield i + yield (OpCode.PRIM, self.code) + +class AMVariableDef(object): + def __init__(self, number, value): + self.number = number + self.value = value + + def to_inch(self): + return self + + def to_metric(self): + return self + + def to_gerber(self, settings=None): + return '$%d=%s*' % (self.number, self.value.to_gerber(settings)) + + def to_instructions(self): + for i in self.value.to_instructions(): + yield i + yield (OpCode.STORE, self.number) + + def rotate(self, angle, center=None): + pass + +def to_primitive_defs(instructions): + classes = { + 0: AMCommentPrimitiveDef, + 1: AMCirclePrimitiveDef, + 2: AMVectorLinePrimitiveDef, + 20: AMVectorLinePrimitiveDef, + 21: AMCenterLinePrimitiveDef, + 4: AMOutlinePrimitiveDef, + 5: AMPolygonPrimitiveDef, + 6: AMMoirePrimitiveDef, + 7: AMThermalPrimitiveDef, + } + for code, modifiers in eval_macro(instructions): + if code < 0: + yield AMVariableDef(-code, modifiers[0]) + else: + primitive = classes[code] + yield primitive.from_modifiers(code, modifiers) diff --git a/gerbonara/gerber/panelize/common.py b/gerbonara/gerber/panelize/common.py new file mode 100644 index 0000000..03bf9b0 --- /dev/null +++ b/gerbonara/gerber/panelize/common.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +import os +from ..common import loads as loads_org +from ..exceptions import ParseError +from ..utils import detect_file_format +from .. import rs274x +from .. import ipc356 + +from . import rs274x as ex_rs274x +from . import excellon +from . import dxf + +def read(filename, format=None): + with open(filename, 'rU') as f: + data = f.read() + return loads(data, filename, format=format) + + +def loads(data, filename=None, format=None): + if os.path.splitext(filename if filename else '')[1].lower() == '.dxf': + return dxf.loads(data, filename) + + fmt = detect_file_format(data) + if fmt == 'rs274x': + file = ex_rs274x.loads(data, filename=filename) + return ex_rs274x.GerberFile.from_gerber_file(file) + elif fmt == 'excellon': + return excellon.loads(data, filename=filename, format=format) + elif fmt == 'ipc_d_356': + return ipc356.loads(data, filename=filename) + else: + raise ParseError('Unable to detect file format') + + +def rectangle(width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None): + return dxf.DxfFile.rectangle( + width, height, left, bottom, units, draw_mode, filename) diff --git a/gerbonara/gerber/panelize/composition.py b/gerbonara/gerber/panelize/composition.py new file mode 100644 index 0000000..619a0cf --- /dev/null +++ b/gerbonara/gerber/panelize/composition.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama +import os +from functools import reduce +from ..cam import FileSettings +from ..gerber_statements import EofStmt +from ..excellon_statements import * +from ..excellon import DrillSlot, DrillHit +from . import rs274x +from . import excellon +from . import dxf + +class Composition(object): + def __init__(self, settings = None, comments = None): + self.settings = settings + self.comments = comments if comments != None else [] + +class GerberComposition(Composition): + APERTURE_ID_BIAS = 10 + + def __init__(self, settings=None, comments=None): + super(GerberComposition, self).__init__(settings, comments) + self.aperture_macros = {} + self.apertures = [] + self.drawings = [] + + def merge(self, file): + if isinstance(file, rs274x.GerberFile): + self._merge_gerber(file) + elif isinstance(file, dxf.DxfFile): + self._merge_dxf(file) + else: + raise Exception('unsupported file type') + + def dump(self, path): + def statements(): + for k in self.aperture_macros: + yield self.aperture_macros[k] + for s in self.apertures: + yield s + for s in self.drawings: + yield s + yield EofStmt() + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' + with open(path, 'w') as f: + rs274x.write_gerber_header(f, self.settings) + for statement in statements(): + f.write(statement.to_gerber(self.settings) + '\n') + + def _merge_gerber(self, file): + aperture_macro_map = {} + aperture_map = {} + + if self.settings: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + for macro in file.aperture_macros: + statement = file.aperture_macros[macro] + name = statement.name + newname = self._register_aperture_macro(statement) + aperture_macro_map[name] = newname + + for statement in file.aperture_defs: + if statement.param == 'AD': + if statement.shape in aperture_macro_map: + statement.shape = aperture_macro_map[statement.shape] + dnum = statement.d + newdnum = self._register_aperture(statement) + aperture_map[dnum] = newdnum + + for statement in file.main_statements: + if statement.type == 'APERTURE': + statement.d = aperture_map[statement.d] + self.drawings.append(statement) + + if not self.settings: + self.settings = file.context + + def _merge_dxf(self, file): + if self.settings: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + file.dcode = self._register_aperture(file.aperture) + self.drawings.append(file.statements) + + if not self.settings: + self.settings = file.settings + + + def _register_aperture_macro(self, statement): + name = statement.name + newname = name + offset = 0 + while newname in self.aperture_macros: + offset += 1 + newname = '%s_%d' % (name, offset) + statement.name = newname + self.aperture_macros[newname] = statement + return newname + + def _register_aperture(self, statement): + statement.d = len(self.apertures) + self.APERTURE_ID_BIAS + self.apertures.append(statement) + return statement.d + +class DrillComposition(Composition): + def __init__(self, settings=None, comments=None): + super(DrillComposition, self).__init__(settings, comments) + self.tools = [] + self.hits = [] + self.dxf_statements = [] + + def merge(self, file): + if isinstance(file, excellon.ExcellonFileEx): + self._merge_excellon(file) + elif isinstance(file, DxfFile): + self._merge_dxf(file) + else: + raise Exception('unsupported file type') + + def dump(self, path): + def statements(): + for t in self.tools: + yield ToolSelectionStmt(t.number).to_excellon(self.settings) + for h in self.hits: + if h.tool.number == t.number: + yield h.to_excellon(self.settings) + for num, statement in self.dxf_statements: + if num == t.number: + yield statement.to_excellon(self.settings) + yield EndOfProgramStmt().to_excellon() + + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' + with open(path, 'w') as f: + excellon.write_excellon_header(f, self.settings, self.tools) + for statement in statements(): + f.write(statement + '\n') + + def _merge_excellon(self, file): + tool_map = {} + + if not self.settings: + self.settings = file.settings + else: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + for tool in iter(file.tools.values()): + num = tool.number + tool_map[num] = self._register_tool(tool) + + for hit in file.hits: + hit.tool = tool_map[hit.tool.number] + self.hits.append(hit) + + def _merge_dxf(self, file): + if not self.settings: + self.settings = file.settings + else: + if self.settings.units == 'metric': + file.to_metric() + else: + file.to_inch() + + tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width)) + self.dxf_statements.append((tool.number, file.statements)) + + def _register_tool(self, tool): + for existing in self.tools: + if existing.equivalent(tool): + return existing + new_tool = ExcellonTool.from_tool(tool) + new_tool.settings = self.settings + def toolnums(): + for tool in self.tools: + yield tool.number + max_num = reduce(lambda x, y: x if x > y else y, toolnums(), 0) + new_tool.number = max_num + 1 + self.tools.append(new_tool) + return new_tool diff --git a/gerbonara/gerber/panelize/dxf.py b/gerbonara/gerber/panelize/dxf.py new file mode 100644 index 0000000..9eb9217 --- /dev/null +++ b/gerbonara/gerber/panelize/dxf.py @@ -0,0 +1,796 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +import io, sys +from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt +import dxfgrabber +from ..cam import CamFile, FileSettings +from ..utils import inch, metric, write_gerber_value, rotate_point +from ..gerber_statements import ADParamStmt +from ..excellon_statements import ExcellonTool +from ..excellon_statements import CoordinateStmt +from .utility import is_equal_point, is_equal_value +from .dxf_path import generate_paths, judge_containment +from .excellon import write_excellon_header +from .rs274x import write_gerber_header + +ACCEPTABLE_ERROR = 0.001 + +def _normalize_angle(start_angle, end_angle): + angle = end_angle - start_angle + if angle > 0: + start = start_angle % 360 + else: + angle = -angle + start = end_angle % 360 + angle = min(angle, 360) + start = start - 360 if start > 180 else start + + regions = [] + while angle > 0: + end = start + angle + if end <= 180: + regions.append((start * pi / 180, end * pi / 180)) + angle = 0 + else: + regions.append((start * pi / 180, pi)) + angle = end - 180 + start = -180 + return regions + +def _intersections_of_line_and_circle(start, end, center, radius, error_range): + x1 = start[0] - center[0] + y1 = start[1] - center[1] + x2 = end[0] - center[0] + y2 = end[1] - center[1] + + dx = x2 - x1 + dy = y2 - y1 + dr = sqrt(dx * dx + dy * dy) + D = x1 * y2 - x2 * y1 + + distance = abs(dy * x1 - dx * y1) / dr + + D2 = D * D + dr2 = dr * dr + r2 = radius * radius + delta = r2 * dr2 - D2 + if distance > radius - error_range and distance < radius + error_range: + delta = 0 + if delta < 0: + return None + + sqrt_D = sqrt(delta) + E_x = -dx * sqrt_D if dy < 0 else dx * sqrt_D + E_y = abs(dy) * sqrt_D + + p1_x = (D * dy + E_x) / dr2 + p2_x = (D * dy - E_x) / dr2 + p1_y = (-D * dx + E_y) / dr2 + p2_y = (-D * dx - E_y) / dr2 + + p1_angle = atan2(p1_y, p1_x) + p2_angle = atan2(p2_y, p2_x) + if dx == 0: + p1_t = (p1_y - y1) / dy + p2_t = (p2_y - y1) / dy + else: + p1_t = (p1_x - x1) / dx + p2_t = (p2_x - x1) / dx + + if delta == 0: + return ( + (p1_x + center[0], p1_y + center[1]), + None, + p1_angle, None, + p1_t, None + ) + else: + return ( + (p1_x + center[0], p1_y + center[1]), + (p2_x + center[0], p2_y + center[1]), + p1_angle, p2_angle, + p1_t, p2_t + ) + +class DxfStatement(object): + def __init__(self, entity): + self.entity = entity + self.start = None + self.end = None + self.is_closed = False + + def to_inch(self): + pass + + def to_metric(self): + pass + + def is_equal_to(self, target, error_range=0): + return False + + def reverse(self): + raise Exception('Not implemented') + + def offset(self, offset_x, offset_y): + raise Exception('Not supported') + + def rotate(self, angle, center=(0, 0)): + raise Exception('Not supported') + + +class DxfLineStatement(DxfStatement): + @classmethod + def from_entity(cls, entity): + start = (entity.start[0], entity.start[1]) + end = (entity.end[0], entity.end[1]) + return cls(entity, start, end) + + @property + def bounding_box(self): + return (min(self.start[0], self.end[0]), + min(self.start[1], self.end[1]), + max(self.start[0], self.end[0]), + max(self.start[1], self.end[1])) + + def __init__(self, entity, start, end): + super(DxfLineStatement, self).__init__(entity) + self.start = start + self.end = end + + def to_inch(self): + self.start = ( + inch(self.start[0]), inch(self.start[1])) + self.end = ( + inch(self.end[0]), inch(self.end[1])) + + def to_metric(self): + self.start = ( + metric(self.start[0]), metric(self.start[1])) + self.end = ( + metric(self.end[0]), metric(self.end[1])) + + def is_equal_to(self, target, error_range=0): + if not isinstance(target, DxfLineStatement): + return False + return (is_equal_point(self.start, target.start, error_range) and \ + is_equal_point(self.end, target.end, error_range)) or \ + (is_equal_point(self.start, target.end, error_range) and \ + is_equal_point(self.end, target.start, error_range)) + + def reverse(self): + pt = self.start + self.start = self.end + self.end = pt + + def dots(self, pitch, width, offset=0): + x0, y0 = self.start + x1, y1 = self.end + y1 = self.end[1] + xp = x1 - x0 + yp = y1 - y0 + l = sqrt(xp * xp + yp * yp) + xd = xp * pitch / l + yd = yp * pitch / l + x0 += xp * offset / l + y0 += yp * offset / l + + if offset > l + width / 2: + return (None, offset - l) + else: + d = offset; + while d < l + width / 2: + yield ((x0, y0), d - l) + x0 += xd + y0 += yd + d += pitch + + def offset(self, offset_x, offset_y): + self.start = (self.start[0] + offset_x, self.start[1] + offset_y) + self.end = (self.end[0] + offset_x, self.end[1] + offset_y) + + def rotate(self, angle, center=(0, 0)): + self.start = rotate_point(self.start, angle, center) + self.end = rotate_point(self.end, angle, center) + + def intersections_with_halfline(self, point_from, point_to, error_range): + denominator = (self.end[0] - self.start[0]) * (point_to[1] - point_from[1]) - \ + (self.end[1] - self.start[1]) * (point_to[0] - point_from[0]) + de = error_range * error_range + if denominator >= -de and denominator <= de: + return [] + from_dx = point_from[0] - self.start[0] + from_dy = point_from[1] - self.start[1] + r = ((point_to[1] - point_from[1]) * from_dx - + (point_to[0] - point_from[0]) * from_dy) / denominator + s = ((self.end[1] - self.start[1]) * from_dx - + (self.end[0] - self.start[0]) * from_dy) / denominator + dx = (self.end[0] - self.start[0]) + dy = (self.end[1] - self.start[1]) + le = error_range / sqrt(dx * dx + dy * dy) + if s < 0 or r < -le or r > 1 + le: + return [] + + pt = (self.start[0] + (self.end[0] - self.start[0]) * r, + self.start[1] + (self.end[1] - self.start[1]) * r) + if is_equal_point(pt, self.start, error_range): + return [] + else: + return [pt] + + def intersections_with_arc(self, center, radius, angle_regions, error_range): + intersection = \ + _intersections_of_line_and_circle(self.start, self.end, center, radius, error_range) + if intersection is None: + return [] + else: + p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection + + pts = [] + if p1_t >= 0 and p1_t <= 1: + for region in angle_regions: + if p1_angle >= region[0] and p1_angle <= region[1]: + pts.append(p1) + break + if p2 is not None and p2_t >= 0 and p2_t <= 1: + for region in angle_regions: + if p2_angle >= region[0] and p2_angle <= region[1]: + pts.append(p2) + break + + return pts + +class DxfArcStatement(DxfStatement): + def __init__(self, entity): + super(DxfArcStatement, self).__init__(entity) + if entity.dxftype == 'CIRCLE': + self.radius = self.entity.radius + self.center = (self.entity.center[0], self.entity.center[1]) + self.start = (self.center[0] + self.radius, self.center[1]) + self.end = self.start + self.start_angle = 0 + self.end_angle = 360 + self.is_closed = True + elif entity.dxftype == 'ARC': + self.start_angle = self.entity.start_angle + self.end_angle = self.entity.end_angle + self.radius = self.entity.radius + self.center = (self.entity.center[0], self.entity.center[1]) + self.start = ( + self.center[0] + self.radius * cos(self.start_angle / 180. * pi), + self.center[1] + self.radius * sin(self.start_angle / 180. * pi), + ) + self.end = ( + self.center[0] + self.radius * cos(self.end_angle / 180. * pi), + self.center[1] + self.radius * sin(self.end_angle / 180. * pi), + ) + angle = self.end_angle - self.start_angle + self.is_closed = angle >= 360 or angle <= -360 + else: + raise Exception('invalid DXF type was specified') + self.angle_regions = _normalize_angle(self.start_angle, self.end_angle) + + @property + def bounding_box(self): + return (self.center[0] - self.radius, self.center[1] - self.radius, + self.center[0] + self.radius, self.center[1] + self.radius) + + def to_inch(self): + self.radius = inch(self.radius) + self.center = (inch(self.center[0]), inch(self.center[1])) + self.start = (inch(self.start[0]), inch(self.start[1])) + self.end = (inch(self.end[0]), inch(self.end[1])) + + def to_metric(self): + self.radius = metric(self.radius) + self.center = (metric(self.center[0]), metric(self.center[1])) + self.start = (metric(self.start[0]), metric(self.start[1])) + self.end = (metric(self.end[0]), metric(self.end[1])) + + def is_equal_to(self, target, error_range=0): + if not isinstance(target, DxfArcStatement): + return False + aerror_range = error_range / pi * self.radius * 180 + return is_equal_point(self.center, target.center, error_range) and \ + is_equal_value(self.radius, target.radius, error_range) and \ + ((is_equal_value(self.start_angle, target.start_angle, aerror_range) and + is_equal_value(self.end_angle, target.end_angle, aerror_range)) or + (is_equal_value(self.start_angle, target.end_angle, aerror_range) and + is_equal_value(self.end_angle, target.end_angle, aerror_range))) + + def reverse(self): + tmp = self.start_angle + self.start_angle = self.end_angle + self.end_angle = tmp + tmp = self.start + self.start = self.end + self.end = tmp + + def dots(self, pitch, width, offset=0): + angle = self.end_angle - self.start_angle + afactor = 1 if angle > 0 else -1 + aangle = angle * afactor + L = 2 * pi * self.radius + l = L * aangle / 360 + pangle = pitch / L * 360 + wangle = width / L * 360 + oangle = offset / L * 360 + + if offset > l + width / 2: + yield (None, offset - l) + else: + da = oangle + while da < aangle + wangle / 2: + cangle = self.start_angle + da * afactor + x = self.radius * cos(cangle / 180 * pi) + self.center[0] + y = self.radius * sin(cangle / 180 * pi) + self.center[1] + remain = (da - aangle) / 360 * L + yield((x, y), remain) + da += pangle + + def offset(self, offset_x, offset_y): + self.center = (self.center[0] + offset_x, self.center[1] + offset_y) + self.start = (self.start[0] + offset_x, self.start[1] + offset_y) + self.end = (self.end[0] + offset_x, self.end[1] + offset_y) + + def rotate(self, angle, center=(0, 0)): + self.start_angle += angle + self.end_angle += angle + self.center = rotate_point(self.center, angle, center) + self.start = rotate_point(self.start, angle, center) + self.end = rotate_point(self.end, angle, center) + self.angle_regions = _normalize_angle(self.start_angle, self.end_angle) + + def intersections_with_halfline(self, point_from, point_to, error_range): + intersection = \ + _intersections_of_line_and_circle( + point_from, point_to, self.center, self.radius, error_range) + if intersection is None: + return [] + else: + p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection + + if is_equal_point(p1, self.start, error_range): + p1 = None + elif p2 is not None and is_equal_point(p2, self.start, error_range): + p2 = None + + def is_contained(angle, region, error): + if angle >= region[0] - error and angle <= region[1] + error: + return True + if angle < 0 and region[1] > 0: + angle = angle + 2 * pi + elif angle > 0 and region[0] < 0: + angle = angle - 2 * pi + return angle >= region[0] - error and angle <= region[1] + error + + aerror = error_range * self.radius + pts = [] + if p1 is not None and p1_t >= 0 and not is_equal_point(p1, self.start, error_range): + for region in self.angle_regions: + if is_contained(p1_angle, region, aerror): + pts.append(p1) + break + if p2 is not None and p2_t >= 0 and not is_equal_point(p2, self.start, error_range): + for region in self.angle_regions: + if is_contained(p2_angle, region, aerror): + pts.append(p2) + break + + return pts + + def intersections_with_arc(self, center, radius, angle_regions, error_range): + x1 = center[0] - self.center[0] + y1 = center[1] - self.center[1] + r1 = self.radius + r2 = radius + cd_sq = x1 * x1 + y1 * y1 + cd = sqrt(cd_sq) + rd = abs(r1 - r2) + + if (cd >= 0 and cd <= rd) or cd >= r1 + r2: + return [] + + A = (cd_sq + r1 * r1 - r2 * r2) / 2 + scale = sqrt(cd_sq * r1 * r1 - A * A) / cd_sq + xl = A * x1 / cd_sq + xr = y1 * scale + yl = A * y1 / cd_sq + yr = x1 * scale + + pt1_x = xl + xr + pt1_y = yl - yr + pt2_x = xl - xr + pt2_y = yl + yr + pt1_angle1 = atan2(pt1_y, pt1_x) + pt1_angle2 = atan2(pt1_y - y1, pt1_x - x1) + pt2_angle1 = atan2(pt2_y, pt2_x) + pt2_angle2 = atan2(pt2_y - y1, pt2_x - x1) + + aerror = error_range * self.radius + pts=[] + for region in self.angle_regions: + if pt1_angle1 >= region[0] and pt1_angle1 <= region[1]: + for region in angle_regions: + if pt1_angle2 >= region[0] - aerror and pt1_angle2 <= region[1] + aerror: + pts.append((pt1_x + self.center[0], pt1_y + self.center[1])) + break + break + for region in self.angle_regions: + if pt2_angle1 >= region[0] and pt2_angle1 <= region[1]: + for region in angle_regions: + if pt2_angle2 >= region[0] - aerror and pt2_angle2 <= region[1] + aerror: + pts.append((pt2_x + self.center[0], pt2_y + self.center[1])) + break + break + return pts + +class DxfPolylineStatement(DxfStatement): + def __init__(self, entity): + super(DxfPolylineStatement, self).__init__(entity) + self.start = (self.entity.points[0][0], self.entity.points[0][1]) + self.is_closed = self.entity.is_closed + if self.is_closed: + self.end = self.start + else: + self.end = (self.entity.points[-1][0], self.entity.points[-1][1]) + + def disassemble(self): + class Item: + pass + + def ptseq(): + for i in range(1, len(self.entity.points)): + yield i + if self.entity.is_closed: + yield 0 + + x0 = self.entity.points[0][0] + y0 = self.entity.points[0][1] + b = self.entity.bulge[0] + for idx in ptseq(): + pt = self.entity.points[idx] + x1 = pt[0] + y1 = pt[1] + if b == 0: + item = Item() + item.dxftype = 'LINE' + item.start = (x0, y0) + item.end = (x1, y1) + item.is_closed = False + yield DxfLineStatement.from_entity(item) + else: + ang = 4 * atan(b) + xm = x0 + x1 + ym = y0 + y1 + t = 1 / tan(ang / 2) + xc = (xm - t * (y1 - y0)) / 2 + yc = (ym + t * (x1 - x0)) / 2 + r = sqrt((x0 - xc)*(x0 - xc) + (y0 - yc)*(y0 - yc)) + rx0 = x0 - xc + ry0 = y0 - yc + rc = max(min(rx0 / r, 1.0), -1.0) + start_angle = acos(rc) if ry0 > 0 else 2 * pi - acos(rc) + start_angle *= 180 / pi + end_angle = start_angle + ang * 180 / pi + + item = Item() + item.dxftype = 'ARC' + item.start = (x0, y0) + item.end = (x1, y1) + item.start_angle = start_angle + item.end_angle = end_angle + item.radius = r + item.center = (xc, yc) + item.is_closed = end_angle - start_angle >= 360 + yield DxfArcStatement(item) + + x0 = x1 + y0 = y1 + b = self.entity.bulge[idx] + + def to_inch(self): + self.start = (inch(self.start[0]), inch(self.start[1])) + self.end = (inch(self.end[0]), inch(self.end[1])) + for idx in range(0, len(self.entity.points)): + self.entity.points[idx] = ( + inch(self.entity.points[idx][0]), inch(self.entity.points[idx][1])) + + def to_metric(self): + self.start = (metric(self.start[0]), metric(self.start[1])) + self.end = (metric(self.end[0]), metric(self.end[1])) + for idx in range(0, len(self.entity.points)): + self.entity.points[idx] = ( + metric(self.entity.points[idx][0]), metric(self.entity.points[idx][1])) + + def offset(self, offset_x, offset_y): + for idx in range(len(self.entity.points)): + self.entity.points[idx] = ( + self.entity.points[idx][0] + offset_x, self.entity.points[idx][1] + offset_y) + + def rotate(self, angle, center=(0, 0)): + for idx in range(len(self.entity.points)): + self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center) + +class DxfStatements(object): + def __init__(self, statements, units, dcode=10, draw_mode=None, fill_mode=None): + if draw_mode is None: + draw_mode = DxfFile.DM_LINE + if fill_mode is None: + fill_mode = DxfFile.FM_TURN_OVER + self._units = units + self.dcode = dcode + self.draw_mode = draw_mode + self.fill_mode = fill_mode + self.pitch = inch(1) if self._units == 'inch' else 1 + self.width = 0 + self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR + self.statements = list(filter( + lambda i: not (isinstance(i, DxfLineStatement) and \ + is_equal_point(i.start, i.end, self.error_range)), + statements + )) + self.close_paths, self.open_paths = generate_paths(self.statements, self.error_range) + self.sorted_close_paths = [] + self.polarity = True # True means dark, False means clear + + @property + def units(self): + return _units + + def _polarity_command(self, polarity=None): + if polarity is None: + polarity = self.polarity + return '%LPD*%' if polarity else '%LPC*%' + + def _prepare_sorted_close_paths(self): + if self.sorted_close_paths: + return + for i in range(0, len(self.close_paths)): + for j in range(i + 1, len(self.close_paths)): + containee, container = judge_containment( + self.close_paths[i], self.close_paths[j], self.error_range) + if containee is not None: + containee.containers.append(container) + self.sorted_close_paths = sorted(self.close_paths, key=lambda path: len(path.containers)) + + def to_gerber(self, settings=FileSettings()): + def gerbers(): + yield 'G75*' + yield self._polarity_command() + yield 'D{0}*'.format(self.dcode) + if self.draw_mode == DxfFile.DM_FILL: + yield 'G36*' + if self.fill_mode == DxfFile.FM_TURN_OVER: + self._prepare_sorted_close_paths() + polarity = self.polarity + level = 0 + for path in self.sorted_close_paths: + if len(path.containers) > level: + level = len(path.containers) + polarity = not polarity + yield 'G37*' + yield self._polarity_command(polarity) + yield 'G36*' + yield path.to_gerber(settings) + else: + for path in self.close_paths: + yield path.to_gerber(settings) + yield 'G37*' + else: + pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0 + for path in self.open_paths: + yield path.to_gerber(settings, pitch=pitch, width=self.width) + for path in self.close_paths: + yield path.to_gerber(settings, pitch=pitch, width=self.width) + + return '\n'.join(gerbers()) + + def to_excellon(self, settings=FileSettings()): + if self.draw_mode == DxfFile.DM_FILL: + return + def drills(): + pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0 + for path in self.open_paths: + yield path.to_excellon(settings, pitch=pitch, width=self.width) + for path in self.close_paths: + yield path.to_excellon(settings, pitch=pitch, width=self.width) + return '\n'.join(drills()) + + def to_inch(self): + if self._units == 'metric': + self._units = 'inch' + self.pitch = inch(self.pitch) + self.width = inch(self.width) + self.error_range = inch(self.error_range) + for path in self.open_paths: + path.to_inch() + for path in self.close_paths: + path.to_inch() + + def to_metric(self): + if self._units == 'inch': + self._units = 'metric' + self.pitch = metric(self.pitch) + self.width = metric(self.width) + self.error_range = metric(self.error_range) + for path in self.open_paths: + path.to_metric() + for path in self.close_paths: + path.to_metric() + + def offset(self, offset_x, offset_y): + for path in self.open_paths: + path.offset(offset_x, offset_y) + for path in self.close_paths: + path.offset(offset_x, offset_y) + + def rotate(self, angle, center=(0, 0)): + for path in self.open_paths: + path.rotate(angle, center) + for path in self.close_paths: + path.rotate(angle, center) + +class DxfFile(CamFile): + DM_LINE = 0 + DM_FILL = 1 + DM_MOUSE_BITES = 2 + + FM_SIMPLE = 0 + FM_TURN_OVER = 1 + + FT_RX274X = 0 + FT_EXCELLON = 1 + + @classmethod + def from_dxf(cls, dxf, settings=None, draw_mode=None, filename=None): + fsettings = settings if settings else \ + FileSettings(zero_suppression='leading') + + if dxf.header['$INSUNITS'] == 1: + fsettings.units = 'inch' + if not settings: + fsettings.format = (2, 5) + else: + fsettings.units = 'metric' + if not settings: + fsettings.format = (3, 4) + + statements = [] + for entity in dxf.entities: + if entity.dxftype == 'LWPOLYLINE': + statements.append(DxfPolylineStatement(entity)) + elif entity.dxftype == 'LINE': + statements.append(DxfLineStatement.from_entity(entity)) + elif entity.dxftype == 'CIRCLE': + statements.append(DxfArcStatement(entity)) + elif entity.dxftype == 'ARC': + statements.append(DxfArcStatement(entity)) + + return cls(statements, fsettings, draw_mode, filename) + + @classmethod + def rectangle(cls, width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None): + if units == 'metric': + settings = FileSettings(units=units, zero_suppression='leading', format=(3,4)) + else: + settings = FileSettings(units=units, zero_suppression='leading', format=(2,5)) + statements = [ + DxfLineStatement(None, (left, bottom), (left + width, bottom)), + DxfLineStatement(None, (left + width, bottom), (left + width, bottom + height)), + DxfLineStatement(None, (left + width, bottom + height), (left, bottom + height)), + DxfLineStatement(None, (left, bottom + height), (left, bottom)), + ] + return cls(statements, settings, draw_mode, filename) + + def __init__(self, statements, settings=None, draw_mode=None, filename=None): + if not settings: + settings = FileSettings(units='metric', format=(3,4), zero_suppression='leading') + if draw_mode == None: + draw_mode = self.DM_LINE + + super(DxfFile, self).__init__(settings=settings, filename=filename) + self._draw_mode = draw_mode + self._fill_mode = self.FM_TURN_OVER + + self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0) + if settings.units == 'inch': + self.aperture.to_inch() + else: + self.aperture.to_metric() + self.statements = DxfStatements( + statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode, fill_mode=self.filename) + + @property + def dcode(self): + return self.aperture.dcode + + @dcode.setter + def dcode(self, value): + self.aperture.d = value + self.statements.dcode = value + + @property + def width(self): + return self.aperture.modifiers[0][0] + + @width.setter + def width(self, value): + self.aperture.modifiers = ([float(value),],) + self.statements.width = value + + @property + def draw_mode(self): + return self._draw_mode + + @draw_mode.setter + def draw_mode(self, value): + self._draw_mode = value + self.statements.draw_mode = value + + @property + def fill_mode(self): + return self._fill_mode + + @fill_mode.setter + def fill_mode(self, value): + self._fill_mode = value + self.statements.fill_mode = value + + @property + def pitch(self): + return self.statements.pitch + + @pitch.setter + def pitch(self, value): + self.statements.pitch = value + + def write(self, filename=None, filetype=FT_RX274X): + self.settings.notation = 'absolute' + self.settings.zeros = 'trailing' + filename = filename if filename is not None else self.filename + with open(filename, 'w') as f: + if filetype == self.FT_RX274X: + write_gerber_header(f, self.settings) + f.write(self.aperture.to_gerber(self.settings) + '\n') + f.write(self.statements.to_gerber(self.settings) + '\n') + f.write('M02*\n') + else: + tools = [ExcellonTool(self.settings, number=1, diameter=self.width)] + write_excellon_header(f, self.settings, tools) + f.write('T01\n') + f.write(self.statements.to_excellon(self.settings) + '\n') + f.write('M30\n') + + + def to_inch(self): + if self.units == 'metric': + self.aperture.to_inch() + self.statements.to_inch() + self.pitch = inch(self.pitch) + self.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + self.aperture.to_metric() + self.statements.to_metric() + self.pitch = metric(self.pitch) + self.units = 'metric' + + def offset(self, offset_x, offset_y): + self.statements.offset(offset_x, offset_y) + + def rotate(self, angle, center=(0, 0)): + self.statements.rotate(angle, center) + + def negate_polarity(self): + self.statements.polarity = not self.statements.polarity + +def loads(data, filename=None): + if sys.version_info.major == 2: + data = unicode(data) + stream = io.StringIO(data) + dxf = dxfgrabber.read(stream) + return DxfFile.from_dxf(dxf) diff --git a/gerbonara/gerber/panelize/dxf_path.py b/gerbonara/gerber/panelize/dxf_path.py new file mode 100644 index 0000000..201dcff --- /dev/null +++ b/gerbonara/gerber/panelize/dxf_path.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..utils import inch, metric, write_gerber_value +from ..cam import FileSettings +from .utility import is_equal_point, is_equal_value, normalize_vec2d, dot_vec2d +from .excellon import CoordinateStmtEx + +class DxfPath(object): + def __init__(self, statements, error_range=0): + self.statements = statements + self.error_range = error_range + self.bounding_box = statements[0].bounding_box + self.containers = [] + for statement in statements[1:]: + self._merge_bounding_box(statement.bounding_box) + + @property + def start(self): + return self.statements[0].start + + @property + def end(self): + return self.statements[-1].end + + @property + def is_closed(self): + if len(self.statements) == 1: + return self.statements[0].is_closed + else: + return is_equal_point(self.start, self.end, self.error_range) + + def is_equal_to(self, target, error_range=0): + if not isinstance(target, DxfPath): + return False + if len(self.statements) != len(target.statements): + return False + if is_equal_point(self.start, target.start, error_range) and \ + is_equal_point(self.end, target.end, error_range): + for i in range(0, len(self.statements)): + if not self.statements[i].is_equal_to(target.statements[i], error_range): + return False + return True + elif is_equal_point(self.start, target.end, error_range) and \ + is_equal_point(self.end, target.start, error_range): + for i in range(0, len(self.statements)): + if not self.statements[i].is_equal_to(target.statements[-1 - i], error_range): + return False + return True + return False + + def contain(self, target, error_range=0): + for statement in self.statements: + if statement.is_equal_to(target, error_range): + return True + else: + return False + + def to_inch(self): + self.error_range = inch(self.error_range) + for statement in self.statements: + statement.to_inch() + + def to_metric(self): + self.error_range = metric(self.error_range) + for statement in self.statements: + statement.to_metric() + + def offset(self, offset_x, offset_y): + for statement in self.statements: + statement.offset(offset_x, offset_y) + + def rotate(self, angle, center=(0, 0)): + for statement in self.statements: + statement.rotate(angle, center) + + def reverse(self): + rlist = [] + for statement in reversed(self.statements): + statement.reverse() + rlist.append(statement) + self.statements = rlist + + def merge(self, element, error_range=0): + if self.is_closed or element.is_closed: + return False + if not error_range: + error_range = self.error_range + if is_equal_point(self.end, element.start, error_range): + return self._append_at_end(element, error_range) + elif is_equal_point(self.end, element.end, error_range): + element.reverse() + return self._append_at_end(element, error_range) + elif is_equal_point(self.start, element.end, error_range): + return self._insert_on_top(element, error_range) + elif is_equal_point(self.start, element.start, error_range): + element.reverse() + return self._insert_on_top(element, error_range) + else: + return False + + def _append_at_end(self, element, error_range=0): + if isinstance(element, DxfPath): + if self.is_equal_to(element, error_range): + return False + for i in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[-1 - i].is_equal_to(element.statements[i]): + break + for j in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[j].is_equal_to(element.statements[-1 - j]): + break + if i + j >= len(element.statements): + return False + mergee = list(element.statements) + if i > 0: + del mergee[0:i] + del self.statements[-i] + if j > 0: + del mergee[-j] + del self.statements[0:j] + for statement in mergee: + self._merge_bounding_box(statement.bounding_box) + self.statements.extend(mergee) + return True + else: + if self.statements[-1].is_equal_to(element, error_range) or \ + self.statements[0].is_equal_to(element, error_range): + return False + self._merge_bounding_box(element.bounding_box) + self.statements.appen(element) + return True + + def _insert_on_top(self, element, error_range=0): + if isinstance(element, DxfPath): + if self.is_equal_to(element, error_range): + return False + for i in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[-1 - i].is_equal_to(element.statements[i]): + break + for j in range(0, min(len(self.statements), len(element.statements))): + if not self.statements[j].is_equal_to(element.statements[-1 - j]): + break + if i + j >= len(element.statements): + return False + mergee = list(element.statements) + if i > 0: + del mergee[0:i] + del self.statements[-i] + if j > 0: + del mergee[-j] + del self.statements[0:j] + self.statements[0:0] = mergee + return True + else: + if self.statements[-1].is_equal_to(element, error_range) or \ + self.statements[0].is_equal_to(element, error_range): + return False + self.statements.insert(0, element) + return True + + def _merge_bounding_box(self, box): + self.bounding_box = (min(self.bounding_box[0], box[0]), + min(self.bounding_box[1], box[1]), + max(self.bounding_box[2], box[2]), + max(self.bounding_box[3], box[3])) + + def may_be_in_collision(self, path): + if self.bounding_box[0] >= path.bounding_box[2] or \ + self.bounding_box[1] >= path.bounding_box[3] or \ + self.bounding_box[2] <= path.bounding_box[0] or \ + self.bounding_box[3] <= path.bounding_box[1]: + return False + else: + return True + + def to_gerber(self, settings=FileSettings(), pitch=0, width=0): + from .dxf import DxfArcStatement + if pitch == 0: + x0, y0 = self.statements[0].start + gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format( + write_gerber_value(x0, settings.format, + settings.zero_suppression), + write_gerber_value(y0, settings.format, + settings.zero_suppression), + ) + + for statement in self.statements: + x0, y0 = statement.start + x1, y1 = statement.end + if isinstance(statement, DxfArcStatement): + xc, yc = statement.center + gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format( + '03' if statement.end_angle > statement.start_angle else '02', + write_gerber_value(x1, settings.format, + settings.zero_suppression), + write_gerber_value(y1, settings.format, + settings.zero_suppression), + write_gerber_value(xc - x0, settings.format, + settings.zero_suppression), + write_gerber_value(yc - y0, settings.format, + settings.zero_suppression) + ) + else: + gerber += '\nG01*\nX{0}Y{1}D01*'.format( + write_gerber_value(x1, settings.format, + settings.zero_suppression), + write_gerber_value(y1, settings.format, + settings.zero_suppression), + ) + else: + def ploter(x, y): + return 'X{0}Y{1}D03*\n'.format( + write_gerber_value(x, settings.format, + settings.zero_suppression), + write_gerber_value(y, settings.format, + settings.zero_suppression), + ) + gerber = self._plot_dots(pitch, width, ploter) + + return gerber + + def to_excellon(self, settings=FileSettings(), pitch=0, width=0): + from .dxf import DxfArcStatement + if pitch == 0: + x0, y0 = self.statements[0].start + excellon = 'G00{0}\nM15\n'.format( + CoordinateStmtEx(x=x0, y=y0).to_excellon(settings)) + + for statement in self.statements: + x0, y0 = statement.start + x1, y1 = statement.end + if isinstance(statement, DxfArcStatement): + i = statement.center[0] - x0 + j = statement.center[1] - y0 + excellon += '{0}{1}\n'.format( + 'G03' if statement.end_angle > statement.start_angle else 'G02', + CoordinateStmtEx(x=x1, y=y1, i=i, j=j).to_excellon(settings)) + else: + excellon += 'G01{0}\n'.format( + CoordinateStmtEx(x=x1, y=y1).to_excellon(settings)) + + excellon += 'M16\nG05\n' + else: + def ploter(x, y): + return CoordinateStmtEx(x=x, y=y).to_excellon(settings) + '\n' + excellon = self._plot_dots(pitch, width, ploter) + + return excellon + + def _plot_dots(self, pitch, width, ploter): + out = '' + offset = 0 + for idx in range(0, len(self.statements)): + statement = self.statements[idx] + if offset < 0: + offset += pitch + for dot, offset in statement.dots(pitch, width, offset): + if dot is None: + break + if offset > 0 and (statement.is_closed or idx != len(self.statements) - 1): + break + #if idx == len(self.statements) - 1 and statement.is_closed and offset > -pitch: + # break + out += ploter(dot[0], dot[1]) + return out + + def intersections_with_halfline(self, point_from, point_to, error_range=0): + def calculator(statement): + return statement.intersections_with_halfline(point_from, point_to, error_range) + def validator(pt, statement, idx): + if is_equal_point(pt, statement.end, error_range) and \ + not self._judge_cross(point_from, point_to, idx, error_range): + return False + return True + return self._collect_intersections(calculator, validator, error_range) + + def intersections_with_arc(self, center, radius, angle_regions, error_range=0): + def calculator(statement): + return statement.intersections_with_arc(center, radius, angle_regions, error_range) + return self._collect_intersections(calculator, None, error_range) + + def _collect_intersections(self, calculator, validator, error_range): + allpts = [] + last = allpts + for i in range(0, len(self.statements)): + statement = self.statements[i] + cur = calculator(statement) + if cur: + for pt in cur: + for dest in allpts: + if is_equal_point(pt, dest, error_range): + break + else: + if validator is not None and not validator(pt, statement, i): + continue + allpts.append(pt) + last = cur + return allpts + + def _judge_cross(self, from_pt, to_pt, index, error_range): + standard = normalize_vec2d((to_pt[0] - from_pt[0], to_pt[1] - from_pt[1])) + normal = (standard[1], -standard[0]) + def statements(): + for i in range(index, len(self.statements)): + yield self.statements[i] + for i in range(0, index): + yield self.statements[i] + dot_standard = None + for statement in statements(): + tstart = statement.start + tend = statement.end + target = normalize_vec2d((tend[0] - tstart[0], tend[1] - tstart[1])) + dot= dot_vec2d(normal, target) + if dot_standard is None: + dot_standard = dot + continue + if is_equal_point(standard, target, error_range): + continue + return (dot_standard > 0 and dot > 0) or (dot_standard < 0 and dot < 0) + raise Exception('inconsistensy is detected while cross judgement between paths') + +def generate_paths(statements, error_range=0): + from .dxf import DxfPolylineStatement + + paths = [] + for statement in filter(lambda s: isinstance(s, DxfPolylineStatement), statements): + units = [unit for unit in statement.disassemble()] + paths.append(DxfPath(units, error_range)) + + unique_statements = [] + redundant = 0 + for statement in filter(lambda s: not isinstance(s, DxfPolylineStatement), statements): + for path in paths: + if path.contain(statement): + redundant += 1 + break + else: + for target in unique_statements: + if statement.is_equal_to(target, error_range): + redundant += 1 + break + else: + unique_statements.append(statement) + + paths.extend([DxfPath([s], error_range) for s in unique_statements]) + + prev_paths_num = 0 + while prev_paths_num != len(paths): + working = [] + for i in range(len(paths)): + mergee = paths[i] + for j in range(i + 1, len(paths)): + target = paths[j] + if target.merge(mergee, error_range): + break + else: + working.append(mergee) + prev_paths_num = len(paths) + paths = working + + closed_path = list(filter(lambda p: p.is_closed, paths)) + open_path = list(filter(lambda p: not p.is_closed, paths)) + return (closed_path, open_path) + +def judge_containment(path1, path2, error_range=0): + from .dxf import DxfArcStatement, DxfLineStatement + + nocontainment = (None, None) + if not path1.may_be_in_collision(path2): + return nocontainment + + def is_in_line_segment(point_from, point_to, point): + dx = point_to[0] - point_from[0] + ratio = (point[0] - point_from[0]) / dx if dx != 0 else \ + (point[1] - point_from[1]) / (point_to[1] - point_from[1]) + return ratio >= 0 and ratio <= 1 + + def contain_in_path(statement, path): + if isinstance(statement, DxfLineStatement): + segment = (statement.start, statement.end) + elif isinstance(statement, DxfArcStatement): + if statement.start == statement.end: + segment = (statement.start, statement.center) + else: + segment = (statement.start, statement.end) + else: + raise Exception('invalid dxf statement type') + pts = path.intersections_with_halfline(segment[0], segment[1], error_range) + if len(pts) % 2 == 0: + return False + for pt in pts: + if is_in_line_segment(segment[0], segment[1], pt): + return False + if isinstance(statement, DxfArcStatement): + pts = path.intersections_with_arc( + statement.center, statement.radius, statement.angle_regions, error_range) + if len(pts) > 0: + return False + return True + + if contain_in_path(path1.statements[0], path2): + containment = [path1, path2] + elif contain_in_path(path2.statements[0], path1): + containment = [path2, path1] + else: + return nocontainment + for i in range(1, len(containment[0].statements)): + if not contain_in_path(containment[0].statements[i], containment[1]): + return nocontainment + return containment diff --git a/gerbonara/gerber/panelize/excellon.py b/gerbonara/gerber/panelize/excellon.py new file mode 100644 index 0000000..ae0b68e --- /dev/null +++ b/gerbonara/gerber/panelize/excellon.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +import operator + +from .. import excellon +from ..excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot +from ..excellon_statements import ExcellonStatement, UnitStmt, CoordinateStmt, UnknownStmt, \ + SlotStmt, DrillModeStmt, RouteModeStmt, LinearModeStmt, \ + ToolSelectionStmt, ZAxisRoutPositionStmt, \ + RetractWithClampingStmt, RetractWithoutClampingStmt, \ + EndOfProgramStmt +from ..cam import FileSettings +from ..utils import inch, metric, write_gerber_value, parse_gerber_value +from .utility import rotate + +def loads(data, filename=None, settings=None, tools=None, format=None): + if not settings: + settings = FileSettings(**detect_excellon_format(data)) + if format: + settings.format = format + excellon.CoordinateStmt = CoordinateStmtEx + excellon.UnitStmt = UnitStmtEx + file = ExcellonParser(settings, tools).parse_raw(data, filename) + return ExcellonFileEx.from_file(file) + +def write_excellon_header(file, settings, tools): + file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % + UnitStmtEx(settings.units, settings.zeros, settings.format).to_excellon(settings)) + for tool in tools: + file.write(tool.to_excellon(settings) + '\n') + file.write('%%\nG90\n%s\n' % ('M72' if settings.units == 'inch' else 'M71')) + +class ExcellonFileEx(ExcellonFile): + @classmethod + def from_file(cls, file): + def correct_statements(): + for stmt in file.statements: + if isinstance(stmt, UnknownStmt): + line = stmt.stmt.strip() + if line[:3] == 'G02': + yield CircularCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) + elif line[:3] == 'G03': + yield CircularCCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) + elif line[0] == 'X' or line[0] == 'Y' or line[0] == 'A' or line[0] == 'I': + yield CoordinateStmtEx.from_excellon(line, file.settings) + else: + yield stmt + else: + yield stmt + + def generate_hits(statements): + class CoordinateCtx: + def __init__(self, notation): + self.notation = notation + self.x = 0. + self.y = 0. + self.radius = None + self.center_offset = None + + def update(self, x=None, y=None, radius=None, center_offset=None): + if self.notation == 'absolute': + if x is not None: + self.x = x + if y is not None: + self.y = y + else: + if x is not None: + self.x += x + if y is not None: + self.y += y + if radius is not None: + self.radius = radius + if center_offset is not None: + self.center_offset = center_offset + + def node(self, mode, center_offset): + radius, offset = None, None + if mode == DrillRout.MODE_CIRCULER_CW or mode == DrillRout.MODE_CIRCULER_CCW: + if center_offset is None: + radius = self.radius + offset = self.center_offset + else: + radius = None + offset = center_offset + return DrillRout.Node(mode, self.x, self.y, radius, offset) + + STAT_DRILL = 0 + STAT_ROUT_UP = 1 + STAT_ROUT_DOWN = 2 + + status = STAT_DRILL + current_tool = None + rout_mode = None + coordinate_ctx = CoordinateCtx(file.notation) + rout_nodes = [] + + last_position = (0., 0.) + last_radius = None + last_center_offset = None + + def make_rout(status, nodes): + if status != STAT_ROUT_DOWN or len(nodes) == 0 or current_tool is None: + return None + return DrillRout(current_tool, nodes) + + for stmt in statements: + if isinstance(stmt, ToolSelectionStmt): + current_tool = file.tools[stmt.tool] + elif isinstance(stmt, DrillModeStmt): + rout = make_rout(status, rout_nodes) + rout_nodes = [] + if rout is not None: + yield rout + status = STAT_DRILL + rout_mode = None + elif isinstance(stmt, RouteModeStmt): + if status == STAT_DRILL: + status = STAT_ROUT_UP + rout_mode = DrillRout.MODE_ROUT + else: + rout_mode = DrillRout.MODE_LINEAR + + elif isinstance(stmt, LinearModeStmt): + rout_mode = DrillRout.MODE_LINEAR + elif isinstance(stmt, CircularCWModeStmt): + rout_mode = DrillRout.MODE_CIRCULER_CW + elif isinstance(stmt, CircularCCWModeStmt): + rout_mode = DrillRout.MODE_CIRCULER_CCW + elif isinstance(stmt, ZAxisRoutPositionStmt) and status == STAT_ROUT_UP: + status = STAT_ROUT_DOWN + elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt): + rout = make_rout(status, rout_nodes) + rout_nodes = [] + if rout is not None: + yield rout + status = STAT_ROUT_UP + elif isinstance(stmt, SlotStmt): + coordinate_ctx.update(stmt.x_start, stmt.y_start) + x_start = coordinate_ctx.x + y_start = coordinate_ctx.y + coordinate_ctx.update(stmt.x_end, stmt.y_end) + x_end = coordinate_ctx.x + y_end = coordinate_ctx.y + yield DrillSlotEx(current_tool, (x_start, y_start), + (x_end, y_end), DrillSlotEx.TYPE_G85) + elif isinstance(stmt, CoordinateStmtEx): + center_offset = (stmt.i, stmt.j) \ + if stmt.i is not None and stmt.j is not None else None + coordinate_ctx.update(stmt.x, stmt.y, stmt.radius, center_offset) + if stmt.x is not None or stmt.y is not None: + if status == STAT_DRILL: + yield DrillHitEx(current_tool, (coordinate_ctx.x, coordinate_ctx.y)) + elif status == STAT_ROUT_UP: + rout_nodes = [coordinate_ctx.node(DrillRout.MODE_ROUT, None)] + elif status == STAT_ROUT_DOWN: + rout_nodes.append(coordinate_ctx.node(rout_mode, center_offset)) + + statements = [s for s in correct_statements()] + hits = [h for h in generate_hits(statements)] + return cls(statements, file.tools, hits, file.settings, file.filename) + + @property + def primitives(self): + return [] + + def __init__(self, statements, tools, hits, settings, filename=None): + super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename) + + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + for hit in self.hits: + hit.rotate(angle, center) + + def to_inch(self): + if self.units == 'metric': + for stmt in self.statements: + stmt.to_inch() + for tool in self.tools: + self.tools[tool].to_inch() + for hit in self.hits: + hit.to_inch() + self.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + for stmt in self.statements: + stmt.to_metric() + for tool in self.tools: + self.tools[tool].to_metric() + for hit in self.hits: + hit.to_metric() + self.units = 'metric' + + def write(self, filename=None): + self.notation = 'absolute' + self.zeros = 'trailing' + filename = filename if filename is not None else self.filename + with open(filename, 'w') as f: + write_excellon_header(f, self.settings, [self.tools[t] for t in self.tools]) + for tool in iter(self.tools.values()): + f.write(ToolSelectionStmt( + tool.number).to_excellon(self.settings) + '\n') + for hit in self.hits: + if hit.tool.number == tool.number: + f.write(hit.to_excellon(self.settings) + '\n') + f.write(EndOfProgramStmt().to_excellon() + '\n') + +class DrillHitEx(DrillHit): + def to_inch(self): + self.position = tuple(map(inch, self.position)) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + + def rotate(self, angle, center=(0, 0)): + self.position = rotate(*self.position, angle, center) + + def to_excellon(self, settings): + return CoordinateStmtEx(*self.position).to_excellon(settings) + +class DrillSlotEx(DrillSlot): + def to_inch(self): + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + + def to_metric(self): + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + + def rotate(self, angle, center=(0,0)): + self.start = rotate(*self.start, angle, center) + self.end = rotate(*self.end, angle, center) + + def to_excellon(self, settings): + return SlotStmt(*self.start, *self.end).to_excellon(settings) + +class DrillRout(object): + MODE_ROUT = 'G00' + MODE_LINEAR = 'G01' + MODE_CIRCULER_CW = 'G02' + MODE_CIRCULER_CCW = 'G03' + + class Node(object): + def __init__(self, mode, x, y, radius=None, center_offset=None): + self.mode = mode + self.position = (x, y) + self.radius = radius + self.center_offset = center_offset + + def to_excellon(self, settings): + center_offset = self.center_offset \ + if self.center_offset is not None else (None, None) + return self.mode + CoordinateStmtEx( + *self.position, self.radius, *center_offset).to_excellon(settings) + + def __init__(self, tool, nodes): + self.tool = tool + self.nodes = nodes + self.nodes[0].mode = self.MODE_ROUT + + def to_excellon(self, settings): + excellon = self.nodes[0].to_excellon(settings) + '\nM15\n' + for node in self.nodes[1:]: + excellon += node.to_excellon(settings) + '\n' + excellon += 'M16\nG05' + return excellon + + def to_inch(self): + for node in self.nodes: + node.position = tuple(map(inch, node.position)) + node.radius = inch( + node.radius) if node.radius is not None else None + if node.center_offset is not None: + node.center_offset = tuple(map(inch, node.center_offset)) + + def to_metric(self): + for node in self.nodes: + node.position = tuple(map(metric, node.position)) + node.radius = metric( + node.radius) if node.radius is not None else None + if node.center_offset is not None: + node.center_offset = tuple(map(metric, node.center_offset)) + + def offset(self, x_offset=0, y_offset=0): + for node in self.nodes: + node.position = tuple(map(operator.add, node.position, (x_offset, y_offset))) + + def rotate(self, angle, center=(0, 0)): + for node in self.nodes: + node.position = rotate(*node.position, angle, center) + if node.center_offset is not None: + node.center_offset = rotate(*node.center_offset, angle, (0., 0.)) + +class UnitStmtEx(UnitStmt): + @classmethod + def from_statement(cls, stmt): + return cls(units=stmt.units, zeros=stmt.zeros, format=stmt.format, id=stmt.id) + + def __init__(self, units='inch', zeros='leading', format=None, **kwargs): + super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs) + + def to_excellon(self, settings=None): + format = settings.format if settings else self.format + stmt = None + if self.units == 'inch' and format == (2, 4): + stmt = 'INCH,%s' % ('LZ' if self.zeros == 'leading' else 'TZ') + else: + stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC', + 'LZ' if self.zeros == 'leading' else 'TZ', + '0' * format[0], '0' * format[1]) + return stmt + +class CircularCWModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CircularCWModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G02' + +class CircularCCWModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CircularCCWModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G02' + +class CoordinateStmtEx(CoordinateStmt): + @classmethod + def from_statement(cls, stmt): + newStmt = cls(x=stmt.x, y=stmt.y) + newStmt.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None + return newStmt + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + stmt = None + if 'A' in line: + parts = line.split('A') + stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) \ + if parts[0] != '' else cls() + stmt.radius = parse_gerber_value( + parts[1], settings.format, settings.zero_suppression) + elif 'I' in line: + jparts = line.split('J') + iparts = jparts[0].split('I') + stmt = cls.from_statement(CoordinateStmt.from_excellon(iparts[0], settings)) \ + if iparts[0] != '' else cls() + stmt.i = parse_gerber_value( + iparts[1], settings.format, settings.zero_suppression) + stmt.j = parse_gerber_value( + jparts[1], settings.format, settings.zero_suppression) + else: + stmt = cls.from_statement(CoordinateStmt.from_excellon(line, settings)) + + return stmt + + def __init__(self, x=None, y=None, radius=None, i=None, j=None, **kwargs): + super(CoordinateStmtEx, self).__init__(x, y, **kwargs) + self.radius = radius + self.i = i + self.j = j + + def to_excellon(self, settings): + stmt = '' + if self.x is not None: + stmt += 'X%s' % write_gerber_value(self.x, settings.format, + settings.zero_suppression) + if self.y is not None: + stmt += 'Y%s' % write_gerber_value(self.y, settings.format, + settings.zero_suppression) + if self.radius is not None: + stmt += 'A%s' % write_gerber_value(self.radius, settings.format, + settings.zero_suppression) + elif self.i is not None and self.j is not None: + stmt += 'I%sJ%s' % (write_gerber_value(self.i, settings.format, + settings.zero_suppression), + write_gerber_value(self.j, settings.format, + settings.zero_suppression)) + return stmt + + def __str__(self): + coord_str = '' + if self.x is not None: + coord_str += 'X: %g ' % self.x + if self.y is not None: + coord_str += 'Y: %g ' % self.y + if self.radius is not None: + coord_str += 'A: %g ' % self.radius + if self.i is not None: + coord_str += 'I: %g ' % self.i + if self.j is not None: + coord_str += 'J: %g ' % self.j + + return '' % (coord_str) diff --git a/gerbonara/gerber/panelize/gerber_statements.py b/gerbonara/gerber/panelize/gerber_statements.py new file mode 100644 index 0000000..875d656 --- /dev/null +++ b/gerbonara/gerber/panelize/gerber_statements.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..gerber_statements import AMParamStmt, ADParamStmt +from ..utils import inch, metric +from .am_primitive import to_primitive_defs + +class AMParamStmtEx(AMParamStmt): + @classmethod + def from_stmt(cls, stmt): + return cls(stmt.param, stmt.name, stmt.macro, stmt.units) + + @classmethod + def circle(cls, name, units): + return cls('AM', name, '1,1,$1,0,0,0*1,0,$2,0,0,0', units) + + @classmethod + def rectangle(cls, name, units): + return cls('AM', name, '21,1,$1,$2,0,0,0*1,0,$3,0,0,0', units) + + @classmethod + def landscape_obround(cls, name, units): + return cls( + 'AM', name, + '$4=$1-$2*' + '$5=$1-$4*' + '21,1,$5,$2,0,0,0*' + '1,1,$4,$4/2,0,0*' + '1,1,$4,-$4/2,0,0*' + '1,0,$3,0,0,0', units) + + @classmethod + def portrate_obround(cls, name, units): + return cls( + 'AM', name, + '$4=$2-$1*' + '$5=$2-$4*' + '21,1,$1,$5,0,0,0*' + '1,1,$4,0,$4/2,0*' + '1,1,$4,0,-$4/2,0*' + '1,0,$3,0,0,0', units) + + @classmethod + def polygon(cls, name, units): + return cls('AM', name, '5,1,$2,0,0,$1,$3*1,0,$4,0,0,0', units) + + def __init__(self, param, name, macro, units): + super(AMParamStmtEx, self).__init__(param, name, macro) + self.units = units + self.primitive_defs = list(to_primitive_defs(self.instructions)) + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + for p in self.primitive_defs: + p.to_inch() + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + for p in self.primitive_defs: + p.to_metric() + + def to_gerber(self, settings = None): + def plist(): + for p in self.primitive_defs: + yield p.to_gerber(settings) + return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist())) + + def rotate(self, angle, center=None): + for primitive_def in self.primitive_defs: + primitive_def.rotate(angle, center) + +class ADParamStmtEx(ADParamStmt): + GEOMETRIES = { + 'C': [0,1], + 'R': [0,1,2], + 'O': [0,1,2], + 'P': [0,3], + } + + @classmethod + def from_stmt(cls, stmt): + modstr = ','.join([ + 'X'.join(['{0}'.format(x) for x in modifier]) + for modifier in stmt.modifiers]) + return cls(stmt.param, stmt.d, stmt.shape, modstr, stmt.units) + + def __init__(self, param, d, shape, modifiers, units): + super(ADParamStmtEx, self).__init__(param, d, shape, modifiers) + self.units = units + + def to_inch(self): + if self.units == 'inch': + return + self.units = 'inch' + if self.shape in self.GEOMETRIES: + indices = self.GEOMETRIES[self.shape] + self.modifiers = [tuple([ + inch(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \ + for i in range(len(self.modifiers[0])) + ])] + + def to_metric(self): + if self.units == 'metric': + return + self.units = 'metric' + if self.shape in self.GEOMETRIES: + indices = self.GEOMETRIES[self.shape] + self.modifiers = [tuple([ + metric(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \ + for i in range(len(self.modifiers[0])) + ])] diff --git a/gerbonara/gerber/panelize/rs274x.py b/gerbonara/gerber/panelize/rs274x.py new file mode 100644 index 0000000..2f44cd4 --- /dev/null +++ b/gerbonara/gerber/panelize/rs274x.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from ..cam import FileSettings +from .. import rs274x +from ..gerber_statements import * +from .gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx +from .utility import rotate +import re + +def loads(data, filename=None): + cls = rs274x.GerberParser + cls.SF = \ + r"(?PSF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=cls.DECIMAL) + cls.PARAMS = (cls.FS, cls.MO, cls.LP, cls.AD_CIRCLE, + cls.AD_RECT, cls.AD_OBROUND, cls.AD_POLY, + cls.AD_MACRO, cls.AM, cls.AS, cls.IF, cls.IN, + cls.IP, cls.IR, cls.MI, cls.OF, cls.SF, cls.LN) + cls.PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in cls.PARAMS] + return cls().parse_raw(data, filename) + +def write_gerber_header(file, settings): + file.write('%s\n%s\n%%IPPOS*%%\n' % ( + MOParamStmt('MO', settings.units).to_gerber(settings), + FSParamStmt('FS', settings.zero_suppression, + settings.notation, settings.format).to_gerber(settings))) + +class GerberFile(rs274x.GerberFile): + @classmethod + def from_gerber_file(cls, gerber_file): + if not isinstance(gerber_file, rs274x.GerberFile): + raise Exception('only gerbonara.gerber.rs274x.GerberFile object is specified') + + return cls(gerber_file.statements, gerber_file.settings, gerber_file.primitives,\ + gerber_file.apertures, gerber_file.filename) + + def __init__(self, statements, settings, primitives, apertures, filename=None): + super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename) + self.context = GerberContext.from_settings(self.settings) + self.aperture_macros = {} + self.aperture_defs = [] + self.main_statements = [] + for stmt in self.statements: + type, stmts = self.context.normalize_statement(stmt) + if type == self.context.TYPE_AM: + for mdef in stmts: + self.aperture_macros[mdef.name] = mdef + elif type == self.context.TYPE_AD: + self.aperture_defs.extend(stmts) + elif type == self.context.TYPE_MAIN: + self.main_statements.extend(stmts) + if self.context.angle != 0: + self.rotate(self.context.angle) + if self.context.is_negative: + self.nagate_polarity() + self.context.notation = 'absolute' + self.context.zeros = 'trailing' + + def write(self, filename=None): + self.context.notation = 'absolute' + self.context.zeros = 'trailing' + self.context.format = self.format + self.units = self.units + filename=filename if filename is not None else self.filename + with open(filename, 'w') as f: + write_gerber_header(f, self.context) + for macro in self.aperture_macros: + f.write(self.aperture_macros[macro].to_gerber(self.context) + '\n') + for aperture in self.aperture_defs: + f.write(aperture.to_gerber(self.context) + '\n') + for statement in self.main_statements: + f.write(statement.to_gerber(self.context) + '\n') + f.write('M02*\n') + + def to_inch(self): + if self.units == 'metric': + for macro in self.aperture_macros: + self.aperture_macros[macro].to_inch() + for aperture in self.aperture_defs: + aperture.to_inch() + for statement in self.statements: + statement.to_inch() + self.units = 'inch' + self.context.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + for macro in self.aperture_macros: + self.aperture_macros[macro].to_metric() + for aperture in self.aperture_defs: + aperture.to_metric() + for statement in self.statements: + statement.to_metric() + self.units='metric' + self.context.units='metric' + + def offset(self, x_offset=0, y_offset=0): + for statement in self.main_statements: + if isinstance(statement, CoordStmt): + if statement.x is not None: + statement.x += x_offset + if statement.y is not None: + statement.y += y_offset + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) + + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + self._generalize_aperture() + last_x = 0 + last_y = 0 + last_rx = 0 + last_ry = 0 + for name in self.aperture_macros: + self.aperture_macros[name].rotate(angle, center) + for statement in self.main_statements: + if isinstance(statement, CoordStmt) and statement.x != None and statement.y != None: + if statement.i != None and statement.j != None: + cx = last_x + statement.i + cy = last_y + statement.j + cx, cy = rotate(cx, cy, angle, center) + statement.i = cx - last_rx + statement.j = cy - last_ry + last_x = statement.x + last_y = statement.y + last_rx, last_ry = rotate(statement.x, statement.y, angle, center) + statement.x = last_rx + statement.y = last_ry + + def nagate_polarity(self): + for statement in self.main_statements: + if isinstance(statement, LPParamStmt): + statement.lp = 'dark' if statement.lp == 'clear' else 'clear' + + def _generalize_aperture(self): + RECTANGLE = 0 + LANDSCAPE_OBROUND = 1 + PORTRATE_OBROUND = 2 + POLYGON = 3 + macro_defs = [ + ('MACR', AMParamStmtEx.rectangle), + ('MACLO', AMParamStmtEx.landscape_obround), + ('MACPO', AMParamStmtEx.portrate_obround), + ('MACP', AMParamStmtEx.polygon) + ] + + need_to_change = False + for statement in self.aperture_defs: + if isinstance(statement, ADParamStmt) and statement.shape in ['R', 'O', 'P']: + need_to_change = True + + if need_to_change: + for idx in range(0, len(macro_defs)): + macro_def = macro_defs[idx] + name = macro_def[0] + num = 1 + while name in self.aperture_macros: + name = '%s_%d' % (macro_def[0], num) + num += 1 + self.aperture_macros[name] = macro_def[1](name, self.units) + macro_defs[idx] = (name, macro_def[1]) + for statement in self.aperture_defs: + if isinstance(statement, ADParamStmt): + if statement.shape == 'R': + statement.shape = macro_defs[RECTANGLE][0] + elif statement.shape == 'O': + x = statement.modifiers[0][0] \ + if len(statement.modifiers[0]) > 0 else 0 + y = statement.modifiers[0][1] \ + if len(statement.modifiers[0]) > 1 else 0 + statement.shape = macro_defs[LANDSCAPE_OBROUND][0] \ + if x > y else macro_defs[PORTRATE_OBROUND][0] + elif statement.shape == 'P': + statement.shape = macro_defs[POLYGON][0] + +class GerberContext(FileSettings): + TYPE_NONE = 'none' + TYPE_AM = 'am' + TYPE_AD = 'ad' + TYPE_MAIN = 'main' + IP_LINEAR = 'lenear' + IP_ARC = 'arc' + DIR_CLOCKWISE = 'cw' + DIR_COUNTERCLOCKWISE = 'ccw' + + ignored_stmt = ('FSParamStmt', 'MOParamStmt', 'ASParamStmt', + 'INParamStmt', 'IPParamStmt', 'IRParamStmt', + 'MIParamStmt', 'OFParamStmt', 'SFParamStmt', + 'LNParamStmt', 'CommentStmt', 'EofStmt',) + + @classmethod + def from_settings(cls, settings): + return cls(settings.notation, settings.units, settings.zero_suppression, + settings.format, settings.zeros, settings.angle_units) + + def __init__(self, notation='absolute', units='inch', + zero_suppression=None, format=(2, 5), zeros=None, + angle_units='degrees', + name=None, + mirror=(False, False), offset=(0., 0.), scale=(1., 1.), + angle=0., axis='xy'): + super(GerberContext, self).__init__(notation, units, zero_suppression, + format, zeros, angle_units) + self.name = name + self.mirror = mirror + self.offset = offset + self.scale = scale + self.angle = angle + self.axis = axis + + self.matrix = (1, 0, + 1, 0, + 1, 1) + + self.is_negative = False + self.is_first_coordinate = True + self.no_polarity = True + self.in_single_quadrant_mode = False + self.op = None + self.interpolation = self.IP_LINEAR + self.direction = self.DIR_CLOCKWISE + self.x = 0. + self.y = 0. + + def normalize_statement(self, stmt): + additional_stmts = None + if isinstance(stmt, INParamStmt): + self.name = stmt.name + elif isinstance(stmt, MIParamStmt): + self.mirror = (stmt.a, stmt.b) + self._update_matrix() + elif isinstance(stmt, OFParamStmt): + self.offset = (stmt.a, stmt.b) + self._update_matrix() + elif isinstance(stmt, SFParamStmt): + self.scale = (stmt.a, stmt.b) + self._update_matrix() + elif isinstance(stmt, ASParamStmt): + self.axis = 'yx' if stmt.mode == 'AYBX' else 'xy' + self._update_matrix() + elif isinstance(stmt, IRParamStmt): + self.angle = stmt.angle + elif isinstance(stmt, AMParamStmt) and not isinstance(stmt, AMParamStmtEx): + stmt = AMParamStmtEx.from_stmt(stmt) + return (self.TYPE_AM, [stmt]) + elif isinstance(stmt, ADParamStmt) and not isinstance(stmt, AMParamStmtEx): + stmt = ADParamStmtEx.from_stmt(stmt) + return (self.TYPE_AD, [stmt]) + elif isinstance(stmt, QuadrantModeStmt): + self.in_single_quadrant_mode = stmt.mode == 'single-quadrant' + stmt.mode = 'multi-quadrant' + elif isinstance(stmt, IPParamStmt): + self.is_negative = stmt.ip == 'negative' + elif isinstance(stmt, LPParamStmt): + self.no_polarity = False + elif isinstance(stmt, CoordStmt): + self._normalize_coordinate(stmt) + if self.is_first_coordinate: + self.is_first_coordinate = False + if self.no_polarity: + additional_stmts = [LPParamStmt('LP', 'dark'), stmt] + + if type(stmt).__name__ in self.ignored_stmt: + return (self.TYPE_NONE, None) + elif additional_stmts is not None: + return (self.TYPE_MAIN, additional_stmts) + else: + return (self.TYPE_MAIN, [stmt]) + + def _update_matrix(self): + if self.axis == 'xy': + mx = -1 if self.mirror[0] else 1 + my = -1 if self.mirror[1] else 1 + self.matrix = ( + self.scale[0] * mx, self.offset[0], + self.scale[1] * my, self.offset[1], + self.scale[0] * mx, self.scale[1] * my) + else: + mx = -1 if self.mirror[1] else 1 + my = -1 if self.mirror[0] else 1 + self.matrix = ( + self.scale[1] * mx, self.offset[1], + self.scale[0] * my, self.offset[0], + self.scale[1] * mx, self.scale[0] * my) + + def _normalize_coordinate(self, stmt): + if stmt.function == 'G01' or stmt.function == 'G1': + self.interpolation = self.IP_LINEAR + elif stmt.function == 'G02' or stmt.function == 'G2': + self.interpolation = self.IP_ARC + self.direction = self.DIR_CLOCKWISE + if self.mirror[0] != self.mirror[1]: + stmt.function = 'G03' + elif stmt.function == 'G03' or stmt.function == 'G3': + self.interpolation = self.IP_ARC + self.direction = self.DIR_COUNTERCLOCKWISE + if self.mirror[0] != self.mirror[1]: + stmt.function = 'G02' + if stmt.only_function: + return + + last_x = self.x + last_y = self.y + if self.notation == 'absolute': + x = stmt.x if stmt.x is not None else self.x + y = stmt.y if stmt.y is not None else self.y + else: + x = self.x + stmt.x if stmt.x is not None else 0 + y = self.y + stmt.y if stmt.y is not None else 0 + self.x, self.y = x, y + self.op = stmt.op if stmt.op is not None else self.op + + stmt.op = self.op + stmt.x = self.matrix[0] * x + self.matrix[1] + stmt.y = self.matrix[2] * y + self.matrix[3] + if stmt.op == 'D01' and self.interpolation == self.IP_ARC: + qx, qy = 1, 1 + if self.in_single_quadrant_mode: + if self.direction == self.DIR_CLOCKWISE: + qx = 1 if y > last_y else -1 + qy = 1 if x < last_x else -1 + else: + qx = 1 if y < last_y else -1 + qy = 1 if x > last_x else -1 + if last_x == x and last_y == y: + qx, qy = 0, 0 + stmt.i = qx * self.matrix[4] * stmt.i if stmt.i is not None else 0 + stmt.j = qy * self.matrix[5] * stmt.j if stmt.j is not None else 0 diff --git a/gerbonara/gerber/panelize/utility.py b/gerbonara/gerber/panelize/utility.py new file mode 100644 index 0000000..37de5e8 --- /dev/null +++ b/gerbonara/gerber/panelize/utility.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from math import cos, sin, pi, sqrt + +def rotate(x, y, angle, center): + x0 = x - center[0] + y0 = y - center[1] + angle = angle * pi / 180.0 + return (cos(angle) * x0 - sin(angle) * y0 + center[0], + sin(angle) * x0 + cos(angle) * y0 + center[1]) + +def is_equal_value(a, b, error_range=0): + return (a - b) * (a - b) <= error_range * error_range + +def is_equal_point(a, b, error_range=0): + return is_equal_value(a[0], b[0], error_range) and \ + is_equal_value(a[1], b[1], error_range) + +def normalize_vec2d(vec): + length = sqrt(vec[0] * vec[0] + vec[1] * vec[1]) + return (vec[0] / length, vec[1] / length) + +def dot_vec2d(vec1, vec2): + return vec1[0] * vec2[0] + vec1[1] * vec2[1] \ No newline at end of file diff --git a/gerbonara/gerber/pcb.py b/gerbonara/gerber/pcb.py new file mode 100644 index 0000000..1d22e74 --- /dev/null +++ b/gerbonara/gerber/pcb.py @@ -0,0 +1,124 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2015 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 os +from .exceptions import ParseError +from .layers import PCBLayer, sort_layers, layer_signatures +from .common import read as gerber_read +from .utils import listdir + + +class PCB(object): + + @classmethod + def from_directory(cls, directory, board_name=None, verbose=False): + layers = [] + names = set() + + # Validate + directory = os.path.abspath(directory) + if not os.path.isdir(directory): + raise TypeError('{} is not a directory.'.format(directory)) + + # Load gerber files + for filename in listdir(directory, True, True): + try: + camfile = gerber_read(os.path.join(directory, filename)) + layer = PCBLayer.from_cam(camfile) + layers.append(layer) + name = os.path.splitext(filename)[0] + if len(os.path.splitext(filename)) > 1: + _name, ext = os.path.splitext(name) + if ext[1:] in layer_signatures(layer.layer_class): + name = _name + if layer.layer_class == 'drill' and 'drill' in ext: + name = _name + names.add(name) + if verbose: + print('[PCB]: Added {} layer <{}>'.format(layer.layer_class, + filename)) + except ParseError: + if verbose: + print('[PCB]: Skipping file {}'.format(filename)) + except IOError: + if verbose: + print('[PCB]: Skipping file {}'.format(filename)) + + # Try to guess board name + if board_name is None: + if len(names) == 1: + board_name = names.pop() + else: + board_name = os.path.basename(directory) + # Return PCB + return cls(layers, board_name) + + def __init__(self, layers, name=None): + self.layers = sort_layers(layers) + self.name = name + + def __len__(self): + return len(self.layers) + + @property + def top_layers(self): + board_layers = [l for l in reversed(self.layers) if l.layer_class in + ('topsilk', 'topmask', 'top')] + drill_layers = [l for l in self.drill_layers if 'top' in l.layers] + # Drill layer goes under soldermask for proper rendering of tented vias + return [board_layers[0]] + drill_layers + board_layers[1:] + + @property + def bottom_layers(self): + board_layers = [l for l in self.layers if l.layer_class in + ('bottomsilk', 'bottommask', 'bottom')] + drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers] + # Drill layer goes under soldermask for proper rendering of tented vias + return [board_layers[0]] + drill_layers + board_layers[1:] + + @property + def drill_layers(self): + return [l for l in self.layers if l.layer_class == 'drill'] + + @property + def copper_layers(self): + return list(reversed([layer for layer in self.layers if + layer.layer_class in + ('top', 'bottom', 'internal')])) + + @property + def outline_layer(self): + for layer in self.layers: + if layer.layer_class == 'outline': + return layer + + @property + def layer_count(self): + """ Number of *COPPER* layers + """ + return len([l for l in self.layers if l.layer_class in + ('top', 'bottom', 'internal')]) + + @property + def board_bounds(self): + for layer in self.layers: + if layer.layer_class == 'outline': + return layer.bounds + for layer in self.layers: + if layer.layer_class == 'top': + return layer.bounds diff --git a/gerbonara/gerber/primitives.py b/gerbonara/gerber/primitives.py new file mode 100644 index 0000000..757f117 --- /dev/null +++ b/gerbonara/gerber/primitives.py @@ -0,0 +1,1697 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2016 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 add +from itertools import combinations +from .utils import validate_coordinates, inch, metric, convex_hull +from .utils import rotate_point, nearly_equal + + + + +class Primitive(object): + """ Base class for all Cam file primitives + + Parameters + --------- + level_polarity : string + Polarity of the parameter. May be 'dark' or 'clear'. Dark indicates + a "positive" primitive, i.e. indicating where coppper should remain, + and clear indicates a negative primitive, such as where copper should + be removed. clear primitives are often used to create cutouts in region + pours. + + rotation : float + Rotation of a primitive about its origin in degrees. Positive rotation + is counter-clockwise as viewed from the board top. + + units : string + Units in which primitive was defined. 'inch' or 'metric' + + net_name : string + Name of the electrical net the primitive belongs to + """ + + def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None): + self.level_polarity = level_polarity + self.net_name = net_name + self._to_convert = list() + self._memoized = list() + self._units = units + self._rotation = rotation + self._cos_theta = math.cos(math.radians(rotation)) + self._sin_theta = math.sin(math.radians(rotation)) + self._bounding_box = None + self._vertices = None + self._segments = None + + @property + def flashed(self): + '''Is this a flashed primitive''' + raise NotImplementedError('Is flashed must be ' + 'implemented in subclass') + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @property + def units(self): + return self._units + + @units.setter + def units(self, value): + self._changed() + self._units = value + + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, value): + self._changed() + self._rotation = value + self._cos_theta = math.cos(math.radians(value)) + self._sin_theta = math.sin(math.radians(value)) + + @property + def vertices(self): + return None + + @property + def segments(self): + if self._segments is None: + if self.vertices is not None and len(self.vertices): + self._segments = [segment for segment in + combinations(self.vertices, 2)] + return self._segments + + @property + def bounding_box(self): + """ Calculate axis-aligned bounding box + + will be helpful for sweep & prune during DRC clearance checks. + + Return ((min x, max x), (min y, max y)) + """ + raise NotImplementedError('Bounding box calculation must be ' + 'implemented in subclass') + + @property + def bounding_box_no_aperture(self): + """ Calculate bouxing box without considering the aperture + + for most objects, this is the same as the bounding_box, but is different for + Lines and Arcs (which are not flashed) + + Return ((min x, max x), (min y, max y)) + """ + return self.bounding_box + + def to_inch(self): + """ Convert primitive units to inches. + """ + if self.units == 'metric': + self.units = 'inch' + for attr, value in [(attr, getattr(self, attr)) + for attr in self._to_convert]: + if hasattr(value, 'to_inch'): + value.to_inch() + else: + try: + if len(value) > 1: + if hasattr(value[0], 'to_inch'): + for v in value: + v.to_inch() + elif isinstance(value[0], tuple): + setattr(self, attr, + [tuple(map(inch, point)) + for point in value]) + else: + setattr(self, attr, tuple(map(inch, value))) + except: + if value is not None: + setattr(self, attr, inch(value)) + + def to_metric(self): + """ Convert primitive units to metric. + """ + if self.units == 'inch': + self.units = 'metric' + for attr, value in [(attr, getattr(self, attr)) + for attr in self._to_convert]: + if hasattr(value, 'to_metric'): + value.to_metric() + else: + try: + if len(value) > 1: + if hasattr(value[0], 'to_metric'): + for v in value: + v.to_metric() + elif isinstance(value[0], tuple): + setattr(self, attr, + [tuple(map(metric, point)) + for point in value]) + else: + setattr(self, attr, tuple(map(metric, value))) + except: + if value is not None: + setattr(self, attr, metric(value)) + + def offset(self, x_offset=0, y_offset=0): + """ Move the primitive by the specified x and y offset amount. + + values are specified in the primitive's native units + """ + if hasattr(self, 'position'): + self._changed() + self.position = tuple([coord + offset for coord, offset + in zip(self.position, + (x_offset, y_offset))]) + + def to_statement(self): + pass + + def _changed(self): + """ Clear memoized properties. + + Forces a recalculation next time any memoized propery is queried. + This must be called from a subclass every time a parameter that affects + a memoized property is changed. The easiest way to do this is to call + _changed() from property.setter methods. + """ + self._bounding_box = None + self._vertices = None + self._segments = None + for attr in self._memoized: + setattr(self, attr, None) + +class Line(Primitive): + """ + """ + + def __init__(self, start, end, aperture, level_polarity=None, **kwargs): + super(Line, self).__init__(**kwargs) + self.level_polarity = level_polarity + self._start = start + self._end = end + self.aperture = aperture + self._to_convert = ['start', 'end', 'aperture'] + + @property + def flashed(self): + return False + + @property + def start(self): + return self._start + + @start.setter + def start(self, value): + self._changed() + self._start = value + + @property + def end(self): + return self._end + + @end.setter + def end(self, value): + self._changed() + self._end = value + + @property + def angle(self): + delta_x, delta_y = tuple( + [end - start for end, start in zip(self.end, self.start)]) + angle = math.atan2(delta_y, delta_x) + return angle + + @property + def bounding_box(self): + if self._bounding_box is None: + if isinstance(self.aperture, Circle): + width_2 = self.aperture.radius + height_2 = width_2 + else: + width_2 = self.aperture.width / 2. + height_2 = self.aperture.height / 2. + min_x = min(self.start[0], self.end[0]) - width_2 + max_x = max(self.start[0], self.end[0]) + width_2 + min_y = min(self.start[1], self.end[1]) - height_2 + max_y = max(self.start[1], self.end[1]) + height_2 + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + @property + def bounding_box_no_aperture(self): + '''Gets the bounding box without the aperture''' + min_x = min(self.start[0], self.end[0]) + max_x = max(self.start[0], self.end[0]) + min_y = min(self.start[1], self.end[1]) + max_y = max(self.start[1], self.end[1]) + return ((min_x, max_x), (min_y, max_y)) + + @property + def vertices(self): + if self._vertices is None: + start = self.start + end = self.end + if isinstance(self.aperture, Rectangle): + width = self.aperture.width + height = self.aperture.height + + # Find all the corners of the start and end position + start_ll = (start[0] - (width / 2.), start[1] - (height / 2.)) + start_lr = (start[0] + (width / 2.), start[1] - (height / 2.)) + start_ul = (start[0] - (width / 2.), start[1] + (height / 2.)) + start_ur = (start[0] + (width / 2.), start[1] + (height / 2.)) + end_ll = (end[0] - (width / 2.), end[1] - (height / 2.)) + end_lr = (end[0] + (width / 2.), end[1] - (height / 2.)) + end_ul = (end[0] - (width / 2.), end[1] + (height / 2.)) + end_ur = (end[0] + (width / 2.), end[1] + (height / 2.)) + + # The line is defined by the convex hull of the points + self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur)) + elif isinstance(self.aperture, Polygon): + points = [map(add, point, vertex) + for vertex in self.aperture.vertices + for point in (start, end)] + self._vertices = convex_hull(points) + return self._vertices + + def offset(self, x_offset=0, y_offset=0): + self._changed() + self.start = tuple([coord + offset for coord, offset + in zip(self.start, (x_offset, y_offset))]) + self.end = tuple([coord + offset for coord, offset + in zip(self.end, (x_offset, y_offset))]) + + def equivalent(self, other, offset): + + if not isinstance(other, Line): + return False + + equiv_start = tuple(map(add, other.start, offset)) + equiv_end = tuple(map(add, other.end, offset)) + + + return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end) + + def __str__(self): + return "".format(self.start, self.end) + + def __repr__(self): + return str(self) + +class Arc(Primitive): + """ + """ + + def __init__(self, start, end, center, direction, aperture, quadrant_mode, + level_polarity=None, **kwargs): + super(Arc, self).__init__(**kwargs) + self.level_polarity = level_polarity + self._start = start + self._end = end + self._center = center + self.direction = direction + self.aperture = aperture + self._quadrant_mode = quadrant_mode + self._to_convert = ['start', 'end', 'center', 'aperture'] + + @property + def flashed(self): + return False + + @property + def start(self): + return self._start + + @start.setter + def start(self, value): + self._changed() + self._start = value + + @property + def end(self): + return self._end + + @end.setter + def end(self, value): + self._changed() + self._end = value + + @property + def center(self): + return self._center + + @center.setter + def center(self, value): + self._changed() + self._center = value + + @property + def quadrant_mode(self): + return self._quadrant_mode + + @quadrant_mode.setter + def quadrant_mode(self, quadrant_mode): + self._changed() + self._quadrant_mode = quadrant_mode + + @property + def radius(self): + dy, dx = tuple([start - center for start, center + in zip(self.start, self.center)]) + return math.sqrt(dy ** 2 + dx ** 2) + + @property + def start_angle(self): + dx, dy = tuple([start - center for start, center + in zip(self.start, self.center)]) + return math.atan2(dy, dx) + + @property + def end_angle(self): + dx, dy = tuple([end - center for end, center + in zip(self.end, self.center)]) + return math.atan2(dy, dx) + + @property + def sweep_angle(self): + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + if self.direction == 'counterclockwise': + return abs(theta1 - theta0) + else: + theta0 += two_pi + return abs(theta0 - theta1) % two_pi + + @property + def bounding_box(self): + if self._bounding_box is None: + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + points = [self.start, self.end] + if self.quadrant_mode == 'multi-quadrant': + if self.direction == 'counterclockwise': + # Passes through 0 degrees + if theta0 >= theta1: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if (((theta0 <= math.pi / 2.) and ((theta1 >= math.pi / 2.) or (theta1 <= theta0))) + or ((theta1 > math.pi / 2.) and (theta1 <= theta0))): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0)) + or ((theta1 > math.pi) and (theta1 <= theta0))): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if (theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 <= theta0) + or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))): + points.append((self.center[0], self.center[1] - self.radius)) + else: + # Passes through 0 degrees + if theta1 >= theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if (((theta1 <= math.pi / 2.) and (theta0 >= math.pi / 2. or theta0 <= theta1)) + or ((theta0 > math.pi / 2.) and (theta0 <= theta1))): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1)) + or ((theta0 > math.pi) and (theta0 <= theta1))): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if (((theta1 <= math.pi * 1.5) and (theta0 >= math.pi * 1.5 or theta0 <= theta1)) + or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))): + points.append((self.center[0], self.center[1] - self.radius)) + x, y = zip(*points) + if hasattr(self.aperture, 'radius'): + min_x = min(x) - self.aperture.radius + max_x = max(x) + self.aperture.radius + min_y = min(y) - self.aperture.radius + max_y = max(y) + self.aperture.radius + else: + min_x = min(x) - self.aperture.width + max_x = max(x) + self.aperture.width + min_y = min(y) - self.aperture.height + max_y = max(y) + self.aperture.height + + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + @property + def bounding_box_no_aperture(self): + '''Gets the bounding box without considering the aperture''' + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + points = [self.start, self.end] + if self.quadrant_mode == 'multi-quadrant': + if self.direction == 'counterclockwise': + # Passes through 0 degrees + if theta0 >= theta1: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if (((theta0 <= math.pi / 2.) and ( + (theta1 >= math.pi / 2.) or (theta1 <= theta0))) + or ((theta1 > math.pi / 2.) and (theta1 <= theta0))): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0)) + or ((theta1 > math.pi) and (theta1 <= theta0))): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if (theta0 <= math.pi * 1.5 and ( + theta1 >= math.pi * 1.5 or theta1 <= theta0) + or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))): + points.append((self.center[0], self.center[1] - self.radius)) + else: + # Passes through 0 degrees + if theta1 >= theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if (((theta1 <= math.pi / 2.) and ( + theta0 >= math.pi / 2. or theta0 <= theta1)) + or ((theta0 > math.pi / 2.) and (theta0 <= theta1))): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1)) + or ((theta0 > math.pi) and (theta0 <= theta1))): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if (((theta1 <= math.pi * 1.5) and ( + theta0 >= math.pi * 1.5 or theta0 <= theta1)) + or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))): + points.append((self.center[0], self.center[1] - self.radius)) + x, y = zip(*points) + + min_x = min(x) + max_x = max(x) + min_y = min(y) + max_y = max(y) + return ((min_x, max_x), (min_y, max_y)) + + def offset(self, x_offset=0, y_offset=0): + self._changed() + self.start = tuple(map(add, self.start, (x_offset, y_offset))) + self.end = tuple(map(add, self.end, (x_offset, y_offset))) + self.center = tuple(map(add, self.center, (x_offset, y_offset))) + + +class Circle(Primitive): + """ + """ + + def __init__(self, position, diameter, hole_diameter=None, + hole_width=0, hole_height=0, **kwargs): + super(Circle, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self._diameter = diameter + self.hole_diameter = hole_diameter + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'diameter', 'hole_diameter', 'hole_width', 'hole_height'] + + @property + def flashed(self): + return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def diameter(self): + return self._diameter + + @diameter.setter + def diameter(self, value): + self._changed() + self._diameter = value + + @property + def radius(self): + return self.diameter / 2. + + @property + def hole_radius(self): + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + + def equivalent(self, other, offset): + '''Is this the same as the other circle, ignoring the offiset?''' + + if not isinstance(other, Circle): + return False + + if self.diameter != other.diameter or self.hole_diameter != other.hole_diameter: + return False + + equiv_position = tuple(map(add, other.position, offset)) + + return nearly_equal(self.position, equiv_position) + + +class Ellipse(Primitive): + """ + """ + def __init__(self, position, width, height, **kwargs): + super(Ellipse, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self._width = width + self._height = height + self._to_convert = ['position', 'width', 'height'] + + @property + def flashed(self): + return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - (self.axis_aligned_width / 2.0) + max_x = self.position[0] + (self.axis_aligned_width / 2.0) + min_y = self.position[1] - (self.axis_aligned_height / 2.0) + max_y = self.position[1] + (self.axis_aligned_height / 2.0) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + @property + def axis_aligned_width(self): + ux = (self.width / 2.) * math.cos(math.radians(self.rotation)) + vx = (self.height / 2.) * \ + math.cos(math.radians(self.rotation) + (math.pi / 2.)) + return 2 * math.sqrt((ux * ux) + (vx * vx)) + + @property + def axis_aligned_height(self): + uy = (self.width / 2.) * math.sin(math.radians(self.rotation)) + vy = (self.height / 2.) * \ + math.sin(math.radians(self.rotation) + (math.pi / 2.)) + return 2 * math.sqrt((uy * uy) + (vy * vy)) + + +class Rectangle(Primitive): + """ + When rotated, the rotation is about the center point. + + Only aperture macro generated Rectangle objects can be rotated. If you aren't in a AMGroup, + then you don't need to worry about rotation + """ + + def __init__(self, position, width, height, hole_diameter=0, + hole_width=0, hole_height=0, **kwargs): + super(Rectangle, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self._width = width + self._height = height + self.hole_diameter = hole_diameter + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'width', 'height', 'hole_diameter', + 'hole_width', 'hole_height'] + # TODO These are probably wrong when rotated + self._lower_left = None + self._upper_right = None + + @property + def flashed(self): + return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def hole_radius(self): + """The radius of the hole. If there is no hole, returns None""" + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None + + @property + def upper_right(self): + return (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + + @property + def lower_left(self): + return (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + @property + def vertices(self): + if self._vertices is None: + delta_w = self.width / 2. + delta_h = self.height / 2. + ll = ((self.position[0] - delta_w), (self.position[1] - delta_h)) + ul = ((self.position[0] - delta_w), (self.position[1] + delta_h)) + ur = ((self.position[0] + delta_w), (self.position[1] + delta_h)) + lr = ((self.position[0] + delta_w), (self.position[1] - delta_h)) + self._vertices = [((x * self._cos_theta - y * self._sin_theta), + (x * self._sin_theta + y * self._cos_theta)) + for x, y in [ll, ul, ur, lr]] + return self._vertices + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + self._sin_theta * self.height) + + @property + def axis_aligned_height(self): + return (self._cos_theta * self.height + self._sin_theta * self.width) + + def equivalent(self, other, offset): + """Is this the same as the other rect, ignoring the offset?""" + + if not isinstance(other, Rectangle): + return False + + if self.width != other.width or self.height != other.height or self.rotation != other.rotation or self.hole_diameter != other.hole_diameter: + return False + + equiv_position = tuple(map(add, other.position, offset)) + + return nearly_equal(self.position, equiv_position) + + def __str__(self): + return "".format(self.width, self.height, self.rotation * 180/math.pi) + + def __repr__(self): + return self.__str__() + + +class Diamond(Primitive): + """ + """ + + def __init__(self, position, width, height, **kwargs): + super(Diamond, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self._width = width + self._height = height + self._to_convert = ['position', 'width', 'height'] + + @property + def flashed(self): + return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + @property + def vertices(self): + if self._vertices is None: + delta_w = self.width / 2. + delta_h = self.height / 2. + top = (self.position[0], (self.position[1] + delta_h)) + right = ((self.position[0] + delta_w), self.position[1]) + bottom = (self.position[0], (self.position[1] - delta_h)) + left = ((self.position[0] - delta_w), self.position[1]) + self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), + ((x * self._sin_theta) + (y * self._cos_theta))) + for x, y in [top, right, bottom, left]] + return self._vertices + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + self._sin_theta * self.height) + + @property + def axis_aligned_height(self): + return (self._cos_theta * self.height + self._sin_theta * self.width) + + +class ChamferRectangle(Primitive): + """ + """ + def __init__(self, position, width, height, chamfer, corners=None, **kwargs): + super(ChamferRectangle, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self._width = width + self._height = height + self._chamfer = chamfer + self._corners = corners if corners is not None else [True] * 4 + self._to_convert = ['position', 'width', 'height', 'chamfer'] + + @property + def flashed(self): + return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def chamfer(self): + return self._chamfer + + @chamfer.setter + def chamfer(self, value): + self._changed() + self._chamfer = value + + @property + def corners(self): + return self._corners + + @corners.setter + def corners(self, value): + self._changed() + self._corners = value + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + @property + def vertices(self): + if self._vertices is None: + vertices = [] + delta_w = self.width / 2. + delta_h = self.height / 2. + # order is UR, UL, LL, LR + rect_corners = [ + ((self.position[0] + delta_w), (self.position[1] + delta_h)), + ((self.position[0] - delta_w), (self.position[1] + delta_h)), + ((self.position[0] - delta_w), (self.position[1] - delta_h)), + ((self.position[0] + delta_w), (self.position[1] - delta_h)) + ] + for idx, params in enumerate(zip(rect_corners, self.corners)): + corner, chamfered = params + x, y = corner + if chamfered: + if idx == 0: + vertices.append((x - self.chamfer, y)) + vertices.append((x, y - self.chamfer)) + elif idx == 1: + vertices.append((x + self.chamfer, y)) + vertices.append((x, y - self.chamfer)) + elif idx == 2: + vertices.append((x + self.chamfer, y)) + vertices.append((x, y + self.chamfer)) + elif idx == 3: + vertices.append((x - self.chamfer, y)) + vertices.append((x, y + self.chamfer)) + else: + vertices.append(corner) + self._vertices = [((x * self._cos_theta - y * self._sin_theta), + (x * self._sin_theta + y * self._cos_theta)) + for x, y in vertices] + return self._vertices + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + + @property + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + + +class RoundRectangle(Primitive): + """ + """ + + def __init__(self, position, width, height, radius, corners, **kwargs): + super(RoundRectangle, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self._width = width + self._height = height + self._radius = radius + self._corners = corners + self._to_convert = ['position', 'width', 'height', 'radius'] + + @property + def flashed(self): + return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._changed() + self._radius = value + + @property + def corners(self): + return self._corners + + @corners.setter + def corners(self, value): + self._changed() + self._corners = value + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + + @property + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + + +class Obround(Primitive): + """ + """ + + def __init__(self, position, width, height, hole_diameter=0, + hole_width=0,hole_height=0, **kwargs): + super(Obround, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self._width = width + self._height = height + self.hole_diameter = hole_diameter + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'width', 'height', 'hole_diameter', + 'hole_width', 'hole_height' ] + + @property + def flashed(self): + return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def hole_radius(self): + """The radius of the hole. If there is no hole, returns None""" + if self.hole_diameter != None: + return self.hole_diameter / 2. + + return None + + @property + def orientation(self): + return 'vertical' if self.height > self.width else 'horizontal' + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + @property + def subshapes(self): + if self.orientation == 'vertical': + circle1 = Circle((self.position[0], self.position[1] + + (self.height - self.width) / 2.), self.width) + circle2 = Circle((self.position[0], self.position[1] - + (self.height - self.width) / 2.), self.width) + rect = Rectangle(self.position, self.width, + (self.height - self.width)) + else: + circle1 = Circle((self.position[0] + - (self.height - self.width) / 2., + self.position[1]), self.height) + circle2 = Circle((self.position[0] + + (self.height - self.width) / 2., + self.position[1]), self.height) + rect = Rectangle(self.position, (self.width - self.height), + self.height) + return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + + @property + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + + +class Polygon(Primitive): + """ + Polygon flash defined by a set number of sides. + """ + def __init__(self, position, sides, radius, hole_diameter=0, + hole_width=0, hole_height=0, **kwargs): + super(Polygon, self).__init__(**kwargs) + validate_coordinates(position) + self._position = position + self.sides = sides + self._radius = radius + self.hole_diameter = hole_diameter + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'radius', 'hole_diameter', + 'hole_width', 'hole_height'] + + @property + def flashed(self): + return True + + @property + def diameter(self): + return self.radius * 2 + + @property + def hole_radius(self): + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._changed() + self._radius = value + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + + @property + def vertices(self): + + offset = self.rotation + delta_angle = 360.0 / self.sides + + points = [] + for i in range(self.sides): + points.append( + rotate_point((self.position[0] + self.radius, self.position[1]), offset + delta_angle * i, self.position)) + return points + + + def equivalent(self, other, offset): + """ + Is this the outline the same as the other, ignoring the position offset? + """ + + # Quick check if it even makes sense to compare them + if type(self) != type(other) or self.sides != other.sides or self.radius != other.radius: + return False + + equiv_pos = tuple(map(add, other.position, offset)) + + return nearly_equal(self.position, equiv_pos) + + +class AMGroup(Primitive): + """ + """ + def __init__(self, amprimitives, stmt = None, **kwargs): + """ + + stmt : The original statment that generated this, since it is really hard to re-generate from primitives + """ + super(AMGroup, self).__init__(**kwargs) + + self.primitives = [] + for amprim in amprimitives: + prim = amprim.to_primitive(self.units) + if isinstance(prim, list): + for p in prim: + self.primitives.append(p) + elif prim: + self.primitives.append(prim) + self._position = None + self._to_convert = ['_position', 'primitives'] + self.stmt = stmt + + def to_inch(self): + if self.units == 'metric': + super(AMGroup, self).to_inch() + + # If we also have a stmt, convert that too + if self.stmt: + self.stmt.to_inch() + + + def to_metric(self): + if self.units == 'inch': + super(AMGroup, self).to_metric() + + # If we also have a stmt, convert that too + if self.stmt: + self.stmt.to_metric() + + @property + def flashed(self): + return True + + @property + def bounding_box(self): + # TODO Make this cached like other items + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + return ((min_x, max_x), (min_y, max_y)) + + @property + def position(self): + return self._position + + def offset(self, x_offset=0, y_offset=0): + self._position = tuple(map(add, self._position, (x_offset, y_offset))) + + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) + + @position.setter + def position(self, new_pos): + ''' + Sets the position of the AMGroup. + This offset all of the objects by the specified distance. + ''' + + if self._position: + dx = new_pos[0] - self._position[0] + dy = new_pos[1] - self._position[1] + else: + dx = new_pos[0] + dy = new_pos[1] + + for primitive in self.primitives: + primitive.offset(dx, dy) + + self._position = new_pos + + def equivalent(self, other, offset): + ''' + Is this the macro group the same as the other, ignoring the position offset? + ''' + + if len(self.primitives) != len(other.primitives): + return False + + # We know they have the same number of primitives, so now check them all + for i in range(0, len(self.primitives)): + if not self.primitives[i].equivalent(other.primitives[i], offset): + return False + + # If we didn't find any differences, then they are the same + return True + +class Outline(Primitive): + """ + Outlines only exist as the rendering for a apeture macro outline. + They don't exist outside of AMGroup objects + """ + + def __init__(self, primitives, **kwargs): + super(Outline, self).__init__(**kwargs) + self.primitives = primitives + self._to_convert = ['primitives'] + + if self.primitives[0].start != self.primitives[-1].end: + raise ValueError('Outline must be closed') + + @property + def flashed(self): + return True + + @property + def bounding_box(self): + if self._bounding_box is None: + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + def offset(self, x_offset=0, y_offset=0): + self._changed() + for p in self.primitives: + p.offset(x_offset, y_offset) + + @property + def vertices(self): + if self._vertices is None: + theta = math.radians(360/self.sides) + vertices = [(self.position[0] + (math.cos(theta * side) * self.radius), + self.position[1] + (math.sin(theta * side) * self.radius)) + for side in range(self.sides)] + self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), + ((x * self._sin_theta) + (y * self._cos_theta))) + for x, y in vertices] + return self._vertices + + @property + def width(self): + bounding_box = self.bounding_box() + return bounding_box[0][1] - bounding_box[0][0] + + def equivalent(self, other, offset): + ''' + Is this the outline the same as the other, ignoring the position offset? + ''' + + # Quick check if it even makes sense to compare them + if type(self) != type(other) or len(self.primitives) != len(other.primitives): + return False + + for i in range(0, len(self.primitives)): + if not self.primitives[i].equivalent(other.primitives[i], offset): + return False + + return True + +class Region(Primitive): + """ + """ + + def __init__(self, primitives, **kwargs): + super(Region, self).__init__(**kwargs) + self.primitives = primitives + self._to_convert = ['primitives'] + + @property + def flashed(self): + return False + + @property + def bounding_box(self): + if self._bounding_box is None: + xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + def offset(self, x_offset=0, y_offset=0): + self._changed() + for p in self.primitives: + p.offset(x_offset, y_offset) + + +class RoundButterfly(Primitive): + """ A circle with two diagonally-opposite quadrants removed + """ + + def __init__(self, position, diameter, **kwargs): + super(RoundButterfly, self).__init__(**kwargs) + validate_coordinates(position) + self.position = position + self.diameter = diameter + self._to_convert = ['position', 'diameter'] + + # TODO This does not reset bounding box correctly + + @property + def flashed(self): + return True + + @property + def radius(self): + return self.diameter / 2. + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + +class SquareButterfly(Primitive): + """ A square with two diagonally-opposite quadrants removed + """ + + def __init__(self, position, side, **kwargs): + super(SquareButterfly, self).__init__(**kwargs) + validate_coordinates(position) + self.position = position + self.side = side + self._to_convert = ['position', 'side'] + + # TODO This does not reset bounding box correctly + + @property + def flashed(self): + return True + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - (self.side / 2.) + max_x = self.position[0] + (self.side / 2.) + min_y = self.position[1] - (self.side / 2.) + max_y = self.position[1] + (self.side / 2.) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + +class Donut(Primitive): + """ A Shape with an identical concentric shape removed from its center + """ + + def __init__(self, position, shape, inner_diameter, + outer_diameter, **kwargs): + super(Donut, self).__init__(**kwargs) + validate_coordinates(position) + self.position = position + if shape not in ('round', 'square', 'hexagon', 'octagon'): + raise ValueError( + 'Valid shapes are round, square, hexagon or octagon') + self.shape = shape + if inner_diameter >= outer_diameter: + raise ValueError( + 'Outer diameter must be larger than inner diameter.') + self.inner_diameter = inner_diameter + self.outer_diameter = outer_diameter + if self.shape in ('round', 'square', 'octagon'): + self.width = outer_diameter + self.height = outer_diameter + else: + # Hexagon + self.width = 0.5 * math.sqrt(3.) * outer_diameter + self.height = outer_diameter + + self._to_convert = ['position', 'width', + 'height', 'inner_diameter', 'outer_diameter'] + + # TODO This does not reset bounding box correctly + + @property + def flashed(self): + return True + + @property + def lower_left(self): + return (self.position[0] - (self.width / 2.), + self.position[1] - (self.height / 2.)) + + @property + def upper_right(self): + return (self.position[0] + (self.width / 2.), + self.position[1] + (self.height / 2.)) + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.width / 2.), + self.position[1] - (self.height / 2.)) + ur = (self.position[0] + (self.width / 2.), + self.position[1] + (self.height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + +class SquareRoundDonut(Primitive): + """ A Square with a circular cutout in the center + """ + + def __init__(self, position, inner_diameter, outer_diameter, **kwargs): + super(SquareRoundDonut, self).__init__(**kwargs) + validate_coordinates(position) + self.position = position + if inner_diameter >= outer_diameter: + raise ValueError( + 'Outer diameter must be larger than inner diameter.') + self.inner_diameter = inner_diameter + self.outer_diameter = outer_diameter + self._to_convert = ['position', 'inner_diameter', 'outer_diameter'] + + @property + def flashed(self): + return True + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = tuple([c - self.outer_diameter / 2. for c in self.position]) + ur = tuple([c + self.outer_diameter / 2. for c in self.position]) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + +class Drill(Primitive): + """ A drill hole + """ + def __init__(self, position, diameter, **kwargs): + super(Drill, self).__init__('dark', **kwargs) + validate_coordinates(position) + self._position = position + self._diameter = diameter + self._to_convert = ['position', 'diameter'] + + @property + def flashed(self): + return False + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def diameter(self): + return self._diameter + + @diameter.setter + def diameter(self, value): + self._changed() + self._diameter = value + + @property + def radius(self): + return self.diameter / 2. + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + def offset(self, x_offset=0, y_offset=0): + self._changed() + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + + def __str__(self): + return '' % (self.diameter, self.units, self.position[0], self.position[1]) + + +class Slot(Primitive): + """ A drilled slot + """ + def __init__(self, start, end, diameter, **kwargs): + super(Slot, self).__init__('dark', **kwargs) + validate_coordinates(start) + validate_coordinates(end) + self.start = start + self.end = end + self.diameter = diameter + self._to_convert = ['start', 'end', 'diameter'] + + + @property + def flashed(self): + return False + + @property + def bounding_box(self): + if self._bounding_box is None: + radius = self.diameter / 2. + min_x = min(self.start[0], self.end[0]) - radius + max_x = max(self.start[0], self.end[0]) + radius + min_y = min(self.start[1], self.end[1]) - radius + max_y = max(self.start[1], self.end[1]) + radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + def offset(self, x_offset=0, y_offset=0): + self.start = tuple(map(add, self.start, (x_offset, y_offset))) + self.end = tuple(map(add, self.end, (x_offset, y_offset))) + + +class TestRecord(Primitive): + """ Netlist Test record + """ + + def __init__(self, position, net_name, layer, **kwargs): + super(TestRecord, self).__init__(**kwargs) + validate_coordinates(position) + self.position = position + self.net_name = net_name + self.layer = layer + self._to_convert = ['position'] diff --git a/gerbonara/gerber/render/__init__.py b/gerbonara/gerber/render/__init__.py new file mode 100644 index 0000000..c7dbdd5 --- /dev/null +++ b/gerbonara/gerber/render/__init__.py @@ -0,0 +1,31 @@ +#! /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 .render import RenderSettings +from .cairo_backend import GerberCairoContext + +available_renderers = { + 'cairo': GerberCairoContext, +} diff --git a/gerbonara/gerber/render/cairo_backend.py b/gerbonara/gerber/render/cairo_backend.py new file mode 100644 index 0000000..e1d1408 --- /dev/null +++ b/gerbonara/gerber/render/cairo_backend.py @@ -0,0 +1,616 @@ +#!/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. + +try: + import cairo +except ImportError: + import cairocffi as cairo + +from operator import mul +import tempfile +import copy +import os + +from .render import GerberContext, RenderSettings +from .theme import THEMES +from ..primitives import * +from ..utils import rotate_point + +from io import BytesIO + + +class GerberCairoContext(GerberContext): + + def __init__(self, scale=300): + super(GerberCairoContext, self).__init__() + self.scale = (scale, scale) + self.surface = None + self.surface_buffer = None + self.ctx = None + self.active_layer = None + self.active_matrix = None + self.output_ctx = None + self.has_bg = False + self.origin_in_inch = None + self.size_in_inch = None + self._xform_matrix = None + self._render_count = 0 + + @property + def origin_in_pixels(self): + return (self.scale_point(self.origin_in_inch) + if self.origin_in_inch is not None else (0.0, 0.0)) + + @property + def size_in_pixels(self): + return (self.scale_point(self.size_in_inch) + if self.size_in_inch is not None else (0.0, 0.0)) + + def set_bounds(self, bounds, new_surface=False): + origin_in_inch = (bounds[0][0], bounds[1][0]) + size_in_inch = (abs(bounds[0][1] - bounds[0][0]), + abs(bounds[1][1] - bounds[1][0])) + size_in_pixels = self.scale_point(size_in_inch) + self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch + self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch + self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, + x0=-self.origin_in_pixels[0], + y0=self.size_in_pixels[1]) + if (self.surface is None) or new_surface: + self.surface_buffer = tempfile.NamedTemporaryFile() + self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) + self.output_ctx = cairo.Context(self.surface) + + def render_layer(self, layer, filename=None, settings=None, bgsettings=None, + verbose=False, bounds=None): + if settings is None: + settings = THEMES['default'].get(layer.layer_class, RenderSettings()) + if bgsettings is None: + bgsettings = THEMES['default'].get('background', RenderSettings()) + + if self._render_count == 0: + if verbose: + print('[Render]: Rendering Background.') + self.clear() + if bounds is not None: + self.set_bounds(bounds) + else: + self.set_bounds(layer.bounds) + self.paint_background(bgsettings) + if verbose: + print('[Render]: Rendering {} Layer.'.format(layer.layer_class)) + self._render_count += 1 + self._render_layer(layer, settings) + if filename is not None: + self.dump(filename, verbose) + + def render_layers(self, layers, filename, theme=THEMES['default'], + verbose=False, max_width=800, max_height=600): + """ Render a set of layers + """ + # Calculate scale parameter + x_range = [10000, -10000] + y_range = [10000, -10000] + for layer in layers: + bounds = layer.bounds + if bounds is not None: + layer_x, layer_y = bounds + x_range[0] = min(x_range[0], layer_x[0]) + x_range[1] = max(x_range[1], layer_x[1]) + y_range[0] = min(y_range[0], layer_y[0]) + y_range[1] = max(y_range[1], layer_y[1]) + width = x_range[1] - x_range[0] + height = y_range[1] - y_range[0] + + scale = math.floor(min(float(max_width)/width, float(max_height)/height)) + self.scale = (scale, scale) + + self.clear() + + # Render layers + bgsettings = theme['background'] + for layer in layers: + settings = theme.get(layer.layer_class, RenderSettings()) + self.render_layer(layer, settings=settings, bgsettings=bgsettings, + verbose=verbose) + self.dump(filename, verbose) + + def dump(self, filename=None, verbose=False): + """ Save image as `filename` + """ + try: + is_svg = os.path.splitext(filename.lower())[1] == '.svg' + except: + is_svg = False + if verbose: + print('[Render]: Writing image to {}'.format(filename)) + if is_svg: + self.surface.finish() + self.surface_buffer.flush() + with open(filename, "wb") as f: + self.surface_buffer.seek(0) + f.write(self.surface_buffer.read()) + f.flush() + else: + return self.surface.write_to_png(filename) + + def dump_str(self): + """ Return a byte-string containing the rendered image. + """ + fobj = BytesIO() + self.surface.write_to_png(fobj) + return fobj.getvalue() + + def dump_svg_str(self): + """ Return a string containg the rendered SVG. + """ + self.surface.finish() + self.surface_buffer.flush() + return self.surface_buffer.read() + + def clear(self): + self.surface = None + self.output_ctx = None + self.has_bg = False + self.origin_in_inch = None + self.size_in_inch = None + self._xform_matrix = None + self._render_count = 0 + self.surface_buffer = None + + def _new_mask(self): + class Mask: + def __enter__(msk): + size_in_pixels = self.size_in_pixels + msk.surface = cairo.SVGSurface(None, size_in_pixels[0], + size_in_pixels[1]) + msk.ctx = cairo.Context(msk.surface) + msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1]) + return msk + + + def __exit__(msk, exc_type, exc_val, traceback): + if hasattr(msk.surface, 'finish'): + msk.surface.finish() + + return Mask() + + def _render_layer(self, layer, settings): + self.invert = settings.invert + # Get a new clean layer to render on + self.new_render_layer(mirror=settings.mirror) + for prim in layer.primitives: + self.render(prim) + # Add layer to image + self.flatten(settings.color, settings.alpha) + + def _render_line(self, line, color): + start = self.scale_point(line.start) + end = self.scale_point(line.end) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + + with self._clip_primitive(line): + with self._new_mask() as mask: + if isinstance(line.aperture, Circle): + width = line.aperture.diameter + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + + elif hasattr(line, 'vertices') and line.vertices is not None: + points = [self.scale_point(x) for x in line.vertices] + mask.ctx.set_line_width(0) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_arc(self, arc, color): + center = self.scale_point(arc.center) + start = self.scale_point(arc.start) + end = self.scale_point(arc.end) + radius = self.scale[0] * arc.radius + two_pi = 2 * math.pi + angle1 = (arc.start_angle + two_pi) % two_pi + angle2 = (arc.end_angle + two_pi) % two_pi + if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant': + # Make the angles slightly different otherwise Cario will draw nothing + angle2 -= 0.000000001 + if isinstance(arc.aperture, Circle): + width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 + else: + width = max(arc.aperture.width, arc.aperture.height, 0.001) + + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(arc): + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND if isinstance(arc.aperture, Circle) else cairo.LINE_CAP_SQUARE) + mask.ctx.move_to(*start) # You actually have to do this... + if arc.direction == 'counterclockwise': + mask.ctx.arc(center[0], center[1], radius, angle1, angle2) + else: + mask.ctx.arc_negative(center[0], center[1], radius, + angle1, angle2) + mask.ctx.move_to(*end) # ...lame + mask.ctx.stroke() + + #if isinstance(arc.aperture, Rectangle): + # print("Flash Rectangle Ends") + # print(arc.aperture.rotation * 180/math.pi) + # rect = arc.aperture + # width = self.scale[0] * rect.width + # height = self.scale[1] * rect.height + # for point, angle in zip((start, end), (angle1, angle2)): + # print("{} w {} h{}".format(point, rect.width, rect.height)) + # mask.ctx.rectangle(point[0] - width/2.0, + # point[1] - height/2.0, width, height) + # mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_region(self, region, color): + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) and region.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(region): + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*self.scale_point(region.primitives[0].start)) + for prim in region.primitives: + if isinstance(prim, Line): + mask.ctx.line_to(*self.scale_point(prim.end)) + else: + center = self.scale_point(prim.center) + radius = self.scale[0] * prim.radius + angle1 = prim.start_angle + angle2 = prim.end_angle + if prim.direction == 'counterclockwise': + mask.ctx.arc(center[0], center[1], radius, + angle1, angle2) + else: + mask.ctx.arc_negative(center[0], center[1], radius, + angle1, angle2) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_circle(self, circle, color): + center = self.scale_point(circle.position) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and circle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(circle): + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi)) + mask.ctx.fill() + + if hasattr(circle, 'hole_diameter') and circle.hole_diameter is not None and circle.hole_diameter > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], center[1], circle.hole_radius * self.scale[0], 0, 2 * math.pi) + mask.ctx.fill() + + if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') + and circle.hole_width is not None and circle.hole_height is not None + and circle.hole_width > 0 and circle.hole_height > 0): + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if circle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((circle.hole_width, circle.hole_height)) + lower_left = rotate_point( + (center[0] - width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_rectangle(self, rectangle, color): + lower_left = self.scale_point(rectangle.lower_left) + width, height = tuple([abs(coord) for coord in + self.scale_point((rectangle.width, + rectangle.height))]) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and rectangle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(rectangle): + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.rectangle(lower_left[0], lower_left[1], width, height) + mask.ctx.fill() + + center = self.scale_point(rectangle.position) + if rectangle.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + + mask.ctx.arc(center[0], center[1], rectangle.hole_radius * self.scale[0], 0, 2 * math.pi) + mask.ctx.fill() + + if rectangle.hole_width > 0 and rectangle.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height)) + lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center) + lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_obround(self, obround, color): + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and obround.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(obround): + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + + # Render circles + for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']): + center = self.scale_point(circle.position) + mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi)) + mask.ctx.fill() + + # Render Rectangle + rectangle = obround.subshapes['rectangle'] + lower_left = self.scale_point(rectangle.lower_left) + width, height = tuple([abs(coord) for coord in + self.scale_point((rectangle.width, + rectangle.height))]) + mask.ctx.rectangle(lower_left[0], lower_left[1], width, height) + mask.ctx.fill() + + center = self.scale_point(obround.position) + if obround.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], center[1], obround.hole_radius * self.scale[0], 0, 2 * math.pi) + mask.ctx.fill() + + if obround.hole_width > 0 and obround.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height =self.scale_point((obround.hole_width, obround.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_polygon(self, polygon, color): + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and polygon.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._clip_primitive(polygon): + with self._new_mask() as mask: + + vertices = polygon.vertices + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + # Start from before the end so it is easy to iterate and make sure + # it is closed + mask.ctx.move_to(*self.scale_point(vertices[-1])) + for v in vertices: + mask.ctx.line_to(*self.scale_point(v)) + mask.ctx.fill() + + center = self.scale_point(polygon.position) + if polygon.hole_radius > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], + center[1], + polygon.hole_radius * self.scale[0], 0, 2 * math.pi) + mask.ctx.fill() + + if polygon.hole_width > 0 and polygon.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((polygon.hole_width, polygon.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_drill(self, circle, color=None): + color = color if color is not None else self.drill_color + self._render_circle(circle, color) + + def _render_slot(self, slot, color): + start = map(mul, slot.start, self.scale) + end = map(mul, slot.end, self.scale) + + width = slot.diameter + + self.ctx.set_operator(cairo.OPERATOR_OVER + if slot.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) + with self._clip_primitive(slot): + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + + def _render_amgroup(self, amgroup, color): + for primitive in amgroup.primitives: + self.render(primitive) + + def _render_test_record(self, primitive, color): + position = [pos + origin for pos, origin in + zip(primitive.position, self.origin_in_inch)] + self.ctx.select_font_face( + 'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + self.ctx.set_font_size(13) + self._render_circle(Circle(position, 0.015), color) + self.ctx.set_operator(cairo.OPERATOR_OVER + if primitive.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) + self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position]) + self.ctx.scale(1, -1) + self.ctx.show_text(primitive.net_name) + self.ctx.scale(1, -1) + + def new_render_layer(self, color=None, mirror=False): + size_in_pixels = self.scale_point(self.size_in_inch) + matrix = copy.copy(self._xform_matrix) + layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) + ctx = cairo.Context(layer) + + if self.invert: + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.paint() + if mirror: + matrix.xx = -1.0 + matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] + self.ctx = ctx + self.ctx.set_matrix(matrix) + self.active_layer = layer + self.active_matrix = matrix + + def flatten(self, color=None, alpha=None): + color = color if color is not None else self.color + alpha = alpha if alpha is not None else self.alpha + self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha) + self.output_ctx.mask_surface(self.active_layer) + self.ctx = None + self.active_layer = None + self.active_matrix = None + + def paint_background(self, settings=None): + color = settings.color if settings is not None else self.background_color + alpha = settings.alpha if settings is not None else 1.0 + if not self.has_bg: + self.has_bg = True + self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha) + self.output_ctx.paint() + + def _clip_primitive(self, primitive): + """ Clip rendering context to pixel-aligned bounding box + + Calculates pixel- and axis- aligned bounding box, and clips current + context to that region. Improves rendering speed significantly. This + returns a context manager, use as follows: + + with self._clip_primitive(some_primitive): + do_rendering_stuff() + do_more_rendering stuff(with, arguments) + + The context manager will reset the context's clipping region when it + goes out of scope. + + """ + class Clip: + def __init__(clp, primitive): + x_range, y_range = primitive.bounding_box + xmin, xmax = x_range + ymin, ymax = y_range + + # Round bounds to the nearest pixel outside of the primitive + clp.xmin = math.floor(self.scale[0] * xmin) + clp.xmax = math.ceil(self.scale[0] * xmax) + + # We need to offset Y to take care of the difference in y-pos + # caused by flipping the axis. + clp.ymin = math.floor( + (self.scale[1] * ymin) - math.ceil(self.origin_in_pixels[1])) + clp.ymax = math.floor( + (self.scale[1] * ymax) - math.floor(self.origin_in_pixels[1])) + + # Calculate width and height, rounded to the nearest pixel + clp.width = abs(clp.xmax - clp.xmin) + clp.height = abs(clp.ymax - clp.ymin) + + def __enter__(clp): + # Clip current context to primitive's bounding box + self.ctx.rectangle(clp.xmin, clp.ymin, clp.width, clp.height) + self.ctx.clip() + + def __exit__(clp, exc_type, exc_val, traceback): + # Reset context clip region + self.ctx.reset_clip() + + return Clip(primitive) + + def scale_point(self, point): + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) diff --git a/gerbonara/gerber/render/excellon_backend.py b/gerbonara/gerber/render/excellon_backend.py new file mode 100644 index 0000000..765d68c --- /dev/null +++ b/gerbonara/gerber/render/excellon_backend.py @@ -0,0 +1,188 @@ + +from .render import GerberContext +from ..excellon import DrillSlot +from ..excellon_statements import * + +class ExcellonContext(GerberContext): + + MODE_DRILL = 1 + MODE_SLOT =2 + + def __init__(self, settings): + GerberContext.__init__(self) + + # Statements that we write + self.comments = [] + self.header = [] + self.tool_def = [] + self.body_start = [RewindStopStmt()] + self.body = [] + self.start = [HeaderBeginStmt()] + + # Current tool and position + self.handled_tools = set() + self.cur_tool = None + self.drill_mode = ExcellonContext.MODE_DRILL + self.drill_down = False + self._pos = (None, None) + + self.settings = settings + + self._start_header() + self._start_comments() + + def _start_header(self): + """Create the header from the settings""" + + self.header.append(UnitStmt.from_settings(self.settings)) + + if self.settings.notation == 'incremental': + raise NotImplementedError('Incremental mode is not implemented') + else: + self.body.append(AbsoluteModeStmt()) + + def _start_comments(self): + + # Write the digits used - this isn't valid Excellon statement, so we write as a comment + self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1]))) + + def _get_end(self): + """How we end depends on our mode""" + + end = [] + + if self.drill_down: + end.append(RetractWithClampingStmt()) + end.append(RetractWithoutClampingStmt()) + + end.append(EndOfProgramStmt()) + + return end + + @property + def statements(self): + return self.start + self.comments + self.header + self.body_start + self.body + self._get_end() + + def set_bounds(self, bounds, *args, **kwargs): + pass + + def paint_background(self): + pass + + def _render_line(self, line, color): + raise ValueError('Invalid Excellon object') + def _render_arc(self, arc, color): + raise ValueError('Invalid Excellon object') + + def _render_region(self, region, color): + raise ValueError('Invalid Excellon object') + + def _render_level_polarity(self, region): + raise ValueError('Invalid Excellon object') + + def _render_circle(self, circle, color): + raise ValueError('Invalid Excellon object') + + def _render_rectangle(self, rectangle, color): + raise ValueError('Invalid Excellon object') + + def _render_obround(self, obround, color): + raise ValueError('Invalid Excellon object') + + def _render_polygon(self, polygon, color): + raise ValueError('Invalid Excellon object') + + def _simplify_point(self, point): + return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) + + def _render_drill(self, drill, color): + + if self.drill_mode != ExcellonContext.MODE_DRILL: + self._start_drill_mode() + + tool = drill.hit.tool + if not tool in self.handled_tools: + self.handled_tools.add(tool) + self.header.append(ExcellonTool.from_tool(tool)) + + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool + + point = self._simplify_point(drill.position) + self._pos = drill.position + self.body.append(CoordinateStmt.from_point(point)) + + def _start_drill_mode(self): + """ + If we are not in drill mode, then end the ROUT so we can do basic drilling + """ + + if self.drill_mode == ExcellonContext.MODE_SLOT: + + # Make sure we are retracted before changing modes + last_cmd = self.body[-1] + if self.drill_down: + self.body.append(RetractWithClampingStmt()) + self.body.append(RetractWithoutClampingStmt()) + self.drill_down = False + + # Switch to drill mode + self.body.append(DrillModeStmt()) + self.drill_mode = ExcellonContext.MODE_DRILL + + else: + raise ValueError('Should be in slot mode') + + def _render_slot(self, slot, color): + + # Set the tool first, before we might go into drill mode + tool = slot.hit.tool + if not tool in self.handled_tools: + self.handled_tools.add(tool) + self.header.append(ExcellonTool.from_tool(tool)) + + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool + + # Two types of drilling - normal drill and slots + if slot.hit.slot_type == DrillSlot.TYPE_ROUT: + + # For ROUT, setting the mode is part of the actual command. + + # Are we in the right position? + if slot.start != self._pos: + if self.drill_down: + # We need to move into the right position, so retract + self.body.append(RetractWithClampingStmt()) + self.drill_down = False + + # Move to the right spot + point = self._simplify_point(slot.start) + self._pos = slot.start + self.body.append(CoordinateStmt.from_point(point, mode="ROUT")) + + # Now we are in the right spot, so drill down + if not self.drill_down: + self.body.append(ZAxisRoutPositionStmt()) + self.drill_down = True + + # Do a linear move from our current position to the end position + point = self._simplify_point(slot.end) + self._pos = slot.end + self.body.append(CoordinateStmt.from_point(point, mode="LINEAR")) + + self.drill_mode = ExcellonContext.MODE_SLOT + + else: + # This is a G85 slot, so do this in normally drilling mode + if self.drill_mode != ExcellonContext.MODE_DRILL: + self._start_drill_mode() + + # Slots don't use simplified points + self._pos = slot.end + self.body.append(SlotStmt.from_points(slot.start, slot.end)) + + def _render_inverted_layer(self): + pass diff --git a/gerbonara/gerber/render/render.py b/gerbonara/gerber/render/render.py new file mode 100644 index 0000000..580a7ea --- /dev/null +++ b/gerbonara/gerber/render/render.py @@ -0,0 +1,246 @@ +#! /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. +""" +Rendering +============ +**Gerber (RS-274X) and Excellon file rendering** + +Render Gerber and Excellon files to a variety of formats. The render module +currently supports SVG rendering using the `svgwrite` library. +""" + + +from ..primitives import * +from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, + CoordStmt, ApertureStmt, RegionModeStmt, + QuadrantModeStmt,) + + +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 + ---------- + units : string + Measurement units. 'inch' or 'metric' + + 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`. + + alpha : float + Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) + """ + + def __init__(self, units='inch'): + self._units = units + self._color = (0.7215, 0.451, 0.200) + self._background_color = (0.0, 0.0, 0.0) + self._drill_color = (0.0, 0.0, 0.0) + self._alpha = 1.0 + self._invert = False + self.ctx = None + + @property + def units(self): + return self._units + + @units.setter + def units(self, units): + if units not in ('inch', 'metric'): + raise ValueError('Units may be "inch" or "metric"') + self._units = units + + @property + def color(self): + return self._color + + @color.setter + def color(self, color): + if len(color) != 3: + raise TypeError('Color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._color = color + + @property + def drill_color(self): + return self._drill_color + + @drill_color.setter + def drill_color(self, color): + if len(color) != 3: + raise TypeError('Drill color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._drill_color = color + + @property + def background_color(self): + return self._background_color + + @background_color.setter + def background_color(self, color): + if len(color) != 3: + raise TypeError('Background color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._background_color = color + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, alpha): + if alpha < 0 or alpha > 1: + raise ValueError('Alpha must be between 0.0 and 1.0') + self._alpha = alpha + + @property + def invert(self): + return self._invert + + @invert.setter + def invert(self, invert): + self._invert = invert + + def render(self, primitive): + if not primitive: + return + + self.pre_render_primitive(primitive) + + color = self.color + if isinstance(primitive, Line): + self._render_line(primitive, color) + elif isinstance(primitive, Arc): + 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(primitive, color) + elif isinstance(primitive, Drill): + self._render_drill(primitive, self.color) + elif isinstance(primitive, Slot): + self._render_slot(primitive, self.color) + elif isinstance(primitive, AMGroup): + self._render_amgroup(primitive, color) + elif isinstance(primitive, Outline): + self._render_region(primitive, color) + elif isinstance(primitive, TestRecord): + self._render_test_record(primitive, color) + + self.post_render_primitive(primitive) + + def set_bounds(self, bounds, *args, **kwargs): + """Called by the renderer to set the extents of the file to render. + + Parameters + ---------- + bounds: Tuple[Tuple[float, float], Tuple[float, float]] + ( (x_min, x_max), (y_min, y_max) + """ + pass + + def paint_background(self): + pass + + def new_render_layer(self): + pass + + def flatten(self): + pass + + def pre_render_primitive(self, primitive): + """ + Called before rendering a primitive. Use the callback to perform some action before rendering + a primitive, for example adding a comment. + """ + return + + def post_render_primitive(self, primitive): + """ + Called after rendering a primitive. Use the callback to perform some action after rendering + a primitive + """ + return + + + def _render_line(self, primitive, color): + pass + + def _render_arc(self, primitive, color): + pass + + def _render_region(self, primitive, color): + pass + + def _render_circle(self, primitive, color): + pass + + def _render_rectangle(self, primitive, color): + pass + + def _render_obround(self, primitive, color): + pass + + def _render_polygon(self, primitive, color): + pass + + def _render_drill(self, primitive, color): + pass + + def _render_slot(self, primitive, color): + pass + + def _render_amgroup(self, primitive, color): + pass + + def _render_test_record(self, primitive, color): + pass + + +class RenderSettings(object): + def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, + mirror=False): + self.color = color + self.alpha = alpha + self.invert = invert + self.mirror = mirror diff --git a/gerbonara/gerber/render/rs274x_backend.py b/gerbonara/gerber/render/rs274x_backend.py new file mode 100644 index 0000000..c7af2ea --- /dev/null +++ b/gerbonara/gerber/render/rs274x_backend.py @@ -0,0 +1,510 @@ +"""Renders an in-memory Gerber file to statements which can be written to a string +""" +from copy import deepcopy + +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .render import GerberContext +from ..am_statements import * +from ..gerber_statements import * +from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle + + +class AMGroupContext(object): + '''A special renderer to generate aperature macros from an AMGroup''' + + def __init__(self): + self.statements = [] + + def render(self, amgroup, name): + + if amgroup.stmt: + # We know the statement it was generated from, so use that to create the AMParamStmt + # It will give a much better result + + stmt = deepcopy(amgroup.stmt) + stmt.name = name + + return stmt + + else: + # Clone ourselves, then offset by the psotion so that + # our render doesn't have to consider offset. Just makes things simpler + nooffset_group = deepcopy(amgroup) + nooffset_group.position = (0, 0) + + # Now draw the shapes + for primitive in nooffset_group.primitives: + if isinstance(primitive, Outline): + self._render_outline(primitive) + elif isinstance(primitive, Circle): + self._render_circle(primitive) + elif isinstance(primitive, Rectangle): + self._render_rectangle(primitive) + elif isinstance(primitive, Line): + self._render_line(primitive) + elif isinstance(primitive, Polygon): + self._render_polygon(primitive) + else: + raise ValueError('amgroup') + + statement = AMParamStmt('AM', name, self._statements_to_string()) + return statement + + def _statements_to_string(self): + macro = '' + + for statement in self.statements: + macro += statement.to_gerber() + + return macro + + def _render_circle(self, circle): + self.statements.append(AMCirclePrimitive.from_primitive(circle)) + + def _render_rectangle(self, rectangle): + self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle)) + + def _render_line(self, line): + self.statements.append(AMVectorLinePrimitive.from_primitive(line)) + + def _render_outline(self, outline): + self.statements.append(AMOutlinePrimitive.from_primitive(outline)) + + def _render_polygon(self, polygon): + self.statements.append(AMPolygonPrimitive.from_primitive(polygon)) + + def _render_thermal(self, thermal): + pass + + +class Rs274xContext(GerberContext): + + def __init__(self, settings): + GerberContext.__init__(self) + self.comments = [] + self.header = [] + self.body = [] + self.end = [EofStmt()] + + # Current values so we know if we have to execute + # moves, levey changes before anything else + self._level_polarity = None + self._pos = (None, None) + self._func = None + self._quadrant_mode = None + self._dcode = None + + # Primarily for testing and comarison to files, should we write + # flashes as a single statement or a move plus flash? Set to true + # to do in a single statement. Normally this can be false + self.condensed_flash = True + + # When closing a region, force a D02 staement to close a region. + # This is normally not necessary because regions are closed with a G37 + # staement, but this will add an extra statement for doubly close + # the region + self.explicit_region_move_end = False + + self._next_dcode = 10 + self._rects = {} + self._circles = {} + self._obrounds = {} + self._polygons = {} + self._macros = {} + + self._i_none = 0 + self._j_none = 0 + + self.settings = settings + + self._start_header(settings) + + def _start_header(self, settings): + self.header.append(FSParamStmt.from_settings(settings)) + self.header.append(MOParamStmt.from_units(settings.units)) + + def _simplify_point(self, point): + return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) + + def _simplify_offset(self, point, offset): + + if point[0] != offset[0]: + xoffset = point[0] - offset[0] + else: + xoffset = self._i_none + + if point[1] != offset[1]: + yoffset = point[1] - offset[1] + else: + yoffset = self._j_none + + return (xoffset, yoffset) + + @property + def statements(self): + return self.comments + self.header + self.body + self.end + + def set_bounds(self, bounds, *args, **kwargs): + pass + + def paint_background(self): + pass + + def _select_aperture(self, aperture): + + # Select the right aperture if not already selected + if aperture: + if isinstance(aperture, Circle): + aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height) + elif isinstance(aperture, Rectangle): + aper = self._get_rectangle(aperture.width, aperture.height) + elif isinstance(aperture, Obround): + aper = self._get_obround(aperture.width, aperture.height) + elif isinstance(aperture, AMGroup): + aper = self._get_amacro(aperture) + else: + raise NotImplementedError('Line with invalid aperture type') + + if aper.d != self._dcode: + self.body.append(ApertureStmt(aper.d)) + self._dcode = aper.d + + def pre_render_primitive(self, primitive): + + if hasattr(primitive, 'comment'): + self.body.append(CommentStmt(primitive.comment)) + + def _render_line(self, line, color, default_polarity='dark'): + + self._select_aperture(line.aperture) + + self._render_level_polarity(line, default_polarity) + + # Get the right function + if self._func != CoordStmt.FUNC_LINEAR: + func = CoordStmt.FUNC_LINEAR + else: + func = None + self._func = CoordStmt.FUNC_LINEAR + + if self._pos != line.start: + self.body.append(CoordStmt.move(func, self._simplify_point(line.start))) + self._pos = line.start + # We already set the function, so the next command doesn't require that + func = None + + point = self._simplify_point(line.end) + + # In some files, we see a lot of duplicated ponts, so omit those + if point[0] != None or point[1] != None: + self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) + self._pos = line.end + elif func: + self.body.append(CoordStmt.mode(func)) + + def _render_arc(self, arc, color, default_polarity='dark'): + + # Optionally set the quadrant mode if it has changed: + if arc.quadrant_mode != self._quadrant_mode: + + if arc.quadrant_mode != 'multi-quadrant': + self.body.append(QuadrantModeStmt.single()) + else: + self.body.append(QuadrantModeStmt.multi()) + + self._quadrant_mode = arc.quadrant_mode + + # Select the right aperture if not already selected + self._select_aperture(arc.aperture) + + self._render_level_polarity(arc, default_polarity) + + # Find the right movement mode. Always set to be sure it is really right + dir = arc.direction + if dir == 'clockwise': + func = CoordStmt.FUNC_ARC_CW + self._func = CoordStmt.FUNC_ARC_CW + elif dir == 'counterclockwise': + func = CoordStmt.FUNC_ARC_CCW + self._func = CoordStmt.FUNC_ARC_CCW + else: + raise ValueError('Invalid circular interpolation mode') + + if self._pos != arc.start: + # TODO I'm not sure if this is right + self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start))) + self._pos = arc.start + + center = self._simplify_offset(arc.center, arc.start) + end = self._simplify_point(arc.end) + self.body.append(CoordStmt.arc(func, end, center)) + self._pos = arc.end + + def _render_region(self, region, color): + + self._render_level_polarity(region) + + self.body.append(RegionModeStmt.on()) + + for p in region.primitives: + + # Make programmatically generated primitives within a region with + # unset level polarity inherit the region's level polarity + if isinstance(p, Line): + self._render_line(p, color, default_polarity=region.level_polarity) + else: + self._render_arc(p, color, default_polarity=region.level_polarity) + + if self.explicit_region_move_end: + self.body.append(CoordStmt.move(None, None)) + + self.body.append(RegionModeStmt.off()) + + def _render_level_polarity(self, obj, default='dark'): + obj_polarity = obj.level_polarity if obj.level_polarity is not None else default + if obj_polarity != self._level_polarity: + self._level_polarity = obj_polarity + self.body.append(LPParamStmt('LP', obj_polarity)) + + def _render_flash(self, primitive, aperture): + + self._render_level_polarity(primitive) + + if aperture.d != self._dcode: + self.body.append(ApertureStmt(aperture.d)) + self._dcode = aperture.d + + if self.condensed_flash: + self.body.append(CoordStmt.flash(self._simplify_point(primitive.position))) + else: + self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position))) + self.body.append(CoordStmt.flash(None)) + + self._pos = primitive.position + + def _get_circle(self, diameter, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): + '''Define a circlar aperture''' + + key = (diameter, hole_diameter, hole_width, hole_height) + aper = self._circles.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height) + self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper + self.header.append(aper) + + return aper + + def _render_circle(self, circle, color): + + aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height) + self._render_flash(circle, aper) + + def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): + '''Get a rectanglar aperture. If it isn't defined, create it''' + + key = (width, height, hole_diameter, hole_width, hole_height) + aper = self._rects.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height) + self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper + self.header.append(aper) + + return aper + + def _render_rectangle(self, rectangle, color): + + aper = self._get_rectangle(rectangle.width, rectangle.height, + rectangle.hole_diameter, + rectangle.hole_width, rectangle.hole_height) + self._render_flash(rectangle, aper) + + def _get_obround(self, width, height, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): + + key = (width, height, hole_diameter, hole_width, hole_height) + aper = self._obrounds.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height) + self._obrounds[key] = aper + self.header.append(aper) + + return aper + + def _render_obround(self, obround, color): + + aper = self._get_obround(obround.width, obround.height, + obround.hole_diameter, obround.hole_width, + obround.hole_height) + self._render_flash(obround, aper) + + def _render_polygon(self, polygon, color): + + aper = self._get_polygon(polygon.radius, polygon.sides, + polygon.rotation, polygon.hole_diameter, + polygon.hole_width, polygon.hole_height) + self._render_flash(polygon, aper) + + def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None, + hole_width=None, hole_height=None, dcode = None): + + key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height) + aper = self._polygons.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, + rotation, hole_diameter, hole_width, + hole_height) + self._polygons[key] = aper + self.header.append(aper) + + return aper + + def _render_drill(self, drill, color): + raise ValueError('Drills are not valid in RS274X files') + + def _hash_amacro(self, amgroup): + '''Calculate a very quick hash code for deciding if we should even check AM groups for comparision''' + + # We always start with an X because this forms part of the name + # Basically, in some cases, the name might start with a C, R, etc. That can appear + # to conflict with normal aperture definitions. Technically, it shouldn't because normal + # aperture definitions should have a comma, but in some cases the commit is omitted + hash = 'X' + for primitive in amgroup.primitives: + + hash += primitive.__class__.__name__[0] + + bbox = primitive.bounding_box + hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2] + hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2] + + if hasattr(primitive, 'primitives'): + hash += str(len(primitive.primitives)) + + if isinstance(primitive, Rectangle): + hash += str(primitive.width * 1000000)[0:2] + hash += str(primitive.height * 1000000)[0:2] + elif isinstance(primitive, Circle): + hash += str(primitive.diameter * 1000000)[0:2] + + if len(hash) > 20: + # The hash might actually get quite complex, so stop before + # it gets too long + break + + return hash + + def _get_amacro(self, amgroup, dcode = None): + # Macros are a little special since we don't have a good way to compare them quickly + # but in most cases, this should work + + hash = self._hash_amacro(amgroup) + macro = None + macroinfo = self._macros.get(hash, None) + + if macroinfo: + + # We have a definition, but check that the groups actually are the same + for macro in macroinfo: + + # Macros should have positions, right? But if the macro is selected for non-flashes + # then it won't have a position. This is of course a bad gerber, but they do exist + if amgroup.position: + position = amgroup.position + else: + position = (0, 0) + + offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1]) + if amgroup.equivalent(macro[1], offset): + break + macro = None + + # Did we find one in the group0 + if not macro: + # This is a new macro, so define it + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + # Create the statements + # TODO + amrenderer = AMGroupContext() + statement = amrenderer.render(amgroup, hash) + + self.header.append(statement) + + aperdef = ADParamStmt.macro(dcode, hash) + self.header.append(aperdef) + + # Store the dcode and the original so we can check if it really is the same + # If it didn't have a postition, set it to 0, 0 + if amgroup.position == None: + amgroup.position = (0, 0) + macro = (aperdef, amgroup) + + if macroinfo: + macroinfo.append(macro) + else: + self._macros[hash] = [macro] + + return macro[0] + + def _render_amgroup(self, amgroup, color): + + aper = self._get_amacro(amgroup) + self._render_flash(amgroup, aper) + + def _render_inverted_layer(self): + pass + + def new_render_layer(self): + # TODO Might need to implement this + pass + + def flatten(self): + # TODO Might need to implement this + pass + + def dump(self): + """Write the rendered file to a StringIO steam""" + statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements) + stream = StringIO() + for statement in statements: + stream.write(statement + '\n') + + return stream diff --git a/gerbonara/gerber/render/theme.py b/gerbonara/gerber/render/theme.py new file mode 100644 index 0000000..2f558a1 --- /dev/null +++ b/gerbonara/gerber/render/theme.py @@ -0,0 +1,112 @@ +#! /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 RenderSettings + +COLORS = { + 'black': (0.0, 0.0, 0.0), + 'white': (1.0, 1.0, 1.0), + 'red': (1.0, 0.0, 0.0), + 'green': (0.0, 1.0, 0.0), + 'yellow': (1.0, 1.0, 0), + 'blue': (0.0, 0.0, 1.0), + 'fr-4': (0.290, 0.345, 0.0), + 'green soldermask': (0.0, 0.412, 0.278), + 'blue soldermask': (0.059, 0.478, 0.651), + 'red soldermask': (0.968, 0.169, 0.165), + 'black soldermask': (0.298, 0.275, 0.282), + 'purple soldermask': (0.2, 0.0, 0.334), + 'enig copper': (0.694, 0.533, 0.514), + 'hasl copper': (0.871, 0.851, 0.839) +} + + +SPECTRUM = [ + (0.804, 0.216, 0), + (0.78, 0.776, 0.251), + (0.545, 0.451, 0.333), + (0.545, 0.137, 0.137), + (0.329, 0.545, 0.329), + (0.133, 0.545, 0.133), + (0, 0.525, 0.545), + (0.227, 0.373, 0.804), +] + + +class Theme(object): + + def __init__(self, name=None, **kwargs): + self.name = 'Default' if name is None else name + self.background = kwargs.get('background', RenderSettings(COLORS['fr-4'])) + self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) + self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True)) + self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True)) + self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True)) + self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) + self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True)) + self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) + self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) + self._internal = kwargs.get('internal', [RenderSettings(x) for x in SPECTRUM]) + self._internal_gen = None + + def __getitem__(self, key): + return getattr(self, key) + + @property + def internal(self): + if not self._internal_gen: + self._internal_gen = self._internal_gen_func() + return next(self._internal_gen) + + def _internal_gen_func(self): + for setting in self._internal: + yield setting + + def get(self, key, noneval=None): + val = getattr(self, key, None) + return val if val is not None else noneval + + +THEMES = { + 'default': Theme(), + 'OSH Park': Theme(name='OSH Park', + background=RenderSettings(COLORS['purple soldermask']), + top=RenderSettings(COLORS['enig copper']), + bottom=RenderSettings(COLORS['enig copper'], mirror=True), + topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True), + bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True), + topsilk=RenderSettings(COLORS['white'], alpha=0.8), + bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=True)), + + 'Blue': Theme(name='Blue', + topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), + bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), + + 'Transparent Copper': Theme(name='Transparent', + background=RenderSettings((0.9, 0.9, 0.9)), + top=RenderSettings(COLORS['red'], alpha=0.5), + bottom=RenderSettings(COLORS['blue'], alpha=0.5), + drill=RenderSettings((0.3, 0.3, 0.3))), + + 'Transparent Multilayer': Theme(name='Transparent Multilayer', + background=RenderSettings((0, 0, 0)), + top=RenderSettings(SPECTRUM[0], alpha=0.8), + bottom=RenderSettings(SPECTRUM[-1], alpha=0.8), + drill=RenderSettings((0.3, 0.3, 0.3)), + internal=[RenderSettings(x, alpha=0.5) for x in SPECTRUM[1:-1]]), +} diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py new file mode 100644 index 0000000..afdf45f --- /dev/null +++ b/gerbonara/gerber/rs274x.py @@ -0,0 +1,800 @@ +#! /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 copy +import json +import os +import re +import sys + +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .gerber_statements import * +from .primitives import * +from .cam import CamFile, FileSettings +from .utils import sq_distance + + +def read(filename): + """ Read data from filename and return a GerberFile + + 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) + + +def loads(data, filename=None): + """ Generate a GerberFile object from rs274x data in memory + + Parameters + ---------- + data : string + string containing gerber file contents + + filename : string, optional + string containing the filename of the data source + + Returns + ------- + file : :class:`gerber.rs274x.GerberFile` + A GerberFile created from the specified file. + """ + return GerberParser().parse_raw(data, 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, primitives, apertures, filename=None): + super(GerberFile, self).__init__(statements, settings, primitives, filename) + + self.apertures = apertures + + @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): + min_x = min_y = 1000000 + max_x = max_y = -1000000 + + for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: + if stmt.x is not None: + min_x = min(stmt.x, min_x) + max_x = max(stmt.x, max_x) + + if stmt.y is not None: + min_y = min(stmt.y, min_y) + max_y = max(stmt.y, max_y) + + return ((min_x, max_x), (min_y, max_y)) + + @property + def bounding_box(self): + min_x = min_y = 1000000 + max_x = max_y = -1000000 + + for prim in self.primitives: + bounds = prim.bounding_box + min_x = min(bounds[0][0], min_x) + max_x = max(bounds[0][1], max_x) + + min_y = min(bounds[1][0], min_y) + max_y = max(bounds[1][1], max_y) + + return ((min_x, max_x), (min_y, max_y)) + + def write(self, filename, settings=None): + """ Write data out to a gerber file. + """ + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_gerber(settings or self.settings)) + f.write("\n") + + def to_inch(self): + if self.units != 'inch': + self.units = 'inch' + for statement in self.statements: + statement.to_inch() + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + if self.units != 'metric': + self.units = 'metric' + for statement in self.statements: + statement.to_metric() + for primitive in self.primitives: + primitive.to_metric() + + def offset(self, x_offset=0, y_offset=0): + for statement in self.statements: + statement.offset(x_offset, y_offset) + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) + + +class GerberParser(object): + """ GerberParser + """ + NUMBER = r"[\+-]?\d+" + DECIMAL = r"[\+-]?\d+([.]?\d+)?" + STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" + NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+" + + FS = r"(?PFS)(?P(L|T|D))?(?P(A|I))[NG0-9]*X(?P[0-7][0-7])Y(?P[0-7][0-7])[DM0-9]*" + MO = r"(?PMO)(?P(MM|IN))" + 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) + # Include File + IF = r"(?PIF)(?P.*)" + + + # begin deprecated + AS = r"(?PAS)(?P(AXBY)|(AYBX))" + IN = r"(?PIN)(?P.*)" + IP = r"(?PIP)(?P(POS|NEG))" + IR = r"(?PIR)(?P{number})".format(number=NUMBER) + MI = r"(?PMI)(A(?P0|1))?(B(?P0|1))?" + OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + SF = r"(?PSF)(?P.*)" + LN = r"(?PLN)(?P.*)" + DEPRECATED_UNIT = re.compile(r'(?PG7[01])\*') + DEPRECATED_FORMAT = re.compile(r'(?PG9[01])\*') + # end deprecated + + PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, + AD_MACRO, AM, AS, IF, IN, IP, IR, MI, OF, SF, LN) + + PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] + + COORD_FUNCTION = r"G0?[123]" + COORD_OP = r"D0?[123]" + + COORD_STMT = re.compile(( + r"(?P{function})?" + r"(X(?P{number}))?(Y(?P{number}))?" + r"(I(?P{number}))?(J(?P{number}))?" + r"(?P{op})?\*".format(number=NUMBER, function=COORD_FUNCTION, op=COORD_OP))) + + APERTURE_STMT = re.compile(r"(?P(G54)|(G55))?D(?P\d+)\*") + + COMMENT_STMT = re.compile(r"G0?4(?P[^*]*)(\*)?") + + EOF_STMT = re.compile(r"(?PM[0]?[012])\*") + + REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') + QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + + # Keep include loop from crashing us + INCLUDE_FILE_RECURSION_LIMIT = 10 + + def __init__(self): + self.filename = None + self.settings = FileSettings() + self.statements = [] + self.primitives = [] + self.apertures = {} + self.macros = {} + self.current_region = None + self.x = 0 + self.y = 0 + self.op = "D02" + self.aperture = 0 + self.interpolation = 'linear' + self.direction = 'clockwise' + 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) + self._recursion_depth = 0 + + def parse(self, filename): + self.filename = filename + with open(filename, "rU") as fp: + data = fp.read() + return self.parse_raw(data, filename) + + def parse_raw(self, data, filename=None): + self.filename = filename + for stmt in self._parse(self._split_commands(data)): + self.evaluate(stmt) + self.statements.append(stmt) + + # Initialize statement units + for stmt in self.statements: + stmt.units = self.settings.units + + return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename) + + def _split_commands(self, data): + """ + Split the data into commands. Commands end with * (and also newline to help with some badly formatted files) + """ + + length = len(data) + start = 0 + in_header = True + + for cur in range(0, length): + + val = data[cur] + + if val == '%' and start == cur: + in_header = True + continue + + if val == '\r' or val == '\n': + if start != cur: + yield data[start:cur] + start = cur + 1 + + elif not in_header and val == '*': + yield data[start:cur + 1] + start = cur + 1 + + elif in_header and val == '%': + yield data[start:cur + 1] + start = cur + 1 + in_header = False + + def dump_json(self): + stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} + return json.dumps(stmts) + + def dump_str(self): + string = "" + for stmt in self.statements: + string += str(stmt) + "\n" + return string + + def _parse(self, data): + oldline = '' + + for line in 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("%") and not "%" in line[1:]: + oldline = line + continue + + did_something = True # make sure we do at least one loop + while did_something and len(line) > 0: + did_something = False + + # consume empty data blocks + if line[0] == '*': + line = line[1:] + did_something = True + continue + + # coord + (coord, r) = _match_one(self.COORD_STMT, line) + if coord: + yield CoordStmt.from_dict(coord, self.settings) + line = r + did_something = True + continue + + # aperture selection + (aperture, r) = _match_one(self.APERTURE_STMT, line) + if aperture: + yield ApertureStmt(**aperture) + did_something = True + line = r + continue + + # parameter + (param, r) = _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"] == "LP": + yield LPParamStmt.from_dict(param) + elif param["param"] == "AD": + yield ADParamStmt.from_dict(param) + elif param["param"] == "AM": + stmt = AMParamStmt.from_dict(param) + stmt.units = self.settings.units + yield stmt + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "IF": + # Don't crash on include loop + if self._recursion_depth < self.INCLUDE_FILE_RECURSION_LIMIT: + self._recursion_depth += 1 + with open(os.path.join(os.path.dirname(self.filename), param["filename"]), 'r') as f: + inc_data = f.read() + for stmt in self._parse(self._split_commands(inc_data)): + yield stmt + self._recursion_depth -= 1 + else: + raise IOError("Include file nesting depth limit exceeded.") + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmt.from_dict(param) + # deprecated commands AS, IN, IP, IR, MI, OF, SF, LN + elif param["param"] == "AS": + yield ASParamStmt.from_dict(param) + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "IP": + yield IPParamStmt.from_dict(param) + elif param["param"] == "IR": + yield IRParamStmt.from_dict(param) + elif param["param"] == "MI": + yield MIParamStmt.from_dict(param) + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "SF": + yield SFParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmt.from_dict(param) + else: + yield UnknownStmt(line) + + did_something = True + line = r + continue + + # Region Mode + (mode, r) = _match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = _match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # comment + (comment, r) = _match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + did_something = True + line = r + continue + + # deprecated codes + (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) + if deprecated_unit: + stmt = MOParamStmt(param="MO", mo="inch" if "G70" in + deprecated_unit["mode"] else "metric") + self.settings.units = stmt.mode + yield stmt + line = r + did_something = True + continue + + (deprecated_format, r) = _match_one(self.DEPRECATED_FORMAT, line) + if deprecated_format: + yield DeprecatedStmt.from_gerber(line) + line = r + did_something = True + continue + + # eof + (eof, r) = _match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + did_something = True + line = r + continue + + if line.find('*') > 0: + yield UnknownStmt(line) + did_something = True + line = "" + continue + + oldline = line + + def evaluate(self, stmt): + """ Evaluate Gerber statement and update image accordingly. + + 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, CoordStmt): + self._evaluate_coord(stmt) + + elif isinstance(stmt, ParamStmt): + self._evaluate_param(stmt) + + elif isinstance(stmt, ApertureStmt): + self._evaluate_aperture(stmt) + + elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): + self._evaluate_mode(stmt) + + elif isinstance(stmt, (CommentStmt, UnknownStmt, DeprecatedStmt, EofStmt)): + return + + else: + raise Exception("Invalid statement to evaluate") + + def _define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + diameter = modifiers[0][0] + + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 2: + hole_diameter = modifiers[0][1] + elif len(modifiers[0]) == 3: + rectangular_hole = modifiers[0][1:3] + + aperture = Circle(position=None, diameter=diameter, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) + + elif shape == 'R': + width = modifiers[0][0] + height = modifiers[0][1] + + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 3: + hole_diameter = modifiers[0][2] + elif len(modifiers[0]) == 4: + rectangular_hole = modifiers[0][2:4] + + aperture = Rectangle(position=None, width=width, height=height, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) + elif shape == 'O': + width = modifiers[0][0] + height = modifiers[0][1] + + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 3: + hole_diameter = modifiers[0][2] + elif len(modifiers[0]) == 4: + rectangular_hole = modifiers[0][2:4] + + aperture = Obround(position=None, width=width, height=height, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) + elif shape == 'P': + outer_diameter = modifiers[0][0] + number_vertices = int(modifiers[0][1]) + if len(modifiers[0]) > 2: + rotation = modifiers[0][2] + else: + rotation = 0 + + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 4: + hole_diameter = modifiers[0][3] + elif len(modifiers[0]) >= 5: + rectangular_hole = modifiers[0][3:5] + + aperture = Polygon(position=None, sides=number_vertices, + radius=outer_diameter/2.0, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + rotation=rotation) + else: + aperture = self.macros[shape].build(modifiers) + + aperture.units = self.settings.units + self.apertures[d] = aperture + + def _evaluate_mode(self, stmt): + if stmt.type == 'RegionMode': + if self.region_mode == 'on' and stmt.mode == 'off': + # Sometimes we have regions that have no points. Skip those + if self.current_region: + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity, units=self.settings.units)) + + self.current_region = None + self.region_mode = stmt.mode + elif stmt.type == 'QuadrantMode': + 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 == "AM": + self.macros[stmt.name] = stmt + 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.only_function: + # Sometimes we get a coordinate statement + # that only sets the function. If so, don't + # try futher otherwise that might draw/flash something + return + + if stmt.op: + self.op = stmt.op + else: + # no implicit op allowed, force here if coord block doesn't have it + stmt.op = self.op + + if self.op == "D01" or self.op == "D1": + start = (self.x, self.y) + end = (x, y) + + if self.interpolation == 'linear': + if self.region_mode == 'off': + self.primitives.append(Line(start, end, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) + else: + # from gerber spec revision J3, Section 4.5, page 55: + # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. + # The current aperture is associated with the region. + # This has no graphical effect, but allows all its attributes to + # be applied to the region. + + if self.current_region is None: + self.current_region = [Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units), ] + else: + self.current_region.append(Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units)) + else: + i = 0 if stmt.i is None else stmt.i + j = 0 if stmt.j is None else stmt.j + center = self._find_center(start, end, (i, j)) + if self.region_mode == 'off': + self.primitives.append(Arc(start, end, center, self.direction, + self.apertures[self.aperture], + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units)) + else: + if self.current_region is None: + self.current_region = [Arc(start, end, center, self.direction, + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units),] + else: + self.current_region.append(Arc(start, end, center, self.direction, + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units)) + # Gerbv seems to reset interpolation mode in regions.. + # TODO: Make sure this is right. + self.interpolation = 'linear' + + elif self.op == "D02" or self.op == "D2": + + if self.region_mode == "on": + # D02 in the middle of a region finishes that region and starts a new one + if self.current_region and len(self.current_region) > 1: + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity, + units=self.settings.units)) + self.current_region = None + + elif self.op == "D03" or self.op == "D3": + primitive = copy.deepcopy(self.apertures[self.aperture]) + + if primitive is not None: + + if not isinstance(primitive, AMParamStmt): + primitive.position = (x, y) + primitive.level_polarity = self.level_polarity + primitive.units = self.settings.units + self.primitives.append(primitive) + else: + # Aperture Macro + for am_prim in primitive.primitives: + renderable = am_prim.to_primitive((x, y), + self.level_polarity, + self.settings.units) + if renderable is not None: + self.primitives.append(renderable) + self.x, self.y = x, y + + def _find_center(self, start, end, offsets): + """ + In single quadrant mode, the offsets are always positive, which means + there are 4 possible centers. The correct center is the only one that + results in an arc with sweep angle of less than or equal to 90 degrees + in the specified direction + """ + two_pi = 2 * math.pi + if self.quadrant_mode == 'single-quadrant': + # The Gerber spec says single quadrant only has one possible center, + # and you can detect it based on the angle. But for real files, this + # seems to work better - there is usually only one option that makes + # sense for the center (since the distance should be the same + # from start and end). We select the center with the least error in + # radius from all the options with a valid sweep angle. + + sqdist_diff_min = sys.maxsize + center = None + for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: + + test_center = (start[0] + offsets[0] * factors[0], + start[1] + offsets[1] * factors[1]) + + # Find angle from center to start and end points + start_angle = math.atan2(*reversed([_start - _center for _start, _center in zip(start, test_center)])) + end_angle = math.atan2(*reversed([_end - _center for _end, _center in zip(end, test_center)])) + + # Clamp angles to 0, 2pi + theta0 = (start_angle + two_pi) % two_pi + theta1 = (end_angle + two_pi) % two_pi + + # Determine sweep angle in the current arc direction + if self.direction == 'counterclockwise': + sweep_angle = abs(theta1 - theta0) + else: + theta0 += two_pi + sweep_angle = abs(theta0 - theta1) % two_pi + + # Calculate the radius error + sqdist_start = sq_distance(start, test_center) + sqdist_end = sq_distance(end, test_center) + sqdist_diff = abs(sqdist_start - sqdist_end) + + # Take the option with the lowest radius error from the set of + # options with a valid sweep angle + # In some rare cases, the sweep angle is numerically (10**-14) above pi/2 + # So it is safer to compare the angles with some tolerance + is_lowest_radius_error = sqdist_diff < sqdist_diff_min + is_valid_sweep_angle = sweep_angle >= 0 and sweep_angle <= math.pi / 2.0 + 1e-6 + if is_lowest_radius_error and is_valid_sweep_angle: + center = test_center + sqdist_diff_min = sqdist_diff + return center + else: + return (start[0] + offsets[0], start[1] + offsets[1]) + + 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) diff --git a/gerbonara/gerber/tests/__init__.py b/gerbonara/gerber/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gerbonara/gerber/tests/golden/example_am_exposure_modifier.png b/gerbonara/gerber/tests/golden/example_am_exposure_modifier.png new file mode 100644 index 0000000..dac951f Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_am_exposure_modifier.png differ diff --git a/gerbonara/gerber/tests/golden/example_coincident_hole.png b/gerbonara/gerber/tests/golden/example_coincident_hole.png new file mode 100644 index 0000000..9855b11 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_coincident_hole.png differ diff --git a/gerbonara/gerber/tests/golden/example_cutin_multiple.png b/gerbonara/gerber/tests/golden/example_cutin_multiple.png new file mode 100644 index 0000000..ebc1191 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_cutin_multiple.png differ diff --git a/gerbonara/gerber/tests/golden/example_flash_circle.png b/gerbonara/gerber/tests/golden/example_flash_circle.png new file mode 100644 index 0000000..0c407f6 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_flash_circle.png differ diff --git a/gerbonara/gerber/tests/golden/example_flash_obround.png b/gerbonara/gerber/tests/golden/example_flash_obround.png new file mode 100644 index 0000000..2fd4dc3 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_flash_obround.png differ diff --git a/gerbonara/gerber/tests/golden/example_flash_polygon.png b/gerbonara/gerber/tests/golden/example_flash_polygon.png new file mode 100644 index 0000000..89a964b Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_flash_polygon.png differ diff --git a/gerbonara/gerber/tests/golden/example_flash_rectangle.png b/gerbonara/gerber/tests/golden/example_flash_rectangle.png new file mode 100644 index 0000000..797e0c3 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_flash_rectangle.png differ diff --git a/gerbonara/gerber/tests/golden/example_fully_coincident.png b/gerbonara/gerber/tests/golden/example_fully_coincident.png new file mode 100644 index 0000000..4e522ff Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_fully_coincident.png differ diff --git a/gerbonara/gerber/tests/golden/example_holes_dont_clear.png b/gerbonara/gerber/tests/golden/example_holes_dont_clear.png new file mode 100644 index 0000000..7efb67b Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_holes_dont_clear.png differ diff --git a/gerbonara/gerber/tests/golden/example_not_overlapping_contour.png b/gerbonara/gerber/tests/golden/example_not_overlapping_contour.png new file mode 100644 index 0000000..4e522ff Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_not_overlapping_contour.png differ diff --git a/gerbonara/gerber/tests/golden/example_not_overlapping_touching.png b/gerbonara/gerber/tests/golden/example_not_overlapping_touching.png new file mode 100644 index 0000000..d485495 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_not_overlapping_touching.png differ diff --git a/gerbonara/gerber/tests/golden/example_overlapping_contour.png b/gerbonara/gerber/tests/golden/example_overlapping_contour.png new file mode 100644 index 0000000..7504311 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_overlapping_contour.png differ diff --git a/gerbonara/gerber/tests/golden/example_overlapping_touching.png b/gerbonara/gerber/tests/golden/example_overlapping_touching.png new file mode 100644 index 0000000..7504311 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_overlapping_touching.png differ diff --git a/gerbonara/gerber/tests/golden/example_simple_contour.png b/gerbonara/gerber/tests/golden/example_simple_contour.png new file mode 100644 index 0000000..564ae14 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_simple_contour.png differ diff --git a/gerbonara/gerber/tests/golden/example_single_contour.png b/gerbonara/gerber/tests/golden/example_single_contour.png new file mode 100644 index 0000000..3341638 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_single_contour.png differ diff --git a/gerbonara/gerber/tests/golden/example_single_contour_3.png b/gerbonara/gerber/tests/golden/example_single_contour_3.png new file mode 100644 index 0000000..1eecfee Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_single_contour_3.png differ diff --git a/gerbonara/gerber/tests/golden/example_single_quadrant.gbr b/gerbonara/gerber/tests/golden/example_single_quadrant.gbr new file mode 100644 index 0000000..b0a3166 --- /dev/null +++ b/gerbonara/gerber/tests/golden/example_single_quadrant.gbr @@ -0,0 +1,16 @@ +%FSLAX23Y23*% +%MOIN*% +%ADD10C,0.01*% +G74* +D10* +%LPD*% +G01X1100Y600D02* +G03X700Y1000I-400J0D01* +G03X300Y600I0J-400D01* +G03X700Y200I400J0D01* +G03X1100Y600I0J400D01* +G01X300D02* +X1100D01* +X700Y200D02* +Y1000D01* +M02* diff --git a/gerbonara/gerber/tests/golden/example_single_quadrant.png b/gerbonara/gerber/tests/golden/example_single_quadrant.png new file mode 100644 index 0000000..89b763f Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_single_quadrant.png differ diff --git a/gerbonara/gerber/tests/golden/example_two_square_boxes.gbr b/gerbonara/gerber/tests/golden/example_two_square_boxes.gbr new file mode 100644 index 0000000..b5c60d1 --- /dev/null +++ b/gerbonara/gerber/tests/golden/example_two_square_boxes.gbr @@ -0,0 +1,16 @@ +%FSLAX25Y25*% +%MOMM*% +%ADD10C,0.01*% +D10* +%LPD*% +G01X0Y0D02* +X500000D01* +Y500000D01* +X0D01* +Y0D01* +X600000D02* +X1100000D01* +Y500000D01* +X600000D01* +Y0D01* +M02* diff --git a/gerbonara/gerber/tests/golden/example_two_square_boxes.png b/gerbonara/gerber/tests/golden/example_two_square_boxes.png new file mode 100644 index 0000000..98d0518 Binary files /dev/null and b/gerbonara/gerber/tests/golden/example_two_square_boxes.png differ diff --git a/gerbonara/gerber/tests/resources/board_outline.GKO b/gerbonara/gerber/tests/resources/board_outline.GKO new file mode 100644 index 0000000..40b8c7d --- /dev/null +++ b/gerbonara/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/gerbonara/gerber/tests/resources/bottom_copper.GBL b/gerbonara/gerber/tests/resources/bottom_copper.GBL new file mode 100644 index 0000000..0d98da3 --- /dev/null +++ b/gerbonara/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/gerbonara/gerber/tests/resources/bottom_mask.GBS b/gerbonara/gerber/tests/resources/bottom_mask.GBS new file mode 100644 index 0000000..b06654f --- /dev/null +++ b/gerbonara/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/gerbonara/gerber/tests/resources/bottom_silk.GBO b/gerbonara/gerber/tests/resources/bottom_silk.GBO new file mode 100644 index 0000000..0e19197 --- /dev/null +++ b/gerbonara/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/gerbonara/gerber/tests/resources/example_am_exposure_modifier.gbr b/gerbonara/gerber/tests/resources/example_am_exposure_modifier.gbr new file mode 100644 index 0000000..5f3f3dd --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_am_exposure_modifier.gbr @@ -0,0 +1,16 @@ +G04 Umaco example for exposure modifier and clearing area* +%FSLAX26Y26*% +%MOIN*% +%AMSQUAREWITHHOLE* +21,0.1,1,1,0,0,0* +1,0,0.5,0,0*% +%ADD10SQUAREWITHHOLE*% +%ADD11C,1*% +G01* +%LPD*% +D11* +X-1000000Y-250000D02* +X1000000Y250000D01* +D10* +X0Y0D03* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_coincident_hole.gbr b/gerbonara/gerber/tests/resources/example_coincident_hole.gbr new file mode 100644 index 0000000..4f896ea --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_coincident_hole.gbr @@ -0,0 +1,24 @@ +G04 ex2: overlapping* +%FSLAX24Y24*% +%MOMM*% +%SRX1Y1I0.000J0.000*% +%ADD10C,1.00000*% +G01* +%LPD*% +G36* +X0Y50000D02* +Y100000D01* +X100000D01* +Y0D01* +X0D01* +Y50000D01* +G04 first fully coincident linear segment* +X10000D01* +X50000Y10000D01* +X90000Y50000D01* +X50000Y90000D01* +X10000Y50000D01* +G04 second fully coincident linear segment* +X0D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_cutin.gbr b/gerbonara/gerber/tests/resources/example_cutin.gbr new file mode 100644 index 0000000..365e5e1 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_cutin.gbr @@ -0,0 +1,18 @@ +G04 Umaco uut-in example* +%FSLAX24Y24*% +G75* +G36* +X20000Y100000D02* +G01* +X120000D01* +Y20000D01* +X20000D01* +Y60000D01* +X50000D01* +G03* +X50000Y60000I30000J0D01* +G01* +X20000D01* +Y100000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_cutin_multiple.gbr b/gerbonara/gerber/tests/resources/example_cutin_multiple.gbr new file mode 100644 index 0000000..8e19429 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_cutin_multiple.gbr @@ -0,0 +1,28 @@ +G04 multiple cutins* +%FSLAX24Y24*% +%MOMM*% +%SRX1Y1I0.000J0.000*% +%ADD10C,1.00000*% +%LPD*% +G36* +X1220000Y2570000D02* +G01* +Y2720000D01* +X1310000D01* +Y2570000D01* +X1250000D01* +Y2600000D01* +X1290000D01* +Y2640000D01* +X1250000D01* +Y2670000D01* +X1290000D01* +Y2700000D01* +X1250000D01* +Y2670000D01* +Y2640000D01* +Y2600000D01* +Y2570000D01* +X1220000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_flash_circle.gbr b/gerbonara/gerber/tests/resources/example_flash_circle.gbr new file mode 100644 index 0000000..20b2566 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_flash_circle.gbr @@ -0,0 +1,10 @@ +G04 Flashes of circular apertures* +%FSLAX24Y24*% +%MOMM*% +%ADD10C,0.5*% +%ADD11C,0.5X0.25*% +D10* +X000000Y000000D03* +D11* +X010000D03* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_flash_obround.gbr b/gerbonara/gerber/tests/resources/example_flash_obround.gbr new file mode 100644 index 0000000..5313f82 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_flash_obround.gbr @@ -0,0 +1,10 @@ +G04 Flashes of rectangular apertures* +%FSLAX24Y24*% +%MOMM*% +%ADD10O,0.46X0.26*% +%ADD11O,0.46X0.26X0.19*% +D10* +X000000Y000000D03* +D11* +X010000D03* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_flash_polygon.gbr b/gerbonara/gerber/tests/resources/example_flash_polygon.gbr new file mode 100644 index 0000000..177cf9b --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_flash_polygon.gbr @@ -0,0 +1,10 @@ +G04 Flashes of rectangular apertures* +%FSLAX24Y24*% +%MOMM*% +%ADD10P,.40X6*% +%ADD11P,.40X6X0.0X0.19*% +D10* +X000000Y000000D03* +D11* +X010000D03* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_flash_rectangle.gbr b/gerbonara/gerber/tests/resources/example_flash_rectangle.gbr new file mode 100644 index 0000000..8fde812 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_flash_rectangle.gbr @@ -0,0 +1,10 @@ +G04 Flashes of rectangular apertures* +%FSLAX24Y24*% +%MOMM*% +%ADD10R,0.44X0.25*% +%ADD11R,0.44X0.25X0.19*% +D10* +X000000Y000000D03* +D11* +X010000D03* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_fully_coincident.gbr b/gerbonara/gerber/tests/resources/example_fully_coincident.gbr new file mode 100644 index 0000000..3764128 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_fully_coincident.gbr @@ -0,0 +1,23 @@ +G04 ex1: non overlapping* +%FSLAX24Y24*% +%MOMM*% +%ADD10C,1.00000*% +G01* +%LPD*% +G36* +X0Y50000D02* +Y100000D01* +X100000D01* +Y0D01* +X0D01* +Y50000D01* +G04 first fully coincident linear segment* +X-10000D01* +X-50000Y10000D01* +X-90000Y50000D01* +X-50000Y90000D01* +X-10000Y50000D01* +G04 second fully coincident linear segment* +X0D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_guess_by_content.g0 b/gerbonara/gerber/tests/resources/example_guess_by_content.g0 new file mode 100644 index 0000000..5b26afe --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_guess_by_content.g0 @@ -0,0 +1,166 @@ +G04 ULTIpost, Date: Nov. 01, 2017 09:40 * +G04 Design file: C:\example_guess_by_content.g0 * +G04 Layer name: Bottom * +G04 Scale: 100 percent, Rotated: Yes, Reflected: No * +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/gerbonara/gerber/tests/resources/example_holes_dont_clear.gbr b/gerbonara/gerber/tests/resources/example_holes_dont_clear.gbr new file mode 100644 index 0000000..deeebd0 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_holes_dont_clear.gbr @@ -0,0 +1,13 @@ +G04 Demonstrates that apertures with holes do not clear the area - only the aperture hole* +%FSLAX26Y26*% +%MOIN*% +%ADD10C,1X0.5*% +%ADD11C,0.1*% +G01* +%LPD*% +D11* +X-1000000Y-250000D02* +X1000000Y250000D01* +D10* +X0Y0D03* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_level_holes.gbr b/gerbonara/gerber/tests/resources/example_level_holes.gbr new file mode 100644 index 0000000..1b4e189 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_level_holes.gbr @@ -0,0 +1,39 @@ +G04 This file illustrates how to use levels to create holes* +%FSLAX25Y25*% +%MOMM*% +G01* +G04 First level: big square - dark polarity* +%LPD*% +G36* +X250000Y250000D02* +X1750000D01* +Y1750000D01* +X250000D01* +Y250000D01* +G37* +G04 Second level: big circle - clear polarity* +%LPC*% +G36* +G75* +X500000Y1000000D02* +G03* +X500000Y1000000I500000J0D01* +G37* +G04 Third level: small square - dark polarity* +%LPD*% +G36* +X750000Y750000D02* +X1250000D01* +Y1250000D01* +X750000D01* +Y750000D01* +G37* +G04 Fourth level: small circle - clear polarity* +%LPC*% +G36* +G75* +X1150000Y1000000D02* +G03* +X1150000Y1000000I250000J0D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_not_overlapping_contour.gbr b/gerbonara/gerber/tests/resources/example_not_overlapping_contour.gbr new file mode 100644 index 0000000..e3ea631 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_not_overlapping_contour.gbr @@ -0,0 +1,20 @@ +G04 Non-overlapping contours* +%FSLAX24Y24*% +%MOMM*% +%ADD10C,1.00000*% +G01* +%LPD*% +G36* +X0Y50000D02* +Y100000D01* +X100000D01* +Y0D01* +X0D01* +Y50000D01* +X-10000D02* +X-50000Y10000D01* +X-90000Y50000D01* +X-50000Y90000D01* +X-10000Y50000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_not_overlapping_touching.gbr b/gerbonara/gerber/tests/resources/example_not_overlapping_touching.gbr new file mode 100644 index 0000000..3b9b955 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_not_overlapping_touching.gbr @@ -0,0 +1,20 @@ +G04 Non-overlapping and touching* +%FSLAX24Y24*% +%MOMM*% +%ADD10C,1.00000*% +G01* +%LPD*% +G36* +X0Y50000D02* +Y100000D01* +X100000D01* +Y0D01* +X0D01* +Y50000D01* +D02* +X-50000Y10000D01* +X-90000Y50000D01* +X-50000Y90000D01* +X0Y50000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_overlapping_contour.gbr b/gerbonara/gerber/tests/resources/example_overlapping_contour.gbr new file mode 100644 index 0000000..74886a2 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_overlapping_contour.gbr @@ -0,0 +1,20 @@ +G04 Overlapping contours* +%FSLAX24Y24*% +%MOMM*% +%ADD10C,1.00000*% +G01* +%LPD*% +G36* +X0Y50000D02* +Y100000D01* +X100000D01* +Y0D01* +X0D01* +Y50000D01* +X10000D02* +X50000Y10000D01* +X90000Y50000D01* +X50000Y90000D01* +X10000Y50000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_overlapping_touching.gbr b/gerbonara/gerber/tests/resources/example_overlapping_touching.gbr new file mode 100644 index 0000000..27fce15 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_overlapping_touching.gbr @@ -0,0 +1,20 @@ +G04 Overlapping and touching* +%FSLAX24Y24*% +%MOMM*% +%ADD10C,1.00000*% +G01* +%LPD*% +G36* +X0Y50000D02* +Y100000D01* +X100000D01* +Y0D01* +X0D01* +Y50000D01* +D02* +X50000Y10000D01* +X90000Y50000D01* +X50000Y90000D01* +X0Y50000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_simple_contour.gbr b/gerbonara/gerber/tests/resources/example_simple_contour.gbr new file mode 100644 index 0000000..d851760 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_simple_contour.gbr @@ -0,0 +1,16 @@ +G04 Ucamco ex. 4.6.4: Simple contour* +%FSLAX25Y25*% +%MOIN*% +%ADD10C,0.010*% +G36* +X200000Y300000D02* +G01* +X700000D01* +Y100000D01* +X1100000Y500000D01* +X700000Y900000D01* +Y700000D01* +X200000D01* +Y300000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_single_contour_1.gbr b/gerbonara/gerber/tests/resources/example_single_contour_1.gbr new file mode 100644 index 0000000..e9f9a75 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_single_contour_1.gbr @@ -0,0 +1,15 @@ +G04 Ucamco ex. 4.6.5: Single contour #1* +%FSLAX25Y25*% +%MOMM*% +%ADD11C,0.01*% +G01* +D11* +X3000Y5000D01* +G36* +X50000Y50000D02* +X60000D01* +Y60000D01* +X50000D01* +Y50000Y50000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_single_contour_2.gbr b/gerbonara/gerber/tests/resources/example_single_contour_2.gbr new file mode 100644 index 0000000..085c72c --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_single_contour_2.gbr @@ -0,0 +1,15 @@ +G04 Ucamco ex. 4.6.5: Single contour #2* +%FSLAX25Y25*% +%MOMM*% +%ADD11C,0.01*% +G01* +D11* +X3000Y5000D01* +X50000Y50000D02* +G36* +X60000D01* +Y60000D01* +X50000D01* +Y50000Y50000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_single_contour_3.gbr b/gerbonara/gerber/tests/resources/example_single_contour_3.gbr new file mode 100644 index 0000000..40de149 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_single_contour_3.gbr @@ -0,0 +1,15 @@ +G04 Ucamco ex. 4.6.5: Single contour #2* +%FSLAX25Y25*% +%MOMM*% +%ADD11C,0.01*% +G01* +D11* +X3000Y5000D01* +X50000Y50000D01* +G36* +X60000D01* +Y60000D01* +X50000D01* +Y50000Y50000D01* +G37* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_single_quadrant.gbr b/gerbonara/gerber/tests/resources/example_single_quadrant.gbr new file mode 100644 index 0000000..c398601 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_single_quadrant.gbr @@ -0,0 +1,18 @@ +G04 Ucamco ex. 4.5.8: Single quadrant* +%FSLAX23Y23*% +%MOIN*% +%ADD10C,0.010*% +G74* +D10* +X1100Y600D02* +G03* +X700Y1000I400J0D01* +X300Y600I0J400D01* +X700Y200I400J0D01* +X1100Y600I0J400D01* +X300D02* +G01* +X1100D01* +X700Y200D02* +Y1000D01* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/example_two_square_boxes.gbr b/gerbonara/gerber/tests/resources/example_two_square_boxes.gbr new file mode 100644 index 0000000..54a8ac1 --- /dev/null +++ b/gerbonara/gerber/tests/resources/example_two_square_boxes.gbr @@ -0,0 +1,19 @@ +G04 Ucamco ex. 1: Two square boxes* +%FSLAX25Y25*% +%MOMM*% +%TF.Part,Other*% +%LPD*% +%ADD10C,0.010*% +D10* +X0Y0D02* +G01* +X500000Y0D01* +Y500000D01* +X0D01* +Y0D01* +X600000D02* +X1100000D01* +Y500000D01* +X600000D01* +Y0D01* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/ipc-d-356.ipc b/gerbonara/gerber/tests/resources/ipc-d-356.ipc new file mode 100644 index 0000000..2ed3f49 --- /dev/null +++ b/gerbonara/gerber/tests/resources/ipc-d-356.ipc @@ -0,0 +1,115 @@ +C IPC-D-356 generated by EAGLE Version 7.1.0 Copyright (c) 1988-2014 CadSoft +C Database /Some/Path/To/File +C +P JOB EAGLE 7.1 NETLIST, DATE: 2/20/15 12:00 AM +P UNITS CUST 0 +P DIM N +P NNAME1 A_REALLY_LONG_NET_NAME +317GND VIA D 24PA00X 14900Y 1450X 396Y 396 +317GND VIA D 24PA00X 3850Y 8500X 396Y 396 +317GND VIA D 24PA00X 6200Y 10650X 396Y 396 +317GND VIA D 24PA00X 8950Y 1000X 396Y 396 +317GND VIA D 24PA00X 11800Y 2250X 396Y 396 +317GND VIA D 24PA00X 15350Y 3200X 396Y 396 +317GND VIA D 24PA00X 13200Y 3800X 396Y 396 +317GND VIA D 24PA00X 9700Y 12050X 396Y 396 +317GND VIA D 24PA00X 13950Y 11900X 396Y 396 +317GND VIA D 24PA00X 13050Y 7050X 396Y 396 +317GND VIA D 24PA00X 13000Y 8400X 396Y 396 +317N$3 VIA D 24PA00X 11350Y 10100X 396Y 396 +317N$3 VIA D 24PA00X 13250Y 5700X 396Y 396 +317VCC VIA D 24PA00X 15550Y 6850X 396Y 396 +327N$3 C1 -+ A01X 9700Y 10402X1575Y 630R270 +327GND C1 -- A01X 9700Y 13198X1575Y 630R270 +327VCC C2 -+ A01X 13950Y 9677X1535Y 630R270 +327GND C2 -- A01X 13950Y 13023X1535Y 630R270 +327VCC C3 -1 A01X 3850Y 9924X 512Y 591R270 +327GND C3 -2 A01X 3850Y 9176X 512Y 591R270 +327VCC C4 -1 A01X 10374Y 1000X 512Y 591R180 +327GND C4 -2 A01X 9626Y 1000X 512Y 591R180 +327VCC C5 -1 A01X 14700Y 3924X 512Y 591R270 +327GND C5 -2 A01X 14700Y 3176X 512Y 591R270 +317DMX+ DMX -1 D 40PA00X 5050Y 13900X 600Y1200R 90 +317DMX- DMX -2 D 40PA00X 6050Y 13900X 600Y1200R 90 +317GND DMX -3 D 40PA00X 7050Y 13900X 600Y1200R 90 +317PIC_MCLR J1 -1 D 35PA00X 16900Y 6400X 554Y 554R 90 +317VCC J1 -2 D 35PA00X 17900Y 6900X 554Y 554R 90 +317GND J1 -3 D 35PA00X 16900Y 7400X 554Y 554R 90 +317PIC_PGD J1 -4 D 35PA00X 17900Y 7900X 554Y 554R 90 +317PIC_PGC J1 -5 D 35PA00X 16900Y 8400X 554Y 554R 90 +317 J1 -6 D 35PA00X 17900Y 8900X 554Y 554R 90 +327N$4 L1 -1 A01X 13950Y 6382X 748Y1339R 90 +327VCC L1 -2 A01X 13950Y 7918X 748Y1339R 90 +327N$5 LED1 -A A01X 16313Y 1450X 472Y 472R 0 +327GND LED1 -C A01X 15487Y 1450X 472Y 472R 0 +317 MIDI -1 D 40PA00X 1200Y 9500X 600Y1200R 0 +317 MIDI -2 D 40PA00X 1200Y 8500X 600Y1200R 0 +317 MIDI -3 D 40PA00X 1200Y 7500X 600Y1200R 0 +317N$9 MIDI -4 D 40PA00X 1200Y 6500X 600Y1200R 0 +317N$10 MIDI -5 D 40PA00X 1200Y 5500X 600Y1200R 0 +317N$3 PWR -1 D 40PA00X 17050Y 13750X 600Y1200R 90 +317GND PWR -2 D 40PA00X 18050Y 13750X 600Y1200R 90 +327DMX+ R1 -1 A01X 5076Y 11500X 512Y 591R 0 +327DMX- R1 -2 A01X 5824Y 11500X 512Y 591R 0 +327VCC R2 -1 A01X 14376Y 5300X 512Y 591R 0 +327PIC_MCLR R2 -2 A01X 15124Y 5300X 512Y 591R 0 +327N$9 R3 -1 A01X 3126Y 6500X 512Y 591R 0 +327N$6 R3 -2 A01X 3874Y 6500X 512Y 591R 0 +327PIC_RX R4 -1 A01X 9600Y 2624X 512Y 591R270 +327VCC R4 -2 A01X 9600Y 1876X 512Y 591R270 +327VCC R5 -1 A01X 17974Y 1450X 512Y 591R180 +327N$5 R5 -2 A01X 17226Y 1450X 512Y 591R180 +327N$3 U1 -1 A01X 12330Y 5710X 420Y 850R 90 +327N$4 U1 -2 A01X 12330Y 6380X 420Y 850R 90 +327GND U1 -3 A01X 12330Y 7050X 420Y 850R 90 +327VCC U1 -4 A01X 12330Y 7720X 420Y 850R 90 +327GND U1 -5 A01X 12330Y 8390X 420Y 850R 90 +327 U1 -6 A01X 9050Y 7050X4252Y4098R 90 +327PIC_MCLR U2 -1 A01X 11123Y 4063X 157Y 591R270 +327 U2 -2 A01X 11123Y 3807X 157Y 591R270 +327 U2 -3 A01X 11123Y 3552X 157Y 591R270 +327N$1 U2 -4 A01X 11123Y 3296X 157Y 591R270 +327N$2 U2 -5 A01X 11123Y 3040X 157Y 591R270 +327PIC_RX U2 -6 A01X 11123Y 2784X 157Y 591R270 +327 U2 -7 A01X 11123Y 2528X 157Y 591R270 +327GND U2 -8 A01X 11123Y 2272X 157Y 591R270 +327 U2 -9 A01X 11123Y 2016X 157Y 591R270 +327 U2 -10 A01X 11123Y 1760X 157Y 591R270 +327 U2 -11 A01X 11123Y 1504X 157Y 591R270 +327 U2 -12 A01X 11123Y 1248X 157Y 591R270 +327VCC U2 -13 A01X 11123Y 993X 157Y 591R270 +327 U2 -14 A01X 11123Y 737X 157Y 591R270 +327 U2 -15 A01X 13977Y 737X 157Y 591R270 +327 U2 -16 A01X 13977Y 993X 157Y 591R270 +327 U2 -17 A01X 13977Y 1248X 157Y 591R270 +327 U2 -18 A01X 13977Y 1504X 157Y 591R270 +327 U2 -19 A01X 13977Y 1760X 157Y 591R270 +327 U2 -20 A01X 13977Y 2016X 157Y 591R270 +327PIC_PGD U2 -21 A01X 13977Y 2272X 157Y 591R270 +327PIC_PGC U2 -22 A01X 13977Y 2528X 157Y 591R270 +327 U2 -23 A01X 13977Y 2784X 157Y 591R270 +327 U2 -24 A01X 13977Y 3040X 157Y 591R270 +327 U2 -25 A01X 13977Y 3296X 157Y 591R270 +327 U2 -26 A01X 13977Y 3552X 157Y 591R270 +327GND U2 -27 A01X 13977Y 3807X 157Y 591R270 +327VCC U2 -28 A01X 13977Y 4063X 157Y 591R270 +327N$2 U3 -1 A01X 4700Y 7540X 260Y 800R 0 +327VCC U3 -2 A01X 5200Y 7540X 260Y 800R 0 +327VCC U3 -3 A01X 5700Y 7540X 260Y 800R 0 +327N$1 U3 -4 A01X 6200Y 7540X 260Y 800R 0 +327GND U3 -5 A01X 6200Y 9960X 260Y 800R 0 +327DMX- U3 -6 A01X 5700Y 9960X 260Y 800R 0 +327DMX+ U3 -7 A01X 5200Y 9960X 260Y 800R 0 +327VCC U3 -8 A01X 4700Y 9960X 260Y 800R 0 +327 U4 -1 A01X 4704Y 3850X 394Y 500R 0 +327N$6 U4 -2 A01X 4704Y 2800X 394Y 500R 0 +327N$10 U4 -3 A01X 4704Y 1800X 394Y 500R 0 +327 U4 -4 A01X 4704Y 750X 394Y 500R 0 +327GND U4 -5 A01X 8396Y 750X 394Y 500R 0 +327PIC_RX U4 -6 A01X 8396Y 1800X 394Y 500R 0 +327 U4 -7 A01X 8396Y 2800X 394Y 500R 0 +327VCC U4 -8 A01X 8396Y 3850X 394Y 500R 0 +327NNAME1 NA -69 A01X 8396Y 3850X 394Y 500R 0 +389BOARD_EDGE X0Y0 X22500 Y15000 X0 +089 X1300Y240 +999 diff --git a/gerbonara/gerber/tests/resources/multiline_read.ger b/gerbonara/gerber/tests/resources/multiline_read.ger new file mode 100644 index 0000000..02242e4 --- /dev/null +++ b/gerbonara/gerber/tests/resources/multiline_read.ger @@ -0,0 +1,9 @@ +G75* +G71* +%OFA0B0*% +%FSLAX23Y23*% +%IPPOS*% +%LPD*% +%ADD10C,0.1*% +%LPD*%D10* +M02* \ No newline at end of file diff --git a/gerbonara/gerber/tests/resources/ncdrill.DRD b/gerbonara/gerber/tests/resources/ncdrill.DRD new file mode 100644 index 0000000..ced00ca --- /dev/null +++ b/gerbonara/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/gerbonara/gerber/tests/resources/top_copper.GTL b/gerbonara/gerber/tests/resources/top_copper.GTL new file mode 100644 index 0000000..01c848e --- /dev/null +++ b/gerbonara/gerber/tests/resources/top_copper.GTL @@ -0,0 +1,27 @@ +G75* +%MOIN*% +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +G04This is a comment,:* +%AMOC8*5,1,8,0,0,1.08239,22.5*% +%ADD10C,0.0000*% +%ADD11R,0.0260X0.0800*% +%ADD12R,0.0591X0.0157*% +%ADD13R,0.4098X0.4252*% +%ADD14R,0.0850X0.0420*% +%ADD15R,0.0630X0.1575*% +%ADD16R,0.0591X0.0512*% +%ADD17R,0.0512X0.0591*% +%ADD18R,0.0630X0.1535*% +%ADD19R,0.1339X0.0748*% +%ADD20C,0.0004*% +%ADD21C,0.0554*% +%ADD22R,0.0394X0.0500*% +%ADD23C,0.0600*% +%ADD24R,0.0472X0.0472*% +%ADD25C,0.0160*% +%ADD26C,0.0396*% +%ADD27C,0.0240*% +D10*X000300Y003064D02*X000300Y018064D01*X022800Y018064D01*X022800Y003064D01*X000300Y003064D01*X001720Y005114D02*X001722Y005164D01*X001728Y005214D01*X001738Y005263D01*X001752Y005311D01*X001769Y005358D01*X001790Y005403D01*X001815Y005447D01*X001843Y005488D01*X001875Y005527D01*X001909Y005564D01*X001946Y005598D01*X001986Y005628D01*X002028Y005655D01*X002072Y005679D01*X002118Y005700D01*X002165Y005716D01*X002213Y005729D01*X002263Y005738D01*X002312Y005743D01*X002363Y005744D01*X002413Y005741D01*X002462Y005734D01*X002511Y005723D01*X002559Y005708D01*X002605Y005690D01*X002650Y005668D01*X002693Y005642D01*X002734Y005613D01*X002773Y005581D01*X002809Y005546D01*X002841Y005508D01*X002871Y005468D01*X002898Y005425D01*X002921Y005381D01*X002940Y005335D01*X002956Y005287D01*X002968Y005238D01*X002976Y005189D01*X002980Y005139D01*X002980Y005089D01*X002976Y005039D01*X002968Y004990D01*X002956Y004941D01*X002940Y004893D01*X002921Y004847D01*X002898Y004803D01*X002871Y004760D01*X002841Y004720D01*X002809Y004682D01*X002773Y004647D01*X002734Y004615D01*X002693Y004586D01*X002650Y004560D01*X002605Y004538D01*X002559Y004520D01*X002511Y004505D01*X002462Y004494D01*X002413Y004487D01*X002363Y004484D01*X002312Y004485D01*X002263Y004490D01*X002213Y004499D01*X002165Y004512D01*X002118Y004528D01*X002072Y004549D01*X002028Y004573D01*X001986Y004600D01*X001946Y004630D01*X001909Y004664D01*X001875Y004701D01*X001843Y004740D01*X001815Y004781D01*X001790Y004825D01*X001769Y004870D01*X001752Y004917D01*X001738Y004965D01*X001728Y005014D01*X001722Y005064D01*X001720Y005114D01*X001670Y016064D02*X001672Y016114D01*X001678Y016164D01*X001688Y016213D01*X001702Y016261D01*X001719Y016308D01*X001740Y016353D01*X001765Y016397D01*X001793Y016438D01*X001825Y016477D01*X001859Y016514D01*X001896Y016548D01*X001936Y016578D01*X001978Y016605D01*X002022Y016629D01*X002068Y016650D01*X002115Y016666D01*X002163Y016679D01*X002213Y016688D01*X002262Y016693D01*X002313Y016694D01*X002363Y016691D01*X002412Y016684D01*X002461Y016673D01*X002509Y016658D01*X002555Y016640D01*X002600Y016618D01*X002643Y016592D01*X002684Y016563D01*X002723Y016531D01*X002759Y016496D01*X002791Y016458D01*X002821Y016418D01*X002848Y016375D01*X002871Y016331D01*X002890Y016285D01*X002906Y016237D01*X002918Y016188D01*X002926Y016139D01*X002930Y016089D01*X002930Y016039D01*X002926Y015989D01*X002918Y015940D01*X002906Y015891D01*X002890Y015843D01*X002871Y015797D01*X002848Y015753D01*X002821Y015710D01*X002791Y015670D01*X002759Y015632D01*X002723Y015597D01*X002684Y015565D01*X002643Y015536D01*X002600Y015510D01*X002555Y015488D01*X002509Y015470D01*X002461Y015455D01*X002412Y015444D01*X002363Y015437D01*X002313Y015434D01*X002262Y015435D01*X002213Y015440D01*X002163Y015449D01*X002115Y015462D01*X002068Y015478D01*X002022Y015499D01*X001978Y015523D01*X001936Y015550D01*X001896Y015580D01*X001859Y015614D01*X001825Y015651D01*X001793Y015690D01*X001765Y015731D01*X001740Y015775D01*X001719Y015820D01*X001702Y015867D01*X001688Y015915D01*X001678Y015964D01*X001672Y016014D01*X001670Y016064D01*X020060Y012714D02*X020062Y012764D01*X020068Y012814D01*X020078Y012863D01*X020091Y012912D01*X020109Y012959D01*X020130Y013005D01*X020154Y013048D01*X020182Y013090D01*X020213Y013130D01*X020247Y013167D01*X020284Y013201D01*X020324Y013232D01*X020366Y013260D01*X020409Y013284D01*X020455Y013305D01*X020502Y013323D01*X020551Y013336D01*X020600Y013346D01*X020650Y013352D01*X020700Y013354D01*X020750Y013352D01*X020800Y013346D01*X020849Y013336D01*X020898Y013323D01*X020945Y013305D01*X020991Y013284D01*X021034Y013260D01*X021076Y013232D01*X021116Y013201D01*X021153Y013167D01*X021187Y013130D01*X021218Y013090D01*X021246Y013048D01*X021270Y013005D01*X021291Y012959D01*X021309Y012912D01*X021322Y012863D01*X021332Y012814D01*X021338Y012764D01*X021340Y012714D01*X021338Y012664D01*X021332Y012614D01*X021322Y012565D01*X021309Y012516D01*X021291Y012469D01*X021270Y012423D01*X021246Y012380D01*X021218Y012338D01*X021187Y012298D01*X021153Y012261D01*X021116Y012227D01*X021076Y012196D01*X021034Y012168D01*X020991Y012144D01*X020945Y012123D01*X020898Y012105D01*X020849Y012092D01*X020800Y012082D01*X020750Y012076D01*X020700Y012074D01*X020650Y012076D01*X020600Y012082D01*X020551Y012092D01*X020502Y012105D01*X020455Y012123D01*X020409Y012144D01*X020366Y012168D01*X020324Y012196D01*X020284Y012227D01*X020247Y012261D01*X020213Y012298D01*X020182Y012338D01*X020154Y012380D01*X020130Y012423D01*X020109Y012469D01*X020091Y012516D01*X020078Y012565D01*X020068Y012614D01*X020062Y012664D01*X020060Y012714D01*X020170Y016064D02*X020172Y016114D01*X020178Y016164D01*X020188Y016213D01*X020202Y016261D01*X020219Y016308D01*X020240Y016353D01*X020265Y016397D01*X020293Y016438D01*X020325Y016477D01*X020359Y016514D01*X020396Y016548D01*X020436Y016578D01*X020478Y016605D01*X020522Y016629D01*X020568Y016650D01*X020615Y016666D01*X020663Y016679D01*X020713Y016688D01*X020762Y016693D01*X020813Y016694D01*X020863Y016691D01*X020912Y016684D01*X020961Y016673D01*X021009Y016658D01*X021055Y016640D01*X021100Y016618D01*X021143Y016592D01*X021184Y016563D01*X021223Y016531D01*X021259Y016496D01*X021291Y016458D01*X021321Y016418D01*X021348Y016375D01*X021371Y016331D01*X021390Y016285D01*X021406Y016237D01*X021418Y016188D01*X021426Y016139D01*X021430Y016089D01*X021430Y016039D01*X021426Y015989D01*X021418Y015940D01*X021406Y015891D01*X021390Y015843D01*X021371Y015797D01*X021348Y015753D01*X021321Y015710D01*X021291Y015670D01*X021259Y015632D01*X021223Y015597D01*X021184Y015565D01*X021143Y015536D01*X021100Y015510D01*X021055Y015488D01*X021009Y015470D01*X020961Y015455D01*X020912Y015444D01*X020863Y015437D01*X020813Y015434D01*X020762Y015435D01*X020713Y015440D01*X020663Y015449D01*X020615Y015462D01*X020568Y015478D01*X020522Y015499D01*X020478Y015523D01*X020436Y015550D01*X020396Y015580D01*X020359Y015614D01*X020325Y015651D01*X020293Y015690D01*X020265Y015731D01*X020240Y015775D01*X020219Y015820D01*X020202Y015867D01*X020188Y015915D01*X020178Y015964D01*X020172Y016014D01*X020170Y016064D01*X020060Y008714D02*X020062Y008764D01*X020068Y008814D01*X020078Y008863D01*X020091Y008912D01*X020109Y008959D01*X020130Y009005D01*X020154Y009048D01*X020182Y009090D01*X020213Y009130D01*X020247Y009167D01*X020284Y009201D01*X020324Y009232D01*X020366Y009260D01*X020409Y009284D01*X020455Y009305D01*X020502Y009323D01*X020551Y009336D01*X020600Y009346D01*X020650Y009352D01*X020700Y009354D01*X020750Y009352D01*X020800Y009346D01*X020849Y009336D01*X020898Y009323D01*X020945Y009305D01*X020991Y009284D01*X021034Y009260D01*X021076Y009232D01*X021116Y009201D01*X021153Y009167D01*X021187Y009130D01*X021218Y009090D01*X021246Y009048D01*X021270Y009005D01*X021291Y008959D01*X021309Y008912D01*X021322Y008863D01*X021332Y008814D01*X021338Y008764D01*X021340Y008714D01*X021338Y008664D01*X021332Y008614D01*X021322Y008565D01*X021309Y008516D01*X021291Y008469D01*X021270Y008423D01*X021246Y008380D01*X021218Y008338D01*X021187Y008298D01*X021153Y008261D01*X021116Y008227D01*X021076Y008196D01*X021034Y008168D01*X020991Y008144D01*X020945Y008123D01*X020898Y008105D01*X020849Y008092D01*X020800Y008082D01*X020750Y008076D01*X020700Y008074D01*X020650Y008076D01*X020600Y008082D01*X020551Y008092D01*X020502Y008105D01*X020455Y008123D01*X020409Y008144D01*X020366Y008168D01*X020324Y008196D01*X020284Y008227D01*X020247Y008261D01*X020213Y008298D01*X020182Y008338D01*X020154Y008380D01*X020130Y008423D01*X020109Y008469D01*X020091Y008516D01*X020078Y008565D01*X020068Y008614D01*X020062Y008664D01*X020060Y008714D01*X020170Y005064D02*X020172Y005114D01*X020178Y005164D01*X020188Y005213D01*X020202Y005261D01*X020219Y005308D01*X020240Y005353D01*X020265Y005397D01*X020293Y005438D01*X020325Y005477D01*X020359Y005514D01*X020396Y005548D01*X020436Y005578D01*X020478Y005605D01*X020522Y005629D01*X020568Y005650D01*X020615Y005666D01*X020663Y005679D01*X020713Y005688D01*X020762Y005693D01*X020813Y005694D01*X020863Y005691D01*X020912Y005684D01*X020961Y005673D01*X021009Y005658D01*X021055Y005640D01*X021100Y005618D01*X021143Y005592D01*X021184Y005563D01*X021223Y005531D01*X021259Y005496D01*X021291Y005458D01*X021321Y005418D01*X021348Y005375D01*X021371Y005331D01*X021390Y005285D01*X021406Y005237D01*X021418Y005188D01*X021426Y005139D01*X021430Y005089D01*X021430Y005039D01*X021426Y004989D01*X021418Y004940D01*X021406Y004891D01*X021390Y004843D01*X021371Y004797D01*X021348Y004753D01*X021321Y004710D01*X021291Y004670D01*X021259Y004632D01*X021223Y004597D01*X021184Y004565D01*X021143Y004536D01*X021100Y004510D01*X021055Y004488D01*X021009Y004470D01*X020961Y004455D01*X020912Y004444D01*X020863Y004437D01*X020813Y004434D01*X020762Y004435D01*X020713Y004440D01*X020663Y004449D01*X020615Y004462D01*X020568Y004478D01*X020522Y004499D01*X020478Y004523D01*X020436Y004550D01*X020396Y004580D01*X020359Y004614D01*X020325Y004651D01*X020293Y004690D01*X020265Y004731D01*X020240Y004775D01*X020219Y004820D01*X020202Y004867D01*X020188Y004915D01*X020178Y004964D01*X020172Y005014D01*X020170Y005064D01*D11*X006500Y010604D03*X006000Y010604D03*X005500Y010604D03*X005000Y010604D03*X005000Y013024D03*X005500Y013024D03*X006000Y013024D03*X006500Y013024D03*D12*X011423Y007128D03*X011423Y006872D03*X011423Y006616D03*X011423Y006360D03*X011423Y006104D03*X011423Y005848D03*X011423Y005592D03*X011423Y005336D03*X011423Y005080D03*X011423Y004825D03*X011423Y004569D03*X011423Y004313D03*X011423Y004057D03*X011423Y003801D03*X014277Y003801D03*X014277Y004057D03*X014277Y004313D03*X014277Y004569D03*X014277Y004825D03*X014277Y005080D03*X014277Y005336D03*X014277Y005592D03*X014277Y005848D03*X014277Y006104D03*X014277Y006360D03*X014277Y006616D03*X014277Y006872D03*X014277Y007128D03*D13*X009350Y010114D03*D14*X012630Y010114D03*X012630Y010784D03*X012630Y011454D03*X012630Y009444D03*X012630Y008774D03*D15*X010000Y013467D03*X010000Y016262D03*D16*X004150Y012988D03*X004150Y012240D03*X009900Y005688D03*X009900Y004940D03*X015000Y006240D03*X015000Y006988D03*D17*X014676Y008364D03*X015424Y008364D03*X017526Y004514D03*X018274Y004514D03*X010674Y004064D03*X009926Y004064D03*X004174Y009564D03*X003426Y009564D03*X005376Y014564D03*X006124Y014564D03*D18*X014250Y016088D03*X014250Y012741D03*D19*X014250Y010982D03*X014250Y009447D03*D20*X022869Y007639D02*X022869Y013789D01*D21*X018200Y011964D03*X017200Y011464D03*X017200Y010464D03*X018200Y009964D03*X018200Y010964D03*X017200Y009464D03*D22*X008696Y006914D03*X008696Y005864D03*X008696Y004864D03*X008696Y003814D03*X005004Y003814D03*X005004Y004864D03*X005004Y005864D03*X005004Y006914D03*D23*X001800Y008564D02*X001200Y008564D01*X001200Y009564D02*X001800Y009564D01*X001800Y010564D02*X001200Y010564D01*X001200Y011564D02*X001800Y011564D01*X001800Y012564D02*X001200Y012564D01*X005350Y016664D02*X005350Y017264D01*X006350Y017264D02*X006350Y016664D01*X007350Y016664D02*X007350Y017264D01*X017350Y017114D02*X017350Y016514D01*X018350Y016514D02*X018350Y017114D01*D24*X016613Y004514D03*X015787Y004514D03*D25*X015200Y004514D01*X014868Y004649D02*X014732Y004649D01*X014842Y004586D02*X014842Y004443D01*X014896Y004311D01*X014997Y004211D01*X015129Y004156D01*X015271Y004156D01*X015395Y004207D01*X015484Y004118D01*X016089Y004118D01*X016183Y004212D01*X016183Y004817D01*X016089Y004911D01*X015484Y004911D01*X015395Y004821D01*X015271Y004872D01*X015129Y004872D01*X014997Y004818D01*X014896Y004717D01*X014842Y004586D01*X014842Y004491D02*X014732Y004491D01*X014732Y004332D02*X014888Y004332D01*X014732Y004174D02*X015086Y004174D01*X015314Y004174D02*X015428Y004174D01*X014732Y004015D02*X019505Y004015D01*X019568Y003922D02*X019568Y003922D01*X019568Y003922D01*X019286Y004335D01*X019286Y004335D01*X019139Y004814D01*X019139Y005315D01*X019286Y005793D01*X019286Y005793D01*X019568Y006207D01*X019568Y006207D01*X019960Y006519D01*X019960Y006519D01*X020426Y006702D01*X020926Y006740D01*X020926Y006740D01*X021414Y006628D01*X021414Y006628D01*X021847Y006378D01*X021847Y006378D01*X022188Y006011D01*X022188Y006011D01*X022320Y005737D01*X022320Y015392D01*X022188Y015118D01*X022188Y015118D01*X021847Y014751D01*X021847Y014751D01*X021414Y014500D01*X021414Y014500D01*X020926Y014389D01*X020926Y014389D01*X020426Y014426D01*X020426Y014426D01*X019960Y014609D01*X019960Y014609D01*X019568Y014922D01*X019568Y014922D01*X019568Y014922D01*X019286Y015335D01*X019286Y015335D01*X019139Y015814D01*X019139Y016315D01*X019286Y016793D01*X019286Y016793D01*X019568Y017207D01*X019568Y017207D01*X019568Y017207D01*X019960Y017519D01*X019960Y017519D01*X020126Y017584D01*X016626Y017584D01*X016637Y017573D01*X016924Y017287D01*X016960Y017375D01*X017089Y017504D01*X017258Y017574D01*X017441Y017574D01*X017611Y017504D01*X017740Y017375D01*X017810Y017206D01*X017810Y016423D01*X017740Y016254D01*X017611Y016124D01*X017441Y016054D01*X017258Y016054D01*X017089Y016124D01*X016960Y016254D01*X016890Y016423D01*X016890Y016557D01*X016841Y016577D01*X016284Y017134D01*X010456Y017134D01*X010475Y017116D01*X010475Y016310D01*X010475Y016310D01*X010495Y016216D01*X010477Y016123D01*X010475Y016120D01*X010475Y015408D01*X010381Y015315D01*X010305Y015315D01*X010358Y015186D01*X010358Y015043D01*X010304Y014911D01*X010203Y014811D01*X010071Y014756D01*X009929Y014756D01*X009797Y014811D01*X009696Y014911D01*X009642Y015043D01*X009642Y015186D01*X009695Y015315D01*X009619Y015315D01*X009525Y015408D01*X009525Y017116D01*X009544Y017134D01*X009416Y017134D01*X009330Y017048D01*X009330Y014080D01*X009525Y013885D01*X009525Y014320D01*X009619Y014414D01*X010381Y014414D01*X010475Y014320D01*X010475Y013747D01*X011403Y013747D01*X011506Y013704D01*X011688Y013522D01*X011721Y013522D01*X011853Y013468D01*X011954Y013367D01*X013755Y013367D01*X013755Y013525D02*X011685Y013525D01*X011526Y013684D02*X013893Y013684D01*X013911Y013689D02*X013866Y013677D01*X013825Y013653D01*X013791Y013619D01*X013767Y013578D01*X013755Y013533D01*X013755Y012819D01*X014173Y012819D01*X014173Y013689D01*X013911Y013689D01*X014173Y013684D02*X014327Y013684D01*X014327Y013689D02*X014327Y012819D01*X014173Y012819D01*X014173Y012664D01*X014327Y012664D01*X014327Y011793D01*X014589Y011793D01*X014634Y011806D01*X014675Y011829D01*X014709Y011863D01*X014733Y011904D01*X014745Y011950D01*X014745Y012664D01*X014327Y012664D01*X014327Y012819D01*X014745Y012819D01*X014745Y013533D01*X014733Y013578D01*X014709Y013619D01*X014675Y013653D01*X014634Y013677D01*X014589Y013689D01*X014327Y013689D01*X014327Y013525D02*X014173Y013525D01*X014173Y013367D02*X014327Y013367D01*X014327Y013208D02*X014173Y013208D01*X014173Y013050D02*X014327Y013050D01*X014327Y012891D02*X014173Y012891D01*X014173Y012733D02*X010475Y012733D01*X010475Y012613D02*X010475Y013187D01*X011232Y013187D01*X011292Y013126D01*X011292Y013093D01*X011346Y012961D01*X011447Y012861D01*X011579Y012806D01*X011721Y012806D01*X011853Y012861D01*X011954Y012961D01*X012008Y013093D01*X012008Y013236D01*X011954Y013367D01*X012008Y013208D02*X013755Y013208D01*X013755Y013050D02*X011990Y013050D01*X011883Y012891D02*X013755Y012891D01*X013755Y012664D02*X013755Y011950D01*X013767Y011904D01*X013791Y011863D01*X013825Y011829D01*X013866Y011806D01*X013911Y011793D01*X014173Y011793D01*X014173Y012664D01*X013755Y012664D01*X013755Y012574D02*X010436Y012574D01*X010475Y012613D02*X010381Y012519D01*X009619Y012519D01*X009525Y012613D01*X009525Y013234D01*X009444Y013234D01*X009341Y013277D01*X009263Y013356D01*X009263Y013356D01*X008813Y013806D01*X008770Y013909D01*X008770Y017220D01*X008813Y017323D01*X009074Y017584D01*X007681Y017584D01*X007740Y017525D01*X007810Y017356D01*X007810Y016573D01*X007740Y016404D01*X007611Y016274D01*X007441Y016204D01*X007258Y016204D01*X007089Y016274D01*X006960Y016404D01*X006890Y016573D01*X006890Y017356D01*X006960Y017525D01*X007019Y017584D01*X006681Y017584D01*X006740Y017525D01*X006810Y017356D01*X006810Y016573D01*X006740Y016404D01*X006611Y016274D01*X006590Y016266D01*X006590Y015367D01*X006553Y015278D01*X006340Y015065D01*X006340Y015020D01*X006446Y015020D01*X006540Y014926D01*X006540Y014203D01*X006446Y014109D01*X006240Y014109D01*X006240Y013961D01*X006297Y014018D01*X006429Y014072D01*X006571Y014072D01*X006703Y014018D01*X006804Y013917D01*X006858Y013786D01*X006858Y013643D01*X006804Y013511D01*X006786Y013494D01*X006790Y013491D01*X006790Y012558D01*X006696Y012464D01*X006304Y012464D01*X006250Y012518D01*X006196Y012464D01*X005804Y012464D01*X005750Y012518D01*X005696Y012464D01*X005304Y012464D01*X005264Y012504D01*X005241Y012480D01*X005199Y012457D01*X005154Y012444D01*X005000Y012444D01*X005000Y013024D01*X005000Y013024D01*X005000Y012444D01*X004846Y012444D01*X004801Y012457D01*X004759Y012480D01*X004726Y012514D01*X004702Y012555D01*X004690Y012601D01*X004690Y013024D01*X005000Y013024D01*X005000Y013024D01*X004964Y012988D01*X004150Y012988D01*X004198Y012940D02*X004198Y013036D01*X004625Y013036D01*X004625Y013268D01*X004613Y013314D01*X004589Y013355D01*X004556Y013388D01*X004515Y013412D01*X004469Y013424D01*X004198Y013424D01*X004198Y013036D01*X004102Y013036D01*X004102Y012940D01*X003675Y012940D01*X003675Y012709D01*X003687Y012663D01*X003711Y012622D01*X003732Y012600D01*X003695Y012562D01*X003695Y011918D01*X003788Y011824D01*X003904Y011824D01*X003846Y011767D01*X003792Y011636D01*X003792Y011493D01*X003846Y011361D01*X003947Y011261D01*X004079Y011206D01*X004221Y011206D01*X004353Y011261D01*X004454Y011361D01*X004508Y011493D01*X004508Y011636D01*X004454Y011767D01*X004396Y011824D01*X004512Y011824D01*X004605Y011918D01*X004605Y012562D01*X004568Y012600D01*X004589Y012622D01*X004613Y012663D01*X004625Y012709D01*X004625Y012940D01*X004198Y012940D01*X004198Y013050D02*X004102Y013050D01*X004102Y013036D02*X004102Y013424D01*X003831Y013424D01*X003785Y013412D01*X003744Y013388D01*X003711Y013355D01*X003687Y013314D01*X003675Y013268D01*X003675Y013036D01*X004102Y013036D01*X004102Y013208D02*X004198Y013208D01*X004198Y013367D02*X004102Y013367D01*X003723Y013367D02*X000780Y013367D01*X000780Y013525D02*X004720Y013525D01*X004726Y013535D02*X004702Y013494D01*X004690Y013448D01*X004690Y013024D01*X005000Y013024D01*X005000Y012264D01*X005750Y011514D01*X005750Y010604D01*X005500Y010604D01*X005500Y010024D01*X005654Y010024D01*X005699Y010037D01*X005741Y010060D01*X005750Y010070D01*X005759Y010060D01*X005801Y010037D01*X005846Y010024D01*X006000Y010024D01*X006154Y010024D01*X006199Y010037D01*X006241Y010060D01*X006260Y010080D01*X006260Y008267D01*X006297Y008178D01*X006364Y008111D01*X006364Y008111D01*X006821Y007654D01*X006149Y007654D01*X005240Y008564D01*X005240Y010080D01*X005259Y010060D01*X005301Y010037D01*X005346Y010024D01*X005500Y010024D01*X005500Y010604D01*X005500Y010604D01*X005500Y010604D01*X005690Y010604D01*X006000Y010604D01*X006000Y010024D01*X006000Y010604D01*X006000Y010604D01*X006000Y010604D01*X005750Y010604D01*X005500Y010604D02*X006000Y010604D01*X006000Y011184D01*X005846Y011184D01*X005801Y011172D01*X005759Y011148D01*X005741Y011148D01*X005699Y011172D01*X005654Y011184D01*X005500Y011184D01*X005346Y011184D01*X005301Y011172D01*X005259Y011148D01*X005213Y011148D01*X005196Y011164D02*X005236Y011125D01*X005259Y011148D01*X005196Y011164D02*X004804Y011164D01*X004710Y011071D01*X004710Y010138D01*X004760Y010088D01*X004760Y009309D01*X004753Y009324D01*X004590Y009488D01*X004590Y009926D01*X004496Y010020D01*X003852Y010020D01*X003800Y009968D01*X003748Y010020D01*X003104Y010020D01*X003010Y009926D01*X003010Y009804D01*X002198Y009804D01*X002190Y009825D01*X002061Y009954D01*X001891Y010024D01*X001108Y010024D01*X000939Y009954D01*X000810Y009825D01*X000780Y009752D01*X000780Y010376D01*X000810Y010304D01*X000939Y010174D01*X001108Y010104D01*X001891Y010104D01*X002061Y010174D01*X002190Y010304D01*X002260Y010473D01*X002260Y010656D01*X002190Y010825D01*X002061Y010954D01*X001891Y011024D01*X001108Y011024D01*X000939Y010954D01*X000810Y010825D01*X000780Y010752D01*X000780Y011376D01*X000810Y011304D01*X000939Y011174D01*X001108Y011104D01*X001891Y011104D01*X002061Y011174D01*X002190Y011304D01*X002260Y011473D01*X002260Y011656D01*X002190Y011825D01*X002061Y011954D01*X001891Y012024D01*X001108Y012024D01*X000939Y011954D01*X000810Y011825D01*X000780Y011752D01*X000780Y012376D01*X000810Y012304D01*X000939Y012174D01*X001108Y012104D01*X001891Y012104D01*X002061Y012174D01*X002190Y012304D01*X002260Y012473D01*X002260Y012656D01*X002190Y012825D01*X002061Y012954D01*X001891Y013024D01*X001108Y013024D01*X000939Y012954D01*X000810Y012825D01*X000780Y012752D01*X000780Y015356D01*X000786Y015335D01*X001068Y014922D01*X001068Y014922D01*X001068Y014922D01*X001460Y014609D01*X001926Y014426D01*X002426Y014389D01*X002914Y014500D01*X003347Y014751D01*X003347Y014751D01*X003688Y015118D01*X003905Y015569D01*X003980Y016064D01*X003905Y016560D01*X003688Y017011D01*X003347Y017378D01*X002990Y017584D01*X005019Y017584D01*X004960Y017525D01*X004890Y017356D01*X004890Y016573D01*X004960Y016404D01*X005089Y016274D01*X005110Y016266D01*X005110Y015020D01*X005054Y015020D01*X004960Y014926D01*X004960Y014203D01*X005054Y014109D01*X005260Y014109D01*X005260Y013549D01*X005241Y013568D01*X005199Y013592D01*X005154Y013604D01*X005000Y013604D01*X004846Y013604D01*X004801Y013592D01*X004759Y013568D01*X004726Y013535D01*X004690Y013367D02*X004577Y013367D01*X004625Y013208D02*X004690Y013208D01*X004690Y013050D02*X004625Y013050D01*X004625Y012891D02*X004690Y012891D01*X004690Y012733D02*X004625Y012733D01*X004593Y012574D02*X004697Y012574D01*X004605Y012416D02*X013755Y012416D01*X013755Y012257D02*X011559Y012257D01*X011559Y012307D02*X011465Y012400D01*X007235Y012400D01*X007141Y012307D01*X007141Y008013D01*X006740Y008414D01*X006740Y010088D01*X006790Y010138D01*X006790Y011071D01*X006696Y011164D01*X006304Y011164D01*X006264Y011125D01*X006241Y011148D01*X006287Y011148D01*X006241Y011148D02*X006199Y011172D01*X006154Y011184D01*X006000Y011184D01*X006000Y010604D01*X006000Y010604D01*X006000Y010672D02*X006000Y010672D01*X006000Y010514D02*X006000Y010514D01*X006000Y010355D02*X006000Y010355D01*X006000Y010197D02*X006000Y010197D01*X006000Y010038D02*X006000Y010038D01*X006202Y010038D02*X006260Y010038D01*X006260Y009880D02*X005240Y009880D01*X005240Y010038D02*X005297Y010038D01*X005500Y010038D02*X005500Y010038D01*X005500Y010197D02*X005500Y010197D01*X005500Y010355D02*X005500Y010355D01*X005500Y010514D02*X005500Y010514D01*X005500Y010604D02*X005500Y011184D01*X005500Y010604D01*X005500Y010604D01*X005500Y010672D02*X005500Y010672D01*X005500Y010831D02*X005500Y010831D01*X005500Y010989D02*X005500Y010989D01*X005500Y011148D02*X005500Y011148D01*X005741Y011148D02*X005750Y011139D01*X005759Y011148D01*X006000Y011148D02*X006000Y011148D01*X006000Y010989D02*X006000Y010989D01*X006000Y010831D02*X006000Y010831D01*X006500Y010604D02*X006500Y008314D01*X007150Y007664D01*X009450Y007664D01*X010750Y006364D01*X011419Y006364D01*X011423Y006360D01*X011377Y006364D01*X011423Y006104D02*X010660Y006104D01*X009350Y007414D01*X006050Y007414D01*X005000Y008464D01*X005000Y010604D01*X004710Y010672D02*X002253Y010672D01*X002260Y010514D02*X004710Y010514D01*X004710Y010355D02*X002211Y010355D01*X002083Y010197D02*X004710Y010197D01*X004760Y010038D02*X000780Y010038D01*X000780Y009880D02*X000865Y009880D01*X000917Y010197D02*X000780Y010197D01*X000780Y010355D02*X000789Y010355D01*X000780Y010831D02*X000816Y010831D01*X000780Y010989D02*X001024Y010989D01*X001003Y011148D02*X000780Y011148D01*X000780Y011306D02*X000809Y011306D01*X000780Y011782D02*X000792Y011782D01*X000780Y011940D02*X000925Y011940D01*X000780Y012099D02*X003695Y012099D01*X003695Y012257D02*X002144Y012257D01*X002236Y012416D02*X003695Y012416D01*X003707Y012574D02*X002260Y012574D01*X002228Y012733D02*X003675Y012733D01*X003675Y012891D02*X002124Y012891D01*X002075Y011940D02*X003695Y011940D01*X003861Y011782D02*X002208Y011782D01*X002260Y011623D02*X003792Y011623D01*X003804Y011465D02*X002257Y011465D01*X002191Y011306D02*X003902Y011306D01*X004150Y011564D02*X004150Y012240D01*X004605Y012257D02*X007141Y012257D01*X007141Y012099D02*X004605Y012099D01*X004605Y011940D02*X007141Y011940D01*X007141Y011782D02*X004439Y011782D01*X004508Y011623D02*X007141Y011623D01*X007141Y011465D02*X004496Y011465D01*X004398Y011306D02*X007141Y011306D01*X007141Y011148D02*X006713Y011148D01*X006790Y010989D02*X007141Y010989D01*X007141Y010831D02*X006790Y010831D01*X006790Y010672D02*X007141Y010672D01*X007141Y010514D02*X006790Y010514D01*X006790Y010355D02*X007141Y010355D01*X007141Y010197D02*X006790Y010197D01*X006740Y010038D02*X007141Y010038D01*X007141Y009880D02*X006740Y009880D01*X006740Y009721D02*X007141Y009721D01*X007141Y009563D02*X006740Y009563D01*X006740Y009404D02*X007141Y009404D01*X007141Y009246D02*X006740Y009246D01*X006740Y009087D02*X007141Y009087D01*X007141Y008929D02*X006740Y008929D01*X006740Y008770D02*X007141Y008770D01*X007141Y008612D02*X006740Y008612D01*X006740Y008453D02*X007141Y008453D01*X007141Y008295D02*X006859Y008295D01*X007017Y008136D02*X007141Y008136D01*X006656Y007819D02*X005984Y007819D01*X005826Y007978D02*X006497Y007978D01*X006339Y008136D02*X005667Y008136D01*X005509Y008295D02*X006260Y008295D01*X006260Y008453D02*X005350Y008453D01*X005240Y008612D02*X006260Y008612D01*X006260Y008770D02*X005240Y008770D01*X005240Y008929D02*X006260Y008929D01*X006260Y009087D02*X005240Y009087D01*X005240Y009246D02*X006260Y009246D01*X006260Y009404D02*X005240Y009404D01*X005240Y009563D02*X006260Y009563D01*X006260Y009721D02*X005240Y009721D01*X004760Y009721D02*X004590Y009721D01*X004590Y009563D02*X004760Y009563D01*X004760Y009404D02*X004673Y009404D01*X004550Y009188D02*X004174Y009564D01*X004590Y009880D02*X004760Y009880D01*X004550Y009188D02*X004550Y006114D01*X004800Y005864D01*X005004Y005864D01*X004647Y005678D02*X004647Y005548D01*X004740Y005454D01*X005267Y005454D01*X005360Y005548D01*X005360Y006181D01*X005267Y006274D01*X004790Y006274D01*X004790Y006504D01*X005267Y006504D01*X005360Y006598D01*X005360Y007231D01*X005267Y007324D01*X004790Y007324D01*X004790Y008344D01*X004797Y008328D01*X005847Y007278D01*X005914Y007211D01*X006002Y007174D01*X008320Y007174D01*X008320Y006933D01*X008678Y006933D01*X008678Y006896D01*X008320Y006896D01*X008320Y006641D01*X008332Y006595D01*X008356Y006554D01*X008389Y006520D01*X008430Y006497D01*X008476Y006484D01*X008678Y006484D01*X008678Y006896D01*X008715Y006896D01*X008715Y006933D01*X009073Y006933D01*X009073Y007174D01*X009251Y007174D01*X010337Y006088D01*X010278Y006088D01*X010262Y006104D01*X009538Y006104D01*X009445Y006011D01*X009445Y005928D01*X009276Y005928D01*X009188Y005892D01*X009064Y005768D01*X009053Y005757D01*X009053Y006181D01*X008960Y006274D01*X008433Y006274D01*X008340Y006181D01*X008340Y005548D01*X008433Y005454D01*X008960Y005454D01*X008960Y005455D01*X008960Y005274D01*X008960Y005274D01*X008433Y005274D01*X008340Y005181D01*X008340Y004548D01*X008433Y004454D01*X008960Y004454D01*X009053Y004548D01*X009053Y004627D01*X009136Y004661D01*X009203Y004728D01*X009403Y004928D01*X009428Y004988D01*X009852Y004988D01*X009852Y004892D01*X009425Y004892D01*X009425Y004661D01*X009437Y004615D01*X009461Y004574D01*X009494Y004540D01*X009535Y004517D01*X009581Y004504D01*X009589Y004504D01*X009510Y004426D01*X009510Y004311D01*X009453Y004368D01*X009321Y004422D01*X009179Y004422D01*X009047Y004368D01*X008984Y004304D01*X008899Y004304D01*X008811Y004268D01*X008767Y004224D01*X008433Y004224D01*X008340Y004131D01*X008340Y003544D01*X005360Y003544D01*X005360Y004131D01*X005267Y004224D01*X004740Y004224D01*X004647Y004131D01*X004647Y003544D01*X002937Y003544D01*X002964Y003550D01*X003397Y003801D01*X003397Y003801D01*X003738Y004168D01*X003955Y004619D01*X004030Y005114D01*X003955Y005610D01*X003738Y006061D01*X003397Y006428D01*X002964Y006678D01*X002964Y006678D01*X002476Y006790D01*X002476Y006790D01*X001976Y006752D01*X001510Y006569D01*X001118Y006257D01*X000836Y005843D01*X000780Y005660D01*X000780Y008376D01*X000810Y008304D01*X000939Y008174D01*X001108Y008104D01*X001891Y008104D01*X002061Y008174D01*X002190Y008304D01*X002198Y008324D01*X003701Y008324D01*X004060Y007965D01*X004060Y005267D01*X004097Y005178D01*X004164Y005111D01*X004497Y004778D01*X004564Y004711D01*X004647Y004677D01*X004647Y004548D01*X004740Y004454D01*X005267Y004454D01*X005360Y004548D01*X005360Y005181D01*X005267Y005274D01*X004740Y005274D01*X004710Y005244D01*X004540Y005414D01*X004540Y005785D01*X004647Y005678D01*X004647Y005600D02*X004540Y005600D01*X004540Y005442D02*X008960Y005442D01*X008960Y005283D02*X004670Y005283D01*X004309Y004966D02*X004008Y004966D01*X004030Y005114D02*X004030Y005114D01*X004028Y005125D02*X004150Y005125D01*X004060Y005283D02*X004005Y005283D01*X003981Y005442D02*X004060Y005442D01*X004060Y005600D02*X003957Y005600D01*X003883Y005759D02*X004060Y005759D01*X004060Y005917D02*X003807Y005917D01*X003738Y006061D02*X003738Y006061D01*X003724Y006076D02*X004060Y006076D01*X004060Y006234D02*X003577Y006234D01*X003430Y006393D02*X004060Y006393D01*X004060Y006551D02*X003184Y006551D01*X003397Y006428D02*X003397Y006428D01*X002825Y006710D02*X004060Y006710D01*X004060Y006868D02*X000780Y006868D01*X000780Y006710D02*X001868Y006710D01*X001976Y006752D02*X001976Y006752D01*X001510Y006569D02*X001510Y006569D01*X001488Y006551D02*X000780Y006551D01*X000780Y006393D02*X001289Y006393D01*X001118Y006257D02*X001118Y006257D01*X001118Y006257D01*X001103Y006234D02*X000780Y006234D01*X000780Y006076D02*X000995Y006076D01*X000887Y005917D02*X000780Y005917D01*X000836Y005843D02*X000836Y005843D01*X000810Y005759D02*X000780Y005759D01*X000780Y007027D02*X004060Y007027D01*X004060Y007185D02*X000780Y007185D01*X000780Y007344D02*X004060Y007344D01*X004060Y007502D02*X000780Y007502D01*X000780Y007661D02*X004060Y007661D01*X004060Y007819D02*X000780Y007819D01*X000780Y007978D02*X004047Y007978D01*X003889Y008136D02*X001969Y008136D01*X002181Y008295D02*X003730Y008295D01*X003800Y008564D02*X001500Y008564D01*X001031Y008136D02*X000780Y008136D01*X000780Y008295D02*X000819Y008295D01*X001500Y009564D02*X003426Y009564D01*X003010Y009880D02*X002135Y009880D01*X002184Y010831D02*X004710Y010831D01*X004710Y010989D02*X001976Y010989D01*X001997Y011148D02*X004787Y011148D01*X005702Y010038D02*X005797Y010038D01*X004830Y008295D02*X004790Y008295D01*X004790Y008136D02*X004989Y008136D01*X005147Y007978D02*X004790Y007978D01*X004790Y007819D02*X005306Y007819D01*X005464Y007661D02*X004790Y007661D01*X004790Y007502D02*X005623Y007502D01*X005781Y007344D02*X004790Y007344D01*X005360Y007185D02*X005976Y007185D01*X006143Y007661D02*X006814Y007661D01*X005360Y007027D02*X008320Y007027D01*X008320Y006868D02*X005360Y006868D01*X005360Y006710D02*X008320Y006710D01*X008358Y006551D02*X005314Y006551D01*X005307Y006234D02*X008393Y006234D01*X008340Y006076D02*X005360Y006076D01*X005360Y005917D02*X008340Y005917D01*X008340Y005759D02*X005360Y005759D01*X005360Y005600D02*X008340Y005600D01*X008340Y005125D02*X005360Y005125D01*X005360Y004966D02*X008340Y004966D01*X008340Y004808D02*X005360Y004808D01*X005360Y004649D02*X008340Y004649D01*X008397Y004491D02*X005303Y004491D01*X005317Y004174D02*X008383Y004174D01*X008340Y004015D02*X005360Y004015D01*X005360Y003857D02*X008340Y003857D01*X008340Y003698D02*X005360Y003698D01*X004647Y003698D02*X003220Y003698D01*X003449Y003857D02*X004647Y003857D01*X004647Y004015D02*X003596Y004015D01*X003738Y004168D02*X003738Y004168D01*X003741Y004174D02*X004690Y004174D01*X004704Y004491D02*X003894Y004491D01*X003955Y004619D02*X003955Y004619D01*X003960Y004649D02*X004647Y004649D01*X004467Y004808D02*X003984Y004808D01*X003817Y004332D02*X009012Y004332D01*X008996Y004491D02*X009575Y004491D01*X009510Y004332D02*X009488Y004332D01*X009250Y004064D02*X008946Y004064D01*X008696Y003814D01*X009053Y003758D02*X009053Y003544D01*X020126Y003544D01*X019960Y003609D01*X019960Y003609D01*X019568Y003922D01*X019650Y003857D02*X014732Y003857D01*X014732Y003698D02*X019848Y003698D01*X019397Y004174D02*X018704Y004174D01*X018710Y004195D02*X018710Y004466D01*X018322Y004466D01*X018322Y004039D01*X018554Y004039D01*X018599Y004051D01*X018640Y004075D01*X018674Y004109D01*X018698Y004150D01*X018710Y004195D01*X018710Y004332D02*X019288Y004332D01*X019238Y004491D02*X018322Y004491D01*X018322Y004466D02*X018322Y004562D01*X018710Y004562D01*X018710Y004833D01*X018698Y004879D01*X018674Y004920D01*X018640Y004954D01*X018599Y004977D01*X018554Y004990D01*X018322Y004990D01*X018322Y004562D01*X018226Y004562D01*X018226Y004990D01*X017994Y004990D01*X017949Y004977D01*X017908Y004954D01*X017886Y004932D01*X017848Y004970D01*X017204Y004970D01*X017110Y004876D01*X017110Y004754D01*X017010Y004754D01*X017010Y004817D01*X016916Y004911D01*X016311Y004911D01*X016217Y004817D01*X016217Y004212D01*X016311Y004118D01*X016916Y004118D01*X017010Y004212D01*X017010Y004274D01*X017110Y004274D01*X017110Y004153D01*X017204Y004059D01*X017848Y004059D01*X017886Y004097D01*X017908Y004075D01*X017949Y004051D01*X017994Y004039D01*X018226Y004039D01*X018226Y004466D01*X018322Y004466D01*X018322Y004332D02*X018226Y004332D01*X018226Y004174D02*X018322Y004174D01*X018322Y004649D02*X018226Y004649D01*X018226Y004808D02*X018322Y004808D01*X018322Y004966D02*X018226Y004966D01*X017930Y004966D02*X017851Y004966D01*X017526Y004514D02*X016613Y004514D01*X016217Y004491D02*X016183Y004491D01*X016183Y004649D02*X016217Y004649D01*X016217Y004808D02*X016183Y004808D01*X016670Y005096D02*X016758Y005133D01*X018836Y007211D01*X018903Y007278D01*X018940Y007367D01*X018940Y010512D01*X018903Y010600D01*X018634Y010870D01*X018637Y010877D01*X018637Y011051D01*X018571Y011212D01*X018448Y011335D01*X018287Y011401D01*X018113Y011401D01*X017952Y011335D01*X017829Y011212D01*X017818Y011185D01*X017634Y011370D01*X017637Y011377D01*X017637Y011551D01*X017571Y011712D01*X017448Y011835D01*X017287Y011901D01*X017113Y011901D01*X016952Y011835D01*X016829Y011712D01*X016763Y011551D01*X016763Y011377D01*X016829Y011217D01*X016952Y011094D01*X017113Y011027D01*X017287Y011027D01*X017295Y011030D01*X017460Y010865D01*X017460Y010823D01*X017448Y010835D01*X017287Y010901D01*X017113Y010901D01*X016952Y010835D01*X016829Y010712D01*X016763Y010551D01*X016763Y010377D01*X016829Y010217D01*X016952Y010094D01*X017113Y010027D01*X017287Y010027D01*X017448Y010094D01*X017460Y010106D01*X017460Y009823D01*X017448Y009835D01*X017287Y009901D01*X017113Y009901D01*X016952Y009835D01*X016829Y009712D01*X016763Y009551D01*X016763Y009377D01*X016829Y009217D01*X016952Y009094D01*X016960Y009091D01*X016960Y008914D01*X016651Y008604D01*X015840Y008604D01*X015840Y008726D01*X015746Y008820D01*X015102Y008820D01*X015064Y008782D01*X015042Y008804D01*X015001Y008827D01*X014956Y008840D01*X014724Y008840D01*X014724Y008412D01*X014628Y008412D01*X014628Y008316D01*X014240Y008316D01*X014240Y008045D01*X014252Y008000D01*X014276Y007959D01*X014310Y007925D01*X014345Y007904D01*X013152Y007904D01*X013064Y007868D01*X012997Y007800D01*X012564Y007368D01*X011375Y007368D01*X011372Y007366D01*X011061Y007366D01*X010968Y007273D01*X010968Y006604D01*X010849Y006604D01*X009625Y007828D01*X011465Y007828D01*X011559Y007922D01*X011559Y012307D01*X011559Y012099D02*X013755Y012099D01*X013758Y011940D02*X011559Y011940D01*X011559Y011782D02*X012096Y011782D01*X012139Y011824D02*X012045Y011731D01*X012045Y011178D01*X012090Y011133D01*X012061Y011105D01*X012037Y011064D01*X012025Y011018D01*X012025Y010809D01*X012605Y010809D01*X012605Y010759D01*X012025Y010759D01*X012025Y010551D01*X012037Y010505D01*X012061Y010464D01*X012090Y010435D01*X012045Y010391D01*X012045Y009838D01*X012104Y009779D01*X012045Y009721D01*X012045Y009168D01*X012104Y009109D01*X012045Y009051D01*X012045Y008498D01*X012139Y008404D01*X013121Y008404D01*X013201Y008484D01*X013324Y008484D01*X013347Y008461D01*X013479Y008406D01*X013621Y008406D01*X013753Y008461D01*X013854Y008561D01*X013908Y008693D01*X013908Y008836D01*X013876Y008913D01*X014986Y008913D01*X015079Y009006D01*X015079Y009887D01*X014986Y009981D01*X013682Y009981D01*X013708Y010043D01*X013708Y010186D01*X013654Y010317D01*X013553Y010418D01*X013421Y010472D01*X013279Y010472D01*X013176Y010430D01*X013170Y010435D01*X013199Y010464D01*X013223Y010505D01*X013235Y010551D01*X013235Y010759D01*X012655Y010759D01*X012655Y010809D01*X013235Y010809D01*X013235Y011018D01*X013223Y011064D01*X013199Y011105D01*X013176Y011128D01*X013229Y011106D01*X013371Y011106D01*X013401Y011118D01*X013401Y011062D01*X014170Y011062D01*X014170Y010902D01*X014330Y010902D01*X014330Y010428D01*X014943Y010428D01*X014989Y010440D01*X015030Y010464D01*X015063Y010498D01*X015087Y010539D01*X015099Y010584D01*X015099Y010902D01*X014330Y010902D01*X014330Y011062D01*X015099Y011062D01*X015099Y011380D01*X015087Y011426D01*X015063Y011467D01*X015030Y011500D01*X014989Y011524D01*X014943Y011536D01*X014330Y011536D01*X014330Y011062D01*X014170Y011062D01*X014170Y011536D01*X013658Y011536D01*X013604Y011667D01*X013503Y011768D01*X013371Y011822D01*X013229Y011822D01*X013154Y011792D01*X013121Y011824D01*X012139Y011824D01*X012045Y011623D02*X011559Y011623D01*X011559Y011465D02*X012045Y011465D01*X012045Y011306D02*X011559Y011306D01*X011559Y011148D02*X012075Y011148D01*X012025Y010989D02*X011559Y010989D01*X011559Y010831D02*X012025Y010831D01*X012025Y010672D02*X011559Y010672D01*X011559Y010514D02*X012035Y010514D01*X012045Y010355D02*X011559Y010355D01*X011559Y010197D02*X012045Y010197D01*X012045Y010038D02*X011559Y010038D01*X011559Y009880D02*X012045Y009880D01*X012046Y009721D02*X011559Y009721D01*X011559Y009563D02*X012045Y009563D01*X012045Y009404D02*X011559Y009404D01*X011559Y009246D02*X012045Y009246D01*X012082Y009087D02*X011559Y009087D01*X011559Y008929D02*X012045Y008929D01*X012045Y008770D02*X011559Y008770D01*X011559Y008612D02*X012045Y008612D01*X012090Y008453D02*X011559Y008453D01*X011559Y008295D02*X014240Y008295D01*X014240Y008412D02*X014628Y008412D01*X014628Y008840D01*X014396Y008840D01*X014351Y008827D01*X014310Y008804D01*X014276Y008770D01*X014252Y008729D01*X014240Y008683D01*X014240Y008412D01*X014240Y008453D02*X013735Y008453D01*X013874Y008612D02*X014240Y008612D01*X014276Y008770D02*X013908Y008770D01*X013365Y008453D02*X013170Y008453D01*X013016Y007819D02*X009634Y007819D01*X009793Y007661D02*X012857Y007661D01*X012699Y007502D02*X009951Y007502D01*X010110Y007344D02*X011039Y007344D01*X010968Y007185D02*X010268Y007185D01*X010427Y007027D02*X010968Y007027D01*X010968Y006868D02*X010585Y006868D01*X010744Y006710D02*X010968Y006710D01*X011423Y007128D02*X012663Y007128D01*X013200Y007664D01*X015250Y007664D01*X015424Y007838D01*X015424Y008364D01*X016750Y008364D01*X017200Y008814D01*X017200Y009464D01*X016817Y009246D02*X015079Y009246D01*X015079Y009404D02*X016763Y009404D01*X016768Y009563D02*X015079Y009563D01*X015079Y009721D02*X016839Y009721D01*X017061Y009880D02*X015079Y009880D01*X015073Y010514D02*X016763Y010514D01*X016772Y010355D02*X013615Y010355D01*X013557Y010428D02*X014170Y010428D01*X014170Y010902D01*X013401Y010902D01*X013401Y010584D01*X013413Y010539D01*X013437Y010498D01*X013470Y010464D01*X013511Y010440D01*X013557Y010428D01*X013427Y010514D02*X013225Y010514D01*X013235Y010672D02*X013401Y010672D01*X013401Y010831D02*X013235Y010831D01*X013235Y010989D02*X014170Y010989D01*X014170Y010831D02*X014330Y010831D01*X014330Y010989D02*X017336Y010989D01*X017452Y010831D02*X017460Y010831D01*X017700Y010964D02*X017200Y011464D01*X016792Y011306D02*X015099Y011306D01*X015099Y011148D02*X016898Y011148D01*X016948Y010831D02*X015099Y010831D01*X015099Y010672D02*X016813Y010672D01*X016849Y010197D02*X013703Y010197D01*X013706Y010038D02*X017086Y010038D01*X017314Y010038D02*X017460Y010038D01*X017460Y009880D02*X017339Y009880D01*X017940Y009588D02*X017960Y009573D01*X018025Y009541D01*X018093Y009518D01*X018164Y009507D01*X018191Y009507D01*X018191Y009956D01*X018209Y009956D01*X018209Y009507D01*X018236Y009507D01*X018307Y009518D01*X018375Y009541D01*X018440Y009573D01*X018460Y009588D01*X018460Y007514D01*X017940Y006994D01*X017940Y009588D01*X017940Y009563D02*X017981Y009563D01*X017940Y009404D02*X018460Y009404D01*X018460Y009246D02*X017940Y009246D01*X017940Y009087D02*X018460Y009087D01*X018460Y008929D02*X017940Y008929D01*X017940Y008770D02*X018460Y008770D01*X018460Y008612D02*X017940Y008612D01*X017940Y008453D02*X018460Y008453D01*X018460Y008295D02*X017940Y008295D01*X017940Y008136D02*X018460Y008136D01*X018460Y007978D02*X017940Y007978D01*X017940Y007819D02*X018460Y007819D01*X018460Y007661D02*X017940Y007661D01*X017940Y007502D02*X018449Y007502D01*X018290Y007344D02*X017940Y007344D01*X017940Y007185D02*X018132Y007185D01*X017973Y007027D02*X017940Y007027D01*X017700Y006814D02*X017700Y010964D01*X017697Y011306D02*X017924Y011306D01*X017952Y011594D02*X018113Y011527D01*X018287Y011527D01*X018448Y011594D01*X018571Y011717D01*X018637Y011877D01*X018637Y012051D01*X018571Y012212D01*X018448Y012335D01*X018287Y012401D01*X018113Y012401D01*X017952Y012335D01*X017829Y012212D01*X017763Y012051D01*X017763Y011877D01*X017829Y011717D01*X017952Y011594D01*X017923Y011623D02*X017607Y011623D01*X017637Y011465D02*X022320Y011465D01*X022320Y011623D02*X020956Y011623D01*X020847Y011594D02*X021132Y011671D01*X021388Y011818D01*X021596Y012027D01*X021744Y012282D01*X021820Y012567D01*X021820Y012862D01*X021744Y013147D01*X021596Y013402D01*X021388Y013611D01*X021132Y013758D01*X020847Y013834D01*X020553Y013834D01*X020268Y013758D01*X020012Y013611D01*X019804Y013402D01*X019656Y013147D01*X019580Y012862D01*X019580Y012567D01*X019656Y012282D01*X019804Y012027D01*X020012Y011818D01*X020268Y011671D01*X020553Y011594D01*X020847Y011594D01*X020444Y011623D02*X018477Y011623D01*X018598Y011782D02*X020075Y011782D01*X019890Y011940D02*X018637Y011940D01*X018617Y012099D02*X019762Y012099D01*X019671Y012257D02*X018525Y012257D01*X017875Y012257D02*X014745Y012257D01*X014745Y012099D02*X017783Y012099D01*X017763Y011940D02*X014742Y011940D01*X014327Y011940D02*X014173Y011940D01*X014173Y012099D02*X014327Y012099D01*X014327Y012257D02*X014173Y012257D01*X014173Y012416D02*X014327Y012416D01*X014327Y012574D02*X014173Y012574D01*X014327Y012733D02*X019580Y012733D01*X019588Y012891D02*X014745Y012891D01*X014745Y013050D02*X019630Y013050D01*X019692Y013208D02*X014745Y013208D01*X014745Y013367D02*X019783Y013367D01*X019927Y013525D02*X014745Y013525D01*X014607Y013684D02*X020139Y013684D01*X021261Y013684D02*X022320Y013684D01*X022320Y013842D02*X010475Y013842D01*X010475Y014001D02*X022320Y014001D01*X022320Y014159D02*X010475Y014159D01*X010475Y014318D02*X022320Y014318D01*X022320Y014476D02*X021308Y014476D01*X021647Y014635D02*X022320Y014635D01*X022320Y014793D02*X021887Y014793D01*X021847Y014751D02*X021847Y014751D01*X022034Y014952D02*X022320Y014952D01*X022320Y015110D02*X022181Y015110D01*X022261Y015269D02*X022320Y015269D01*X020299Y014476D02*X009330Y014476D01*X009330Y014318D02*X009525Y014318D01*X009525Y014159D02*X009330Y014159D01*X009409Y014001D02*X009525Y014001D01*X008935Y013684D02*X006858Y013684D01*X006835Y013842D02*X008797Y013842D01*X008770Y014001D02*X006720Y014001D01*X006496Y014159D02*X008770Y014159D01*X008770Y014318D02*X006540Y014318D01*X006540Y014476D02*X008770Y014476D01*X008770Y014635D02*X006540Y014635D01*X006540Y014793D02*X008770Y014793D01*X008770Y014952D02*X006514Y014952D01*X006385Y015110D02*X008770Y015110D01*X008770Y015269D02*X006544Y015269D01*X006590Y015427D02*X008770Y015427D01*X008770Y015586D02*X006590Y015586D01*X006590Y015744D02*X008770Y015744D01*X008770Y015903D02*X006590Y015903D01*X006590Y016061D02*X008770Y016061D01*X008770Y016220D02*X007479Y016220D01*X007221Y016220D02*X006590Y016220D01*X006715Y016378D02*X006985Y016378D01*X006905Y016537D02*X006795Y016537D01*X006810Y016695D02*X006890Y016695D01*X006890Y016854D02*X006810Y016854D01*X006810Y017012D02*X006890Y017012D01*X006890Y017171D02*X006810Y017171D01*X006810Y017329D02*X006890Y017329D01*X006945Y017488D02*X006755Y017488D01*X006350Y016964D02*X006350Y015414D01*X006100Y015164D01*X006100Y014588D01*X006124Y014564D01*X006000Y014490D01*X006000Y013024D01*X005500Y013024D02*X005500Y014440D01*X005376Y014564D01*X005350Y014590D01*X005350Y016964D01*X004890Y017012D02*X003687Y017012D01*X003688Y017011D02*X003688Y017011D01*X003764Y016854D02*X004890Y016854D01*X004890Y016695D02*X003840Y016695D01*X003905Y016560D02*X003905Y016560D01*X003909Y016537D02*X004905Y016537D01*X004985Y016378D02*X003933Y016378D01*X003957Y016220D02*X005110Y016220D01*X005110Y016061D02*X003980Y016061D01*X003980Y016064D02*X003980Y016064D01*X003956Y015903D02*X005110Y015903D01*X005110Y015744D02*X003932Y015744D01*X003908Y015586D02*X005110Y015586D01*X005110Y015427D02*X003837Y015427D01*X003761Y015269D02*X005110Y015269D01*X005110Y015110D02*X003681Y015110D01*X003688Y015118D02*X003688Y015118D01*X003534Y014952D02*X004986Y014952D01*X004960Y014793D02*X003387Y014793D01*X003347Y014751D02*X003347Y014751D01*X003147Y014635D02*X004960Y014635D01*X004960Y014476D02*X002808Y014476D01*X002914Y014500D02*X002914Y014500D01*X002426Y014389D02*X002426Y014389D01*X001926Y014426D02*X001926Y014426D01*X001799Y014476D02*X000780Y014476D01*X000780Y014318D02*X004960Y014318D01*X005004Y014159D02*X000780Y014159D01*X000780Y014001D02*X005260Y014001D01*X005260Y013842D02*X000780Y013842D01*X000780Y013684D02*X005260Y013684D01*X005000Y013604D02*X005000Y013024D01*X005000Y013604D01*X005000Y013525D02*X005000Y013525D01*X005000Y013367D02*X005000Y013367D01*X005000Y013208D02*X005000Y013208D01*X005000Y013050D02*X005000Y013050D01*X005000Y013024D02*X005000Y013024D01*X005000Y012891D02*X005000Y012891D01*X005000Y012733D02*X005000Y012733D01*X005000Y012574D02*X005000Y012574D01*X003675Y013050D02*X000780Y013050D01*X000780Y013208D02*X003675Y013208D01*X001460Y014609D02*X001460Y014609D01*X001428Y014635D02*X000780Y014635D01*X000780Y014793D02*X001229Y014793D01*X001048Y014952D02*X000780Y014952D01*X000780Y015110D02*X000940Y015110D01*X000832Y015269D02*X000780Y015269D01*X000786Y015335D02*X000786Y015335D01*X003347Y017378D02*X003347Y017378D01*X003392Y017329D02*X004890Y017329D01*X004890Y017171D02*X003539Y017171D01*X003157Y017488D02*X004945Y017488D01*X007755Y017488D02*X008978Y017488D01*X008819Y017329D02*X007810Y017329D01*X007810Y017171D02*X008770Y017171D01*X008770Y017012D02*X007810Y017012D01*X007810Y016854D02*X008770Y016854D01*X008770Y016695D02*X007810Y016695D01*X007795Y016537D02*X008770Y016537D01*X008770Y016378D02*X007715Y016378D01*X009330Y016378D02*X009525Y016378D01*X009525Y016220D02*X009330Y016220D01*X009330Y016061D02*X009525Y016061D01*X009525Y015903D02*X009330Y015903D01*X009330Y015744D02*X009525Y015744D01*X009525Y015586D02*X009330Y015586D01*X009330Y015427D02*X009525Y015427D01*X009676Y015269D02*X009330Y015269D01*X009330Y015110D02*X009642Y015110D01*X009680Y014952D02*X009330Y014952D01*X009330Y014793D02*X009839Y014793D01*X010161Y014793D02*X013933Y014793D01*X013946Y014761D02*X014047Y014661D01*X014179Y014606D01*X014321Y014606D01*X014453Y014661D01*X014554Y014761D01*X014608Y014893D01*X014608Y015036D01*X014557Y015160D01*X014631Y015160D01*X014725Y015254D01*X014725Y016922D01*X014631Y017015D01*X013869Y017015D01*X013775Y016922D01*X013775Y015254D01*X013869Y015160D01*X013943Y015160D01*X013892Y015036D01*X013892Y014893D01*X013946Y014761D01*X013892Y014952D02*X010320Y014952D01*X010358Y015110D02*X013923Y015110D01*X013775Y015269D02*X010324Y015269D01*X010475Y015427D02*X013775Y015427D01*X013775Y015586D02*X010475Y015586D01*X010475Y015744D02*X013775Y015744D01*X013775Y015903D02*X010475Y015903D01*X010475Y016061D02*X013775Y016061D01*X013775Y016220D02*X010494Y016220D01*X010475Y016378D02*X013775Y016378D01*X013775Y016537D02*X010475Y016537D01*X010475Y016695D02*X013775Y016695D01*X013775Y016854D02*X010475Y016854D01*X010475Y017012D02*X013866Y017012D01*X014634Y017012D02*X016406Y017012D01*X016564Y016854D02*X014725Y016854D01*X014725Y016695D02*X016723Y016695D01*X016890Y016537D02*X014725Y016537D01*X014725Y016378D02*X016908Y016378D01*X016994Y016220D02*X014725Y016220D01*X014725Y016061D02*X017242Y016061D01*X017458Y016061D02*X018242Y016061D01*X018258Y016054D02*X018441Y016054D01*X018611Y016124D01*X018740Y016254D01*X018810Y016423D01*X018810Y017206D01*X018740Y017375D01*X018611Y017504D01*X018441Y017574D01*X018258Y017574D01*X018089Y017504D01*X017960Y017375D01*X017890Y017206D01*X017890Y016423D01*X017960Y016254D01*X018089Y016124D01*X018258Y016054D01*X018458Y016061D02*X019139Y016061D01*X019139Y015903D02*X014725Y015903D01*X014725Y015744D02*X019160Y015744D01*X019209Y015586D02*X014725Y015586D01*X014725Y015427D02*X019258Y015427D01*X019332Y015269D02*X014725Y015269D01*X014577Y015110D02*X019440Y015110D01*X019548Y014952D02*X014608Y014952D01*X014567Y014793D02*X019729Y014793D01*X019928Y014635D02*X014390Y014635D01*X014110Y014635D02*X009330Y014635D01*X010000Y015114D02*X010000Y016262D01*X010250Y016214D01*X009525Y016537D02*X009330Y016537D01*X009330Y016695D02*X009525Y016695D01*X009525Y016854D02*X009330Y016854D01*X009330Y017012D02*X009525Y017012D01*X006280Y014001D02*X006240Y014001D01*X006500Y013714D02*X006500Y013024D01*X006790Y013050D02*X009525Y013050D01*X009525Y013208D02*X006790Y013208D01*X006790Y013367D02*X009252Y013367D01*X009093Y013525D02*X006809Y013525D01*X006790Y012891D02*X009525Y012891D01*X009525Y012733D02*X006790Y012733D01*X006790Y012574D02*X009564Y012574D01*X010475Y012891D02*X011417Y012891D01*X011310Y013050D02*X010475Y013050D01*X012630Y011454D02*X013290Y011454D01*X013300Y011464D01*X013622Y011623D02*X016793Y011623D01*X016763Y011465D02*X015064Y011465D01*X014330Y011465D02*X014170Y011465D01*X014170Y011306D02*X014330Y011306D01*X014330Y011148D02*X014170Y011148D01*X014170Y010672D02*X014330Y010672D01*X014330Y010514D02*X014170Y010514D01*X013350Y010114D02*X012630Y010114D01*X013469Y011782D02*X016899Y011782D01*X017501Y011782D02*X017802Y011782D01*X018476Y011306D02*X022320Y011306D01*X022320Y011148D02*X018597Y011148D01*X018637Y010989D02*X022320Y010989D01*X022320Y010831D02*X018673Y010831D01*X018831Y010672D02*X022320Y010672D01*X022320Y010514D02*X018939Y010514D01*X018940Y010355D02*X022320Y010355D01*X022320Y010197D02*X018940Y010197D01*X018940Y010038D02*X022320Y010038D01*X022320Y009880D02*X018940Y009880D01*X018940Y009721D02*X020204Y009721D01*X020268Y009758D02*X020012Y009611D01*X019804Y009402D01*X019656Y009147D01*X019580Y008862D01*X019580Y008567D01*X019656Y008282D01*X019804Y008027D01*X020012Y007818D01*X020268Y007671D01*X020553Y007594D01*X020847Y007594D01*X021132Y007671D01*X021388Y007818D01*X021596Y008027D01*X021744Y008282D01*X021820Y008567D01*X021820Y008862D01*X021744Y009147D01*X021596Y009402D01*X021388Y009611D01*X021132Y009758D01*X020847Y009834D01*X020553Y009834D01*X020268Y009758D01*X019965Y009563D02*X018940Y009563D01*X018940Y009404D02*X019806Y009404D01*X019714Y009246D02*X018940Y009246D01*X018940Y009087D02*X019640Y009087D01*X019598Y008929D02*X018940Y008929D01*X018940Y008770D02*X019580Y008770D01*X019580Y008612D02*X018940Y008612D01*X018940Y008453D02*X019610Y008453D01*X019653Y008295D02*X018940Y008295D01*X018940Y008136D02*X019740Y008136D01*X019853Y007978D02*X018940Y007978D01*X018940Y007819D02*X020011Y007819D01*X020304Y007661D02*X018940Y007661D01*X018940Y007502D02*X022320Y007502D01*X022320Y007344D02*X018931Y007344D01*X018810Y007185D02*X022320Y007185D01*X022320Y007027D02*X018652Y007027D01*X018493Y006868D02*X022320Y006868D01*X022320Y006710D02*X021056Y006710D01*X021547Y006551D02*X022320Y006551D01*X022320Y006393D02*X021821Y006393D01*X021981Y006234D02*X022320Y006234D01*X022320Y006076D02*X022128Y006076D01*X022233Y005917D02*X022320Y005917D01*X022309Y005759D02*X022320Y005759D01*X020528Y006710D02*X018335Y006710D01*X018176Y006551D02*X020042Y006551D01*X019801Y006393D02*X018018Y006393D01*X017859Y006234D02*X019603Y006234D01*X019479Y006076D02*X017701Y006076D01*X017542Y005917D02*X019371Y005917D01*X019276Y005759D02*X017384Y005759D01*X017225Y005600D02*X019227Y005600D01*X019178Y005442D02*X017067Y005442D01*X016908Y005283D02*X019139Y005283D01*X019139Y005125D02*X016738Y005125D01*X016670Y005096D02*X014732Y005096D01*X014732Y003656D01*X014639Y003562D01*X013916Y003562D01*X013822Y003656D01*X013822Y006632D01*X013774Y006632D01*X013703Y006561D01*X013571Y006506D01*X013429Y006506D01*X013297Y006561D01*X013196Y006661D01*X013142Y006793D01*X013142Y006936D01*X013196Y007067D01*X013297Y007168D01*X013429Y007222D01*X013571Y007222D01*X013703Y007168D01*X013759Y007112D01*X013802Y007112D01*X013802Y007128D01*X014277Y007128D01*X014277Y007386D01*X013958Y007386D01*X013912Y007374D01*X013871Y007350D01*X013838Y007317D01*X013814Y007276D01*X013802Y007230D01*X013802Y007128D01*X014277Y007128D01*X014277Y007128D01*X014277Y007128D01*X014277Y007386D01*X014592Y007386D01*X014594Y007388D01*X014635Y007412D01*X014681Y007424D01*X014952Y007424D01*X014952Y007036D01*X015048Y007036D01*X015475Y007036D01*X015475Y007268D01*X015463Y007314D01*X015439Y007355D01*X015406Y007388D01*X015365Y007412D01*X015319Y007424D01*X015048Y007424D01*X015048Y007036D01*X015048Y006940D01*X015475Y006940D01*X015475Y006709D01*X015463Y006663D01*X015439Y006622D01*X015418Y006600D01*X015449Y006569D01*X015579Y006622D01*X015721Y006622D01*X015853Y006568D01*X015954Y006467D01*X016008Y006336D01*X016008Y006193D01*X015954Y006061D01*X015853Y005961D01*X015721Y005906D01*X015579Y005906D01*X015455Y005957D01*X015455Y005918D01*X015369Y005832D01*X016379Y005832D01*X017460Y006914D01*X017460Y009106D01*X017448Y009094D01*X017440Y009091D01*X017440Y008767D01*X017403Y008678D01*X017336Y008611D01*X016886Y008161D01*X016798Y008124D01*X015840Y008124D01*X015840Y008003D01*X015746Y007909D01*X015664Y007909D01*X015664Y007791D01*X015627Y007702D01*X015453Y007528D01*X015453Y007528D01*X015386Y007461D01*X015298Y007424D01*X013299Y007424D01*X012799Y006924D01*X012711Y006888D01*X011878Y006888D01*X011878Y005599D01*X011897Y005618D01*X012029Y005672D01*X012171Y005672D01*X012303Y005618D01*X012404Y005517D01*X012458Y005386D01*X012458Y005243D01*X012404Y005111D01*X012303Y005011D01*X012171Y004956D01*X012029Y004956D01*X011897Y005011D01*X011878Y005030D01*X011878Y004218D01*X011886Y004205D01*X011898Y004159D01*X011898Y004057D01*X011423Y004057D01*X011423Y004057D01*X011898Y004057D01*X011898Y003954D01*X011886Y003909D01*X011878Y003895D01*X011878Y003656D01*X011784Y003562D01*X011061Y003562D01*X011014Y003610D01*X010999Y003601D01*X010954Y003589D01*X010722Y003589D01*X010722Y004016D01*X010626Y004016D01*X010626Y003589D01*X010394Y003589D01*X010349Y003601D01*X010308Y003625D01*X010286Y003647D01*X010248Y003609D01*X009604Y003609D01*X009510Y003703D01*X009510Y003818D01*X009453Y003761D01*X009321Y003706D01*X009179Y003706D01*X009053Y003758D01*X009053Y003698D02*X009515Y003698D01*X009250Y004064D02*X009926Y004064D01*X010286Y004482D02*X010254Y004514D01*X010265Y004517D01*X010306Y004540D01*X010339Y004574D01*X010363Y004615D01*X010375Y004661D01*X010375Y004892D01*X009948Y004892D01*X009948Y004988D01*X010375Y004988D01*X010375Y005220D01*X010363Y005266D01*X010339Y005307D01*X010318Y005328D01*X010355Y005366D01*X010355Y005608D01*X010968Y005608D01*X010968Y005481D01*X010968Y004536D01*X010954Y004540D01*X010722Y004540D01*X010722Y004112D01*X010948Y004112D01*X010948Y004057D01*X011423Y004057D01*X011406Y004040D01*X010674Y004064D01*X010722Y004016D02*X010722Y004112D01*X010626Y004112D01*X010626Y004540D01*X010394Y004540D01*X010349Y004527D01*X010308Y004504D01*X010286Y004482D01*X010277Y004491D02*X010295Y004491D01*X010372Y004649D02*X010968Y004649D01*X010968Y004808D02*X010375Y004808D01*X010375Y005125D02*X010968Y005125D01*X010968Y005283D02*X010353Y005283D01*X010355Y005442D02*X010968Y005442D01*X010968Y005600D02*X010355Y005600D01*X010060Y005848D02*X009900Y005688D01*X009324Y005688D01*X009200Y005564D01*X009200Y005064D01*X009000Y004864D01*X008696Y004864D01*X009108Y004649D02*X009428Y004649D01*X009425Y004808D02*X009283Y004808D01*X009419Y004966D02*X009852Y004966D01*X009948Y004966D02*X010968Y004966D01*X011423Y005336D02*X011445Y005314D01*X012100Y005314D01*X011880Y005600D02*X011878Y005600D01*X011878Y005759D02*X013822Y005759D01*X013822Y005917D02*X011878Y005917D01*X011878Y006076D02*X013822Y006076D01*X013822Y006234D02*X011878Y006234D01*X011878Y006393D02*X013822Y006393D01*X013822Y006551D02*X013680Y006551D01*X013320Y006551D02*X011878Y006551D01*X011878Y006710D02*X013176Y006710D01*X013142Y006868D02*X011878Y006868D01*X012902Y007027D02*X013180Y007027D01*X013060Y007185D02*X013339Y007185D01*X013219Y007344D02*X013865Y007344D01*X013802Y007185D02*X013661Y007185D01*X013507Y006872D02*X013500Y006864D01*X013507Y006872D02*X014277Y006872D01*X014277Y007128D02*X014861Y007128D01*X015000Y006988D01*X015048Y007027D02*X017460Y007027D01*X017460Y007185D02*X015475Y007185D01*X015446Y007344D02*X017460Y007344D01*X017460Y007502D02*X015427Y007502D01*X015586Y007661D02*X017460Y007661D01*X017460Y007819D02*X015664Y007819D01*X015815Y007978D02*X017460Y007978D01*X017460Y008136D02*X016827Y008136D01*X017020Y008295D02*X017460Y008295D01*X017460Y008453D02*X017178Y008453D01*X017337Y008612D02*X017460Y008612D01*X017460Y008770D02*X017440Y008770D01*X017440Y008929D02*X017460Y008929D01*X017460Y009087D02*X017440Y009087D01*X016960Y009087D02*X015079Y009087D01*X015002Y008929D02*X016960Y008929D01*X016817Y008770D02*X015795Y008770D01*X015840Y008612D02*X016658Y008612D01*X018191Y009563D02*X018209Y009563D01*X018209Y009721D02*X018191Y009721D01*X018191Y009880D02*X018209Y009880D01*X018209Y009973D02*X018191Y009973D01*X018191Y010421D01*X018164Y010421D01*X018093Y010410D01*X018025Y010388D01*X017960Y010355D01*X017940Y010341D01*X017940Y010606D01*X017952Y010594D01*X018113Y010527D01*X018287Y010527D01*X018295Y010530D01*X018460Y010365D01*X018460Y010341D01*X018440Y010355D01*X018375Y010388D01*X018307Y010410D01*X018236Y010421D01*X018209Y010421D01*X018209Y009973D01*X018209Y010038D02*X018191Y010038D01*X018191Y010197D02*X018209Y010197D01*X018209Y010355D02*X018191Y010355D01*X018311Y010514D02*X017940Y010514D01*X017940Y010355D02*X017960Y010355D01*X018440Y010355D02*X018460Y010355D01*X018700Y010464D02*X018200Y010964D01*X018700Y010464D02*X018700Y007414D01*X016622Y005336D01*X014277Y005336D01*X014277Y005592D02*X016478Y005592D01*X017700Y006814D01*X017415Y006868D02*X015475Y006868D01*X015475Y006710D02*X017256Y006710D01*X017098Y006551D02*X015869Y006551D01*X015984Y006393D02*X016939Y006393D01*X016781Y006234D02*X016008Y006234D01*X015960Y006076D02*X016622Y006076D01*X016464Y005917D02*X015748Y005917D01*X015552Y005917D02*X015454Y005917D01*X015650Y006264D02*X015024Y006264D01*X015000Y006240D01*X014952Y007185D02*X015048Y007185D01*X015048Y007344D02*X014952Y007344D01*X014277Y007344D02*X014277Y007344D01*X014277Y007185D02*X014277Y007185D01*X014265Y007978D02*X011559Y007978D01*X011559Y008136D02*X014240Y008136D01*X014628Y008453D02*X014724Y008453D01*X014724Y008612D02*X014628Y008612D01*X014628Y008770D02*X014724Y008770D01*X018419Y009563D02*X018460Y009563D01*X021196Y009721D02*X022320Y009721D01*X022320Y009563D02*X021435Y009563D01*X021594Y009404D02*X022320Y009404D01*X022320Y009246D02*X021686Y009246D01*X021760Y009087D02*X022320Y009087D01*X022320Y008929D02*X021802Y008929D01*X021820Y008770D02*X022320Y008770D01*X022320Y008612D02*X021820Y008612D01*X021790Y008453D02*X022320Y008453D01*X022320Y008295D02*X021747Y008295D01*X021660Y008136D02*X022320Y008136D01*X022320Y007978D02*X021547Y007978D01*X021389Y007819D02*X022320Y007819D01*X022320Y007661D02*X021096Y007661D01*X019139Y004966D02*X018618Y004966D01*X018710Y004808D02*X019141Y004808D01*X019190Y004649D02*X018710Y004649D01*X017201Y004966D02*X014732Y004966D01*X014732Y004808D02*X014987Y004808D01*X013822Y004808D02*X011878Y004808D01*X011878Y004966D02*X012004Y004966D01*X012196Y004966D02*X013822Y004966D01*X013822Y005125D02*X012409Y005125D01*X012458Y005283D02*X013822Y005283D01*X013822Y005442D02*X012435Y005442D01*X012320Y005600D02*X013822Y005600D01*X013822Y004649D02*X011878Y004649D01*X011878Y004491D02*X013822Y004491D01*X013822Y004332D02*X011878Y004332D01*X011894Y004174D02*X013822Y004174D01*X013822Y004015D02*X011898Y004015D01*X011878Y003857D02*X013822Y003857D01*X013822Y003698D02*X011878Y003698D01*X011423Y004057D02*X010948Y004057D01*X010948Y004016D01*X010722Y004016D01*X010722Y004015D02*X010626Y004015D01*X010626Y003857D02*X010722Y003857D01*X010722Y003698D02*X010626Y003698D01*X010626Y004174D02*X010722Y004174D01*X010722Y004332D02*X010626Y004332D01*X010626Y004491D02*X010722Y004491D01*X011423Y004057D02*X011423Y004057D01*X011423Y005848D02*X010060Y005848D01*X009890Y005848D02*X009900Y005688D01*X009510Y006076D02*X009053Y006076D01*X009053Y005917D02*X009250Y005917D01*X009055Y005759D02*X009053Y005759D01*X009000Y006234D02*X010191Y006234D01*X010032Y006393D02*X004790Y006393D01*X004566Y005759D02*X004540Y005759D01*X004300Y005314D02*X004300Y008064D01*X003800Y008564D01*X004300Y005314D02*X004700Y004914D01*X004954Y004914D01*X005004Y004864D01*X002964Y003550D02*X002964Y003550D01*X008678Y006551D02*X008715Y006551D01*X008715Y006484D02*X008917Y006484D01*X008963Y006497D01*X009004Y006520D01*X009037Y006554D01*X009061Y006595D01*X009073Y006641D01*X009073Y006896D01*X008715Y006896D01*X008715Y006484D01*X008715Y006710D02*X008678Y006710D01*X008678Y006868D02*X008715Y006868D01*X009073Y006868D02*X009557Y006868D01*X009715Y006710D02*X009073Y006710D01*X009035Y006551D02*X009874Y006551D01*X009398Y007027D02*X009073Y007027D01*X014745Y012416D02*X019620Y012416D01*X019580Y012574D02*X014745Y012574D01*X014250Y014964D02*X014250Y016088D01*X016722Y017488D02*X017073Y017488D01*X016941Y017329D02*X016881Y017329D01*X017627Y017488D02*X018073Y017488D01*X017941Y017329D02*X017759Y017329D01*X017810Y017171D02*X017890Y017171D01*X017890Y017012D02*X017810Y017012D01*X017810Y016854D02*X017890Y016854D01*X017890Y016695D02*X017810Y016695D01*X017810Y016537D02*X017890Y016537D01*X017908Y016378D02*X017792Y016378D01*X017706Y016220D02*X017994Y016220D01*X018706Y016220D02*X019139Y016220D01*X019158Y016378D02*X018792Y016378D01*X018810Y016537D02*X019207Y016537D01*X019256Y016695D02*X018810Y016695D01*X018810Y016854D02*X019328Y016854D01*X019436Y017012D02*X018810Y017012D01*X018810Y017171D02*X019544Y017171D01*X019722Y017329D02*X018759Y017329D01*X018627Y017488D02*X019921Y017488D01*X021473Y013525D02*X022320Y013525D01*X022320Y013367D02*X021617Y013367D01*X021708Y013208D02*X022320Y013208D01*X022320Y013050D02*X021770Y013050D01*X021812Y012891D02*X022320Y012891D01*X022320Y012733D02*X021820Y012733D01*X021820Y012574D02*X022320Y012574D01*X022320Y012416D02*X021780Y012416D01*X021729Y012257D02*X022320Y012257D01*X022320Y012099D02*X021638Y012099D01*X021510Y011940D02*X022320Y011940D01*X022320Y011782D02*X021325Y011782D01*X017110Y004808D02*X017010Y004808D01*X016972Y004174D02*X017110Y004174D01*X016255Y004174D02*X016145Y004174D01*X016183Y004332D02*X016217Y004332D01*X000856Y012257D02*X000780Y012257D01*X000780Y012891D02*X000876Y012891D01*D26*X004150Y011564D03*X006500Y013714D03*X010000Y015114D03*X011650Y013164D03*X013300Y011464D03*X013350Y010114D03*X013550Y008764D03*X013500Y006864D03*X012100Y005314D03*X009250Y004064D03*X015200Y004514D03*X015650Y006264D03*X015850Y009914D03*X014250Y014964D03*D27*X011650Y013164D02*X011348Y013467D01*X010000Y013467D01*X009952Y013514D01*X009500Y013514D01*X009050Y013964D01*X009050Y017164D01*X009300Y017414D01*X016400Y017414D01*X017000Y016814D01*X017350Y016814D01*X014250Y010982D02*X014052Y010784D01*X012630Y010784D01*X012632Y009447D02*X012630Y009444D01*X012632Y009447D02*X014250Y009447D01*X013550Y008764D02*X012640Y008764D01*X012630Y008774D01*M02* diff --git a/gerbonara/gerber/tests/resources/top_mask.GTS b/gerbonara/gerber/tests/resources/top_mask.GTS new file mode 100644 index 0000000..a3886f5 --- /dev/null +++ b/gerbonara/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/gerbonara/gerber/tests/resources/top_silk.GTO b/gerbonara/gerber/tests/resources/top_silk.GTO new file mode 100644 index 0000000..ea46f80 --- /dev/null +++ b/gerbonara/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/gerbonara/gerber/tests/test_am_statements.py b/gerbonara/gerber/tests/test_am_statements.py new file mode 100644 index 0000000..0d100b5 --- /dev/null +++ b/gerbonara/gerber/tests/test_am_statements.py @@ -0,0 +1,395 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +import pytest + +from ..am_statements import * +from ..am_statements import inch, metric + + +def test_AMPrimitive_ctor(): + for exposure in ("on", "off", "ON", "OFF"): + for code in (0, 1, 2, 4, 5, 6, 7, 20, 21, 22): + p = AMPrimitive(code, exposure) + assert p.code == code + assert p.exposure == exposure.lower() + + +def test_AMPrimitive_validation(): + pytest.raises(TypeError, AMPrimitive, "1", "off") + pytest.raises(ValueError, AMPrimitive, 0, "exposed") + pytest.raises(ValueError, AMPrimitive, 3, "off") + + +def test_AMPrimitive_conversion(): + p = AMPrimitive(4, "on") + pytest.raises(NotImplementedError, p.to_inch) + pytest.raises(NotImplementedError, p.to_metric) + + +def test_AMCommentPrimitive_ctor(): + c = AMCommentPrimitive(0, " This is a comment *") + assert c.code == 0 + assert c.comment == "This is a comment" + + +def test_AMCommentPrimitive_validation(): + pytest.raises(ValueError, AMCommentPrimitive, 1, "This is a comment") + + +def test_AMCommentPrimitive_factory(): + c = AMCommentPrimitive.from_gerber("0 Rectangle with rounded corners. *") + assert c.code == 0 + assert c.comment == "Rectangle with rounded corners." + + +def test_AMCommentPrimitive_dump(): + c = AMCommentPrimitive(0, "Rectangle with rounded corners.") + assert c.to_gerber() == "0 Rectangle with rounded corners. *" + + +def test_AMCommentPrimitive_conversion(): + c = AMCommentPrimitive(0, "Rectangle with rounded corners.") + ci = c + cm = c + ci.to_inch() + cm.to_metric() + assert c == ci + assert c == cm + + +def test_AMCommentPrimitive_string(): + c = AMCommentPrimitive(0, "Test Comment") + assert str(c) == "" + + +def test_AMCirclePrimitive_ctor(): + test_cases = ( + (1, "on", 0, (0, 0)), + (1, "off", 1, (0, 1)), + (1, "on", 2.5, (0, 2)), + (1, "off", 5.0, (3, 3)), + ) + for code, exposure, diameter, position in test_cases: + c = AMCirclePrimitive(code, exposure, diameter, position) + assert c.code == code + assert c.exposure == exposure + assert c.diameter == diameter + assert c.position == position + + +def test_AMCirclePrimitive_validation(): + pytest.raises(ValueError, AMCirclePrimitive, 2, "on", 0, (0, 0)) + + +def test_AMCirclePrimitive_factory(): + c = AMCirclePrimitive.from_gerber("1,0,5,0,0*") + assert c.code == 1 + assert c.exposure == "off" + assert c.diameter == 5 + assert c.position == (0, 0) + + +def test_AMCirclePrimitive_dump(): + c = AMCirclePrimitive(1, "off", 5, (0, 0)) + assert c.to_gerber() == "1,0,5,0,0*" + c = AMCirclePrimitive(1, "on", 5, (0, 0)) + assert c.to_gerber() == "1,1,5,0,0*" + + +def test_AMCirclePrimitive_conversion(): + c = AMCirclePrimitive(1, "off", 25.4, (25.4, 0)) + c.to_inch() + assert c.diameter == 1 + assert c.position == (1, 0) + + c = AMCirclePrimitive(1, "off", 1, (1, 0)) + c.to_metric() + assert c.diameter == 25.4 + assert c.position == (25.4, 0) + + +def test_AMVectorLinePrimitive_validation(): + pytest.raises( + ValueError, AMVectorLinePrimitive, 3, "on", 0.1, (0, 0), (3.3, 5.4), 0 + ) + + +def test_AMVectorLinePrimitive_factory(): + l = AMVectorLinePrimitive.from_gerber("20,1,0.9,0,0.45,12,0.45,0*") + assert l.code == 20 + assert l.exposure == "on" + assert l.width == 0.9 + assert l.start == (0, 0.45) + assert l.end == (12, 0.45) + assert l.rotation == 0 + + +def test_AMVectorLinePrimitive_dump(): + l = AMVectorLinePrimitive.from_gerber("20,1,0.9,0,0.45,12,0.45,0*") + assert l.to_gerber() == "20,1,0.9,0.0,0.45,12.0,0.45,0.0*" + + +def test_AMVectorLinePrimtive_conversion(): + l = AMVectorLinePrimitive(20, "on", 25.4, (0, 0), (25.4, 25.4), 0) + l.to_inch() + assert l.width == 1 + assert l.start == (0, 0) + assert l.end == (1, 1) + + l = AMVectorLinePrimitive(20, "on", 1, (0, 0), (1, 1), 0) + l.to_metric() + assert l.width == 25.4 + assert l.start == (0, 0) + assert l.end == (25.4, 25.4) + + +def test_AMOutlinePrimitive_validation(): + pytest.raises( + ValueError, + AMOutlinePrimitive, + 7, + "on", + (0, 0), + [(3.3, 5.4), (4.0, 5.4), (0, 0)], + 0, + ) + pytest.raises( + ValueError, + AMOutlinePrimitive, + 4, + "on", + (0, 0), + [(3.3, 5.4), (4.0, 5.4), (0, 1)], + 0, + ) + + +def test_AMOutlinePrimitive_factory(): + o = AMOutlinePrimitive.from_gerber("4,1,3,0,0,3,3,3,0,0,0,0*") + assert o.code == 4 + assert o.exposure == "on" + assert o.start_point == (0, 0) + assert o.points == [(3, 3), (3, 0), (0, 0)] + assert o.rotation == 0 + + +def test_AMOUtlinePrimitive_dump(): + o = AMOutlinePrimitive(4, "on", (0, 0), [(3, 3), (3, 0), (0, 0)], 0) + # New lines don't matter for Gerber, but we insert them to make it easier to remove + # For test purposes we can ignore them + assert o.to_gerber().replace("\n", "") == "4,1,3,0,0,3,3,3,0,0,0,0*" + + +def test_AMOutlinePrimitive_conversion(): + o = AMOutlinePrimitive(4, "on", (0, 0), [(25.4, 25.4), (25.4, 0), (0, 0)], 0) + o.to_inch() + assert o.start_point == (0, 0) + assert o.points == ((1.0, 1.0), (1.0, 0.0), (0.0, 0.0)) + + o = AMOutlinePrimitive(4, "on", (0, 0), [(1, 1), (1, 0), (0, 0)], 0) + o.to_metric() + assert o.start_point == (0, 0) + assert o.points == ((25.4, 25.4), (25.4, 0), (0, 0)) + + +def test_AMPolygonPrimitive_validation(): + pytest.raises(ValueError, AMPolygonPrimitive, 6, "on", 3, (3.3, 5.4), 3, 0) + pytest.raises(ValueError, AMPolygonPrimitive, 5, "on", 2, (3.3, 5.4), 3, 0) + pytest.raises(ValueError, AMPolygonPrimitive, 5, "on", 13, (3.3, 5.4), 3, 0) + + +def test_AMPolygonPrimitive_factory(): + p = AMPolygonPrimitive.from_gerber("5,1,3,3.3,5.4,3,0") + assert p.code == 5 + assert p.exposure == "on" + assert p.vertices == 3 + assert p.position == (3.3, 5.4) + assert p.diameter == 3 + assert p.rotation == 0 + + +def test_AMPolygonPrimitive_dump(): + p = AMPolygonPrimitive(5, "on", 3, (3.3, 5.4), 3, 0) + assert p.to_gerber() == "5,1,3,3.3,5.4,3,0*" + + +def test_AMPolygonPrimitive_conversion(): + p = AMPolygonPrimitive(5, "off", 3, (25.4, 0), 25.4, 0) + p.to_inch() + assert p.diameter == 1 + assert p.position == (1, 0) + + p = AMPolygonPrimitive(5, "off", 3, (1, 0), 1, 0) + p.to_metric() + assert p.diameter == 25.4 + assert p.position == (25.4, 0) + + +def test_AMMoirePrimitive_validation(): + pytest.raises( + ValueError, AMMoirePrimitive, 7, (0, 0), 5.1, 0.2, 0.4, 6, 0.1, 6.1, 0 + ) + + +def test_AMMoirePrimitive_factory(): + m = AMMoirePrimitive.from_gerber("6,0,0,5,0.5,0.5,2,0.1,6,0*") + assert m.code == 6 + assert m.position == (0, 0) + assert m.diameter == 5 + assert m.ring_thickness == 0.5 + assert m.gap == 0.5 + assert m.max_rings == 2 + assert m.crosshair_thickness == 0.1 + assert m.crosshair_length == 6 + assert m.rotation == 0 + + +def test_AMMoirePrimitive_dump(): + m = AMMoirePrimitive.from_gerber("6,0,0,5,0.5,0.5,2,0.1,6,0*") + assert m.to_gerber() == "6,0,0,5.0,0.5,0.5,2,0.1,6.0,0.0*" + + +def test_AMMoirePrimitive_conversion(): + m = AMMoirePrimitive(6, (25.4, 25.4), 25.4, 25.4, 25.4, 6, 25.4, 25.4, 0) + m.to_inch() + assert m.position == (1.0, 1.0) + assert m.diameter == 1.0 + assert m.ring_thickness == 1.0 + assert m.gap == 1.0 + assert m.crosshair_thickness == 1.0 + assert m.crosshair_length == 1.0 + + m = AMMoirePrimitive(6, (1, 1), 1, 1, 1, 6, 1, 1, 0) + m.to_metric() + assert m.position == (25.4, 25.4) + assert m.diameter == 25.4 + assert m.ring_thickness == 25.4 + assert m.gap == 25.4 + assert m.crosshair_thickness == 25.4 + assert m.crosshair_length == 25.4 + + +def test_AMThermalPrimitive_validation(): + pytest.raises(ValueError, AMThermalPrimitive, 8, (0.0, 0.0), 7, 5, 0.2, 0.0) + pytest.raises(TypeError, AMThermalPrimitive, 7, (0.0, "0"), 7, 5, 0.2, 0.0) + + +def test_AMThermalPrimitive_factory(): + t = AMThermalPrimitive.from_gerber("7,0,0,7,6,0.2,45*") + assert t.code == 7 + assert t.position == (0, 0) + assert t.outer_diameter == 7 + assert t.inner_diameter == 6 + assert t.gap == 0.2 + assert t.rotation == 45 + + +def test_AMThermalPrimitive_dump(): + t = AMThermalPrimitive.from_gerber("7,0,0,7,6,0.2,30*") + assert t.to_gerber() == "7,0,0,7.0,6.0,0.2,30.0*" + + +def test_AMThermalPrimitive_conversion(): + t = AMThermalPrimitive(7, (25.4, 25.4), 25.4, 25.4, 25.4, 0.0) + t.to_inch() + assert t.position == (1.0, 1.0) + assert t.outer_diameter == 1.0 + assert t.inner_diameter == 1.0 + assert t.gap == 1.0 + + t = AMThermalPrimitive(7, (1, 1), 1, 1, 1, 0) + t.to_metric() + assert t.position == (25.4, 25.4) + assert t.outer_diameter == 25.4 + assert t.inner_diameter == 25.4 + assert t.gap == 25.4 + + +def test_AMCenterLinePrimitive_validation(): + pytest.raises(ValueError, AMCenterLinePrimitive, 22, 1, 0.2, 0.5, (0, 0), 0) + + +def test_AMCenterLinePrimtive_factory(): + l = AMCenterLinePrimitive.from_gerber("21,1,6.8,1.2,3.4,0.6,0*") + assert l.code == 21 + assert l.exposure == "on" + assert l.width == 6.8 + assert l.height == 1.2 + assert l.center == (3.4, 0.6) + assert l.rotation == 0 + + +def test_AMCenterLinePrimitive_dump(): + l = AMCenterLinePrimitive.from_gerber("21,1,6.8,1.2,3.4,0.6,0*") + assert l.to_gerber() == "21,1,6.8,1.2,3.4,0.6,0.0*" + + +def test_AMCenterLinePrimitive_conversion(): + l = AMCenterLinePrimitive(21, "on", 25.4, 25.4, (25.4, 25.4), 0) + l.to_inch() + assert l.width == 1.0 + assert l.height == 1.0 + assert l.center == (1.0, 1.0) + + l = AMCenterLinePrimitive(21, "on", 1, 1, (1, 1), 0) + l.to_metric() + assert l.width == 25.4 + assert l.height == 25.4 + assert l.center == (25.4, 25.4) + + +def test_AMLowerLeftLinePrimitive_validation(): + pytest.raises(ValueError, AMLowerLeftLinePrimitive, 23, 1, 0.2, 0.5, (0, 0), 0) + + +def test_AMLowerLeftLinePrimtive_factory(): + l = AMLowerLeftLinePrimitive.from_gerber("22,1,6.8,1.2,3.4,0.6,0*") + assert l.code == 22 + assert l.exposure == "on" + assert l.width == 6.8 + assert l.height == 1.2 + assert l.lower_left == (3.4, 0.6) + assert l.rotation == 0 + + +def test_AMLowerLeftLinePrimitive_dump(): + l = AMLowerLeftLinePrimitive.from_gerber("22,1,6.8,1.2,3.4,0.6,0*") + assert l.to_gerber() == "22,1,6.8,1.2,3.4,0.6,0.0*" + + +def test_AMLowerLeftLinePrimitive_conversion(): + l = AMLowerLeftLinePrimitive(22, "on", 25.4, 25.4, (25.4, 25.4), 0) + l.to_inch() + assert l.width == 1.0 + assert l.height == 1.0 + assert l.lower_left == (1.0, 1.0) + + l = AMLowerLeftLinePrimitive(22, "on", 1, 1, (1, 1), 0) + l.to_metric() + assert l.width == 25.4 + assert l.height == 25.4 + assert l.lower_left == (25.4, 25.4) + + +def test_AMUnsupportPrimitive(): + u = AMUnsupportPrimitive.from_gerber("Test") + assert u.primitive == "Test" + u = AMUnsupportPrimitive("Test") + assert u.to_gerber() == "Test" + + +def test_AMUnsupportPrimitive_smoketest(): + u = AMUnsupportPrimitive.from_gerber("Test") + u.to_inch() + u.to_metric() + + +def test_inch(): + assert inch(25.4) == 1 + + +def test_metric(): + assert metric(1) == 25.4 diff --git a/gerbonara/gerber/tests/test_cairo_backend.py b/gerbonara/gerber/tests/test_cairo_backend.py new file mode 100644 index 0000000..51007a9 --- /dev/null +++ b/gerbonara/gerber/tests/test_cairo_backend.py @@ -0,0 +1,279 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Garret Fick +import os +import shutil +import tempfile + +from ..render.cairo_backend import GerberCairoContext +from ..rs274x import read + + +def _DISABLED_test_render_two_boxes(): + """Umaco exapmle of two boxes""" + _test_render( + "resources/example_two_square_boxes.gbr", "golden/example_two_square_boxes.png" + ) + + +def _DISABLED_test_render_single_quadrant(): + """Umaco exapmle of a single quadrant arc""" + _test_render( + "resources/example_single_quadrant.gbr", "golden/example_single_quadrant.png" + ) + + +def _DISABLED_test_render_simple_contour(): + """Umaco exapmle of a simple arrow-shaped contour""" + gerber = _test_render( + "resources/example_simple_contour.gbr", "golden/example_simple_contour.png" + ) + + # Check the resulting dimensions + assert ((2.0, 11.0), (1.0, 9.0)) == gerber.bounding_box + + +def _DISABLED_test_render_single_contour_1(): + """Umaco example of a single contour + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render( + "resources/example_single_contour_1.gbr", "golden/example_single_contour.png" + ) + + +def _DISABLED_test_render_single_contour_2(): + """Umaco exapmle of a single contour, alternate contour end order + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render( + "resources/example_single_contour_2.gbr", "golden/example_single_contour.png" + ) + + +def _DISABLED_test_render_single_contour_3(): + """Umaco exapmle of a single contour with extra line""" + _test_render( + "resources/example_single_contour_3.gbr", "golden/example_single_contour_3.png" + ) + + +def _DISABLED_test_render_not_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_not_overlapping_contour.gbr", + "golden/example_not_overlapping_contour.png", + ) + + +def _DISABLED_test_render_not_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_not_overlapping_touching.gbr", + "golden/example_not_overlapping_touching.png", + ) + + +def test_render_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_overlapping_touching.gbr", + "golden/example_overlapping_touching.png", + ) + + +def test_render_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_overlapping_contour.gbr", + "golden/example_overlapping_contour.png", + ) + + +def _DISABLED_test_render_level_holes(): + """Umaco example of using multiple levels to create multiple holes""" + + # TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more + # rendering fixes in the related repository that may resolve these. + _test_render( + "resources/example_level_holes.gbr", "golden/example_overlapping_contour.png" + ) + + +def _DISABLED_test_render_cutin(): + """Umaco example of using a cutin""" + + # TODO This is clearly rendering wrong. + _test_render( + "resources/example_cutin.gbr", + "golden/example_cutin.png", + "/Users/ham/Desktop/cutin.png", + ) + + +def _DISABLED_test_render_fully_coincident(): + """Umaco example of coincident lines rendering two contours""" + + _test_render( + "resources/example_fully_coincident.gbr", "golden/example_fully_coincident.png" + ) + + +def _DISABLED_test_render_coincident_hole(): + """Umaco example of coincident lines rendering a hole in the contour""" + + _test_render( + "resources/example_coincident_hole.gbr", "golden/example_coincident_hole.png" + ) + + +def test_render_cutin_multiple(): + """Umaco example of a region with multiple cutins""" + + _test_render( + "resources/example_cutin_multiple.gbr", "golden/example_cutin_multiple.png" + ) + + +def _DISABLED_test_flash_circle(): + """Umaco example a simple circular flash with and without a hole""" + + _test_render( + "resources/example_flash_circle.gbr", + "golden/example_flash_circle.png", + "/Users/ham/Desktop/flashcircle.png", + ) + + +def _DISABLED_test_flash_rectangle(): + """Umaco example a simple rectangular flash with and without a hole""" + + _test_render( + "resources/example_flash_rectangle.gbr", "golden/example_flash_rectangle.png" + ) + + +def _DISABLED_test_flash_obround(): + """Umaco example a simple obround flash with and without a hole""" + + _test_render( + "resources/example_flash_obround.gbr", "golden/example_flash_obround.png" + ) + + +def _DISABLED_test_flash_polygon(): + """Umaco example a simple polygon flash with and without a hole""" + + _test_render( + "resources/example_flash_polygon.gbr", "golden/example_flash_polygon.png" + ) + + +def _DISABLED_test_holes_dont_clear(): + """Umaco example that an aperture with a hole does not clear the area""" + + _test_render( + "resources/example_holes_dont_clear.gbr", "golden/example_holes_dont_clear.png" + ) + + +def _DISABLED_test_render_am_exposure_modifier(): + """Umaco example that an aperture macro with a hole does not clear the area""" + + _test_render( + "resources/example_am_exposure_modifier.gbr", + "golden/example_am_exposure_modifier.png", + ) + + +def test_render_svg_simple_contour(): + """Example of rendering to an SVG file""" + _test_simple_render_svg("resources/example_simple_contour.gbr") + + +def _resolve_path(path): + return os.path.join(os.path.dirname(__file__), path) + + +def _test_render(gerber_path, png_expected_path, create_output_path=None): + """Render the gerber file and compare to the expected PNG output. + + Parameters + ---------- + gerber_path : string + Path to Gerber file to open + png_expected_path : string + Path to the PNG file to compare to + create_output : string|None + If not None, write the generated PNG to the specified path. + This is primarily to help with + """ + + gerber_path = _resolve_path(gerber_path) + png_expected_path = _resolve_path(png_expected_path) + if create_output_path: + create_output_path = _resolve_path(create_output_path) + + gerber = read(gerber_path) + + # Create PNG image to the memory stream + ctx = GerberCairoContext() + gerber.render(ctx) + + actual_bytes = ctx.dump(None) + + # If we want to write the file bytes, do it now. This happens + if create_output_path: + with open(create_output_path, "wb") as out_file: + out_file.write(actual_bytes) + # Creating the output is dangerous - it could overwrite the expected result. + # So if we are creating the output, we make the test fail on purpose so you + # won't forget to disable this + assert not True, ( + "Test created the output %s. This needs to be disabled to make sure the test behaves correctly" + % (create_output_path,) + ) + + # Read the expected PNG file + + with open(png_expected_path, "rb") as expected_file: + expected_bytes = expected_file.read() + + # Don't directly use assert_equal otherwise any failure pollutes the test results + equal = expected_bytes == actual_bytes + assert equal + + return gerber + + +def _test_simple_render_svg(gerber_path): + """Render the gerber file as SVG + + Note: verifies only the header, not the full content. + + Parameters + ---------- + gerber_path : string + Path to Gerber file to open + """ + + gerber_path = _resolve_path(gerber_path) + gerber = read(gerber_path) + + # Create SVG image to the memory stream + ctx = GerberCairoContext() + gerber.render(ctx) + + temp_dir = tempfile.mkdtemp() + svg_temp_path = os.path.join(temp_dir, "output.svg") + + assert not os.path.exists(svg_temp_path) + ctx.dump(svg_temp_path) + assert os.path.exists(svg_temp_path) + + with open(svg_temp_path, "r") as expected_file: + expected_bytes = expected_file.read() + assert expected_bytes[:38] == '' + + shutil.rmtree(temp_dir) diff --git a/gerbonara/gerber/tests/test_cam.py b/gerbonara/gerber/tests/test_cam.py new file mode 100644 index 0000000..8a71a32 --- /dev/null +++ b/gerbonara/gerber/tests/test_cam.py @@ -0,0 +1,151 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +import pytest + +from ..cam import CamFile, FileSettings + + +def test_filesettings_defaults(): + """ Test FileSettings default values + """ + fs = FileSettings() + assert fs.format == (2, 5) + assert fs.notation == "absolute" + assert fs.zero_suppression == "trailing" + assert fs.units == "inch" + + +def test_filesettings_dict(): + """ Test FileSettings Dict + """ + fs = FileSettings() + assert fs["format"] == (2, 5) + assert fs["notation"] == "absolute" + assert fs["zero_suppression"] == "trailing" + assert fs["units"] == "inch" + + +def test_filesettings_assign(): + """ Test FileSettings attribute assignment + """ + fs = FileSettings() + fs.units = "test1" + fs.notation = "test2" + fs.zero_suppression = "test3" + fs.format = "test4" + assert fs.units == "test1" + assert fs.notation == "test2" + assert fs.zero_suppression == "test3" + assert 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 fs.units == "metric" + assert fs.notation == "incremental" + assert fs.zero_suppression == "leading" + assert 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 cf.settings == FileSettings() + + +def test_bounds_override_smoketest(): + cf = CamFile() + cf.bounds + + +def test_zeros(): + """ Test zero/zero_suppression interaction + """ + fs = FileSettings() + assert fs.zero_suppression == "trailing" + assert fs.zeros == "leading" + + fs["zero_suppression"] = "leading" + assert fs.zero_suppression == "leading" + assert fs.zeros == "trailing" + + fs.zero_suppression = "trailing" + assert fs.zero_suppression == "trailing" + assert fs.zeros == "leading" + + fs["zeros"] = "trailing" + assert fs.zeros == "trailing" + assert fs.zero_suppression == "leading" + + fs.zeros = "leading" + assert fs.zeros == "leading" + assert fs.zero_suppression == "trailing" + + fs = FileSettings(zeros="leading") + assert fs.zeros == "leading" + assert fs.zero_suppression == "trailing" + + fs = FileSettings(zero_suppression="leading") + assert fs.zeros == "trailing" + assert fs.zero_suppression == "leading" + + fs = FileSettings(zeros="leading", zero_suppression="trailing") + assert fs.zeros == "leading" + assert fs.zero_suppression == "trailing" + + fs = FileSettings(zeros="trailing", zero_suppression="leading") + assert fs.zeros == "trailing" + assert fs.zero_suppression == "leading" + + +def test_filesettings_validation(): + """ Test FileSettings constructor argument validation + """ + # absolute-ish is not a valid notation + pytest.raises(ValueError, FileSettings, "absolute-ish", "inch", None, (2, 5), None) + + # degrees kelvin isn't a valid unit for a CAM file + pytest.raises( + ValueError, FileSettings, "absolute", "degrees kelvin", None, (2, 5), None + ) + + pytest.raises( + ValueError, FileSettings, "absolute", "inch", "leading", (2, 5), "leading" + ) + + # Technnically this should be an error, but Eangle files often do this incorrectly so we + # allow it + # pytest.raises(ValueError, FileSettings, 'absolute', + # 'inch', 'following', (2, 5), None) + + pytest.raises( + ValueError, FileSettings, "absolute", "inch", None, (2, 5), "following" + ) + pytest.raises(ValueError, FileSettings, "absolute", "inch", None, (2, 5, 6), None) + + +def test_key_validation(): + fs = FileSettings() + pytest.raises(KeyError, fs.__getitem__, "octopus") + pytest.raises(KeyError, fs.__setitem__, "octopus", "do not care") + pytest.raises(ValueError, fs.__setitem__, "notation", "absolute-ish") + pytest.raises(ValueError, fs.__setitem__, "units", "degrees kelvin") + pytest.raises(ValueError, fs.__setitem__, "zero_suppression", "following") + pytest.raises(ValueError, fs.__setitem__, "zeros", "following") + pytest.raises(ValueError, fs.__setitem__, "format", (2, 5, 6)) diff --git a/gerbonara/gerber/tests/test_common.py b/gerbonara/gerber/tests/test_common.py new file mode 100644 index 0000000..a6b1264 --- /dev/null +++ b/gerbonara/gerber/tests/test_common.py @@ -0,0 +1,38 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +from ..exceptions import ParseError +from ..common import read, loads +from ..excellon import ExcellonFile +from ..rs274x import GerberFile +import os +import pytest + + +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) + + +def test_load_from_string(): + with open(NCDRILL_FILE, "rU") as f: + ncdrill = loads(f.read()) + with open(TOP_COPPER_FILE, "rU") as f: + top_copper = loads(f.read()) + assert isinstance(ncdrill, ExcellonFile) + assert isinstance(top_copper, GerberFile) + + +def test_file_type_validation(): + """ Test file format validation + """ + pytest.raises(ParseError, read, __file__) diff --git a/gerbonara/gerber/tests/test_excellon.py b/gerbonara/gerber/tests/test_excellon.py new file mode 100644 index 0000000..d6e83cc --- /dev/null +++ b/gerbonara/gerber/tests/test_excellon.py @@ -0,0 +1,366 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +import os +import pytest + +from ..cam import FileSettings +from ..excellon import read, detect_excellon_format, ExcellonFile, ExcellonParser +from ..excellon import DrillHit, DrillSlot +from ..excellon_statements import ExcellonTool, RouteModeStmt + + +NCDRILL_FILE = os.path.join(os.path.dirname(__file__), "resources/ncdrill.DRD") + + +def test_format_detection(): + """ Test file type detection + """ + with open(NCDRILL_FILE, "rU") as f: + data = f.read() + settings = detect_excellon_format(data) + assert settings["format"] == (2, 4) + assert settings["zeros"] == "trailing" + + settings = detect_excellon_format(filename=NCDRILL_FILE) + assert settings["format"] == (2, 4) + assert settings["zeros"] == "trailing" + + +def test_read(): + ncdrill = read(NCDRILL_FILE) + assert isinstance(ncdrill, ExcellonFile) + + +def test_write(): + ncdrill = read(NCDRILL_FILE) + ncdrill.write("test.ncd") + with open(NCDRILL_FILE, "rU") as src: + srclines = src.readlines() + with open("test.ncd", "rU") as res: + for idx, line in enumerate(res): + assert line.strip() == srclines[idx].strip() + os.remove("test.ncd") + + +def test_read_settings(): + ncdrill = read(NCDRILL_FILE) + assert ncdrill.settings["format"] == (2, 4) + assert ncdrill.settings["zeros"] == "trailing" + + +def test_bounding_box(): + ncdrill = read(NCDRILL_FILE) + xbound, ybound = ncdrill.bounding_box + pytest.approx(xbound, (0.1300, 2.1430)) + pytest.approx(ybound, (0.3946, 1.7164)) + + +def test_report(): + ncdrill = read(NCDRILL_FILE) + rprt = ncdrill.report() + + +def test_conversion(): + import copy + + ncdrill = read(NCDRILL_FILE) + assert ncdrill.settings.units == "inch" + ncdrill_inch = copy.deepcopy(ncdrill) + + ncdrill.to_metric() + assert ncdrill.settings.units == "metric" + for tool in iter(ncdrill_inch.tools.values()): + tool.to_metric() + + for statement in ncdrill_inch.statements: + statement.to_metric() + + for m_tool, i_tool in zip( + iter(ncdrill.tools.values()), iter(ncdrill_inch.tools.values()) + ): + assert i_tool == m_tool + + for m, i in zip(ncdrill.primitives, ncdrill_inch.primitives): + + assert m.position == i.position, "%s not equal to %s" % (m, i) + assert m.diameter == i.diameter, "%s not equal to %s" % (m, i) + + +def test_parser_hole_count(): + settings = FileSettings(**detect_excellon_format(NCDRILL_FILE)) + p = ExcellonParser(settings) + p.parse(NCDRILL_FILE) + assert p.hole_count == 36 + + +def test_parser_hole_sizes(): + settings = FileSettings(**detect_excellon_format(NCDRILL_FILE)) + p = ExcellonParser(settings) + p.parse(NCDRILL_FILE) + assert p.hole_sizes == [0.0236, 0.0354, 0.04, 0.126, 0.128] + + +def test_parse_whitespace(): + p = ExcellonParser(FileSettings()) + assert p._parse_line(" ") == None + + +def test_parse_comment(): + p = ExcellonParser(FileSettings()) + p._parse_line(";A comment") + assert p.statements[0].comment == "A comment" + + +def test_parse_format_comment(): + p = ExcellonParser(FileSettings()) + p._parse_line("; FILE_FORMAT=9:9 ") + assert p.format == (9, 9) + + +def test_parse_header(): + p = ExcellonParser(FileSettings()) + p._parse_line("M48 ") + assert p.state == "HEADER" + p._parse_line("M95 ") + assert p.state == "DRILL" + + +def test_parse_rout(): + p = ExcellonParser(FileSettings()) + p._parse_line("G00X040944Y019842") + assert p.state == "ROUT" + p._parse_line("G05 ") + assert p.state == "DRILL" + + +def test_parse_version(): + p = ExcellonParser(FileSettings()) + p._parse_line("VER,1 ") + assert p.statements[0].version == 1 + p._parse_line("VER,2 ") + assert p.statements[1].version == 2 + + +def test_parse_format(): + p = ExcellonParser(FileSettings()) + p._parse_line("FMAT,1 ") + assert p.statements[0].format == 1 + p._parse_line("FMAT,2 ") + assert p.statements[1].format == 2 + + +def test_parse_units(): + settings = FileSettings(units="inch", zeros="trailing") + p = ExcellonParser(settings) + p._parse_line(";METRIC,LZ") + assert p.units == "inch" + assert p.zeros == "trailing" + p._parse_line("METRIC,LZ") + assert p.units == "metric" + assert p.zeros == "leading" + + +def test_parse_incremental_mode(): + settings = FileSettings(units="inch", zeros="trailing") + p = ExcellonParser(settings) + assert p.notation == "absolute" + p._parse_line("ICI,ON ") + assert p.notation == "incremental" + p._parse_line("ICI,OFF ") + assert p.notation == "absolute" + + +def test_parse_absolute_mode(): + settings = FileSettings(units="inch", zeros="trailing") + p = ExcellonParser(settings) + assert p.notation == "absolute" + p._parse_line("ICI,ON ") + assert p.notation == "incremental" + p._parse_line("G90 ") + assert p.notation == "absolute" + + +def test_parse_repeat_hole(): + p = ExcellonParser(FileSettings()) + p.active_tool = ExcellonTool(FileSettings(), number=8) + p._parse_line("R03X1.5Y1.5") + assert p.statements[0].count == 3 + + +def test_parse_incremental_position(): + p = ExcellonParser(FileSettings(notation="incremental")) + p._parse_line("X01Y01") + p._parse_line("X01Y01") + assert p.pos == [2.0, 2.0] + + +def test_parse_unknown(): + p = ExcellonParser(FileSettings()) + p._parse_line("Not A Valid Statement") + assert p.statements[0].stmt == "Not A Valid Statement" + + +def test_drill_hit_units_conversion(): + """ Test unit conversion for drill hits + """ + # Inch hit + settings = FileSettings(units="inch") + tool = ExcellonTool(settings, diameter=1.0) + hit = DrillHit(tool, (1.0, 1.0)) + + assert hit.tool.settings.units == "inch" + assert hit.tool.diameter == 1.0 + assert hit.position == (1.0, 1.0) + + # No Effect + hit.to_inch() + + assert hit.tool.settings.units == "inch" + assert hit.tool.diameter == 1.0 + assert hit.position == (1.0, 1.0) + + # Should convert + hit.to_metric() + + assert hit.tool.settings.units == "metric" + assert hit.tool.diameter == 25.4 + assert hit.position == (25.4, 25.4) + + # No Effect + hit.to_metric() + + assert hit.tool.settings.units == "metric" + assert hit.tool.diameter == 25.4 + assert hit.position == (25.4, 25.4) + + # Convert back to inch + hit.to_inch() + + assert hit.tool.settings.units == "inch" + assert hit.tool.diameter == 1.0 + assert hit.position == (1.0, 1.0) + + +def test_drill_hit_offset(): + TEST_VECTORS = [ + ((0.0, 0.0), (0.0, 1.0), (0.0, 1.0)), + ((0.0, 0.0), (1.0, 1.0), (1.0, 1.0)), + ((1.0, 1.0), (0.0, -1.0), (1.0, 0.0)), + ((1.0, 1.0), (-1.0, -1.0), (0.0, 0.0)), + ] + for position, offset, expected in TEST_VECTORS: + settings = FileSettings(units="inch") + tool = ExcellonTool(settings, diameter=1.0) + hit = DrillHit(tool, position) + + assert hit.position == position + + hit.offset(offset[0], offset[1]) + + assert hit.position == expected + + +def test_drill_slot_units_conversion(): + """ Test unit conversion for drill hits + """ + # Inch hit + settings = FileSettings(units="inch") + tool = ExcellonTool(settings, diameter=1.0) + hit = DrillSlot(tool, (1.0, 1.0), (10.0, 10.0), DrillSlot.TYPE_ROUT) + + assert hit.tool.settings.units == "inch" + assert hit.tool.diameter == 1.0 + assert hit.start == (1.0, 1.0) + assert hit.end == (10.0, 10.0) + + # No Effect + hit.to_inch() + + assert hit.tool.settings.units == "inch" + assert hit.tool.diameter == 1.0 + assert hit.start == (1.0, 1.0) + assert hit.end == (10.0, 10.0) + + # Should convert + hit.to_metric() + + assert hit.tool.settings.units == "metric" + assert hit.tool.diameter == 25.4 + assert hit.start == (25.4, 25.4) + assert hit.end == (254.0, 254.0) + + # No Effect + hit.to_metric() + + assert hit.tool.settings.units == "metric" + assert hit.tool.diameter == 25.4 + assert hit.start == (25.4, 25.4) + assert hit.end == (254.0, 254.0) + + # Convert back to inch + hit.to_inch() + + assert hit.tool.settings.units == "inch" + assert hit.tool.diameter == 1.0 + assert hit.start == (1.0, 1.0) + assert hit.end == (10.0, 10.0) + + +def test_drill_slot_offset(): + TEST_VECTORS = [ + ((0.0, 0.0), (1.0, 1.0), (0.0, 0.0), (0.0, 0.0), (1.0, 1.0)), + ((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (1.0, 0.0), (2.0, 1.0)), + ((0.0, 0.0), (1.0, 1.0), (1.0, 1.0), (1.0, 1.0), (2.0, 2.0)), + ((0.0, 0.0), (1.0, 1.0), (-1.0, 1.0), (-1.0, 1.0), (0.0, 2.0)), + ] + for start, end, offset, expected_start, expected_end in TEST_VECTORS: + settings = FileSettings(units="inch") + tool = ExcellonTool(settings, diameter=1.0) + slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT) + + assert slot.start == start + assert slot.end == end + + slot.offset(offset[0], offset[1]) + + assert slot.start == expected_start + assert slot.end == expected_end + + +def test_drill_slot_bounds(): + TEST_VECTORS = [ + ((0.0, 0.0), (1.0, 1.0), 1.0, ((-0.5, 1.5), (-0.5, 1.5))), + ((0.0, 0.0), (1.0, 1.0), 0.5, ((-0.25, 1.25), (-0.25, 1.25))), + ] + for start, end, diameter, expected in TEST_VECTORS: + settings = FileSettings(units="inch") + tool = ExcellonTool(settings, diameter=diameter) + slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT) + + assert slot.bounding_box == expected + + +def test_handling_multi_line_g00_and_g1(): + """Route Mode statements with coordinates on separate line are handled + """ + test_data = """ +% +M48 +M72 +T01C0.0236 +% +T01 +G00 +X040944Y019842 +M15 +G01 +X040944Y020708 +M16 +""" + uut = ExcellonParser() + uut.parse_raw(test_data) + assert ( + len([stmt for stmt in uut.statements if isinstance(stmt, RouteModeStmt)]) == 2 + ) diff --git a/gerbonara/gerber/tests/test_excellon_statements.py b/gerbonara/gerber/tests/test_excellon_statements.py new file mode 100644 index 0000000..41fe294 --- /dev/null +++ b/gerbonara/gerber/tests/test_excellon_statements.py @@ -0,0 +1,734 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +import pytest +from ..excellon_statements import * +from ..cam import FileSettings + + +def test_excellon_statement_implementation(): + stmt = ExcellonStatement() + pytest.raises(NotImplementedError, stmt.from_excellon, None) + pytest.raises(NotImplementedError, stmt.to_excellon) + + +def test_excellontstmt(): + """ Smoke test ExcellonStatement + """ + stmt = ExcellonStatement() + stmt.to_inch() + stmt.to_metric() + stmt.offset() + + +def test_excellontool_factory(): + """ Test ExcellonTool factory methods + """ + exc_line = "T8F01B02S00003H04Z05C0.12500" + settings = FileSettings( + format=(2, 5), zero_suppression="trailing", units="inch", notation="absolute" + ) + tool = ExcellonTool.from_excellon(exc_line, settings) + assert tool.number == 8 + assert tool.diameter == 0.125 + assert tool.feed_rate == 1 + assert tool.retract_rate == 2 + assert tool.rpm == 3 + assert tool.max_hit_count == 4 + assert tool.depth_offset == 5 + + stmt = { + "number": 8, + "feed_rate": 1, + "retract_rate": 2, + "rpm": 3, + "diameter": 0.125, + "max_hit_count": 4, + "depth_offset": 5, + } + tool = ExcellonTool.from_dict(settings, stmt) + assert tool.number == 8 + assert tool.diameter == 0.125 + assert tool.feed_rate == 1 + assert tool.retract_rate == 2 + assert tool.rpm == 3 + assert tool.max_hit_count == 4 + assert tool.depth_offset == 5 + + +def test_excellontool_dump(): + """ Test ExcellonTool to_excellon() + """ + exc_lines = [ + "T01F0S0C0.01200", + "T02F0S0C0.01500", + "T03F0S0C0.01968", + "T04F0S0C0.02800", + "T05F0S0C0.03300", + "T06F0S0C0.03800", + "T07F0S0C0.04300", + "T08F0S0C0.12500", + "T09F0S0C0.13000", + "T08B01F02H03S00003C0.12500Z04", + "T01F0S300.999C0.01200", + ] + settings = FileSettings( + format=(2, 5), zero_suppression="trailing", units="inch", notation="absolute" + ) + for line in exc_lines: + tool = ExcellonTool.from_excellon(line, settings) + assert tool.to_excellon() == line + + +def test_excellontool_order(): + settings = FileSettings( + 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 tool1.diameter == tool2.diameter + assert tool1.feed_rate == tool2.feed_rate + assert tool1.rpm == tool2.rpm + + +def test_excellontool_conversion(): + tool = ExcellonTool.from_dict( + FileSettings(units="metric"), {"number": 8, "diameter": 25.4} + ) + tool.to_inch() + assert tool.diameter == 1.0 + tool = ExcellonTool.from_dict( + FileSettings(units="inch"), {"number": 8, "diameter": 1.0} + ) + tool.to_metric() + assert tool.diameter == 25.4 + + # Shouldn't change units if we're already using target units + tool = ExcellonTool.from_dict( + FileSettings(units="inch"), {"number": 8, "diameter": 25.4} + ) + tool.to_inch() + assert tool.diameter == 25.4 + tool = ExcellonTool.from_dict( + FileSettings(units="metric"), {"number": 8, "diameter": 1.0} + ) + tool.to_metric() + assert tool.diameter == 1.0 + + +def test_excellontool_repr(): + tool = ExcellonTool.from_dict(FileSettings(), {"number": 8, "diameter": 0.125}) + assert str(tool) == "" + tool = ExcellonTool.from_dict( + FileSettings(units="metric"), {"number": 8, "diameter": 0.125} + ) + assert str(tool) == "" + + +def test_excellontool_equality(): + t = ExcellonTool.from_dict(FileSettings(), {"number": 8, "diameter": 0.125}) + t1 = ExcellonTool.from_dict(FileSettings(), {"number": 8, "diameter": 0.125}) + assert t == t1 + t1 = ExcellonTool.from_dict( + FileSettings(units="metric"), {"number": 8, "diameter": 0.125} + ) + assert t != t1 + + +def test_toolselection_factory(): + """ Test ToolSelectionStmt factory method + """ + stmt = ToolSelectionStmt.from_excellon("T01") + assert stmt.tool == 1 + assert stmt.compensation_index == None + stmt = ToolSelectionStmt.from_excellon("T0223") + assert stmt.tool == 2 + assert stmt.compensation_index == 23 + stmt = ToolSelectionStmt.from_excellon("T042") + assert stmt.tool == 42 + assert stmt.compensation_index == None + + +def test_toolselection_dump(): + """ Test ToolSelectionStmt to_excellon() + """ + lines = ["T01", "T0223", "T10", "T09", "T0000"] + for line in lines: + stmt = ToolSelectionStmt.from_excellon(line) + assert stmt.to_excellon() == line + + +def test_z_axis_infeed_rate_factory(): + """ Test ZAxisInfeedRateStmt factory method + """ + stmt = ZAxisInfeedRateStmt.from_excellon("F01") + assert stmt.rate == 1 + stmt = ZAxisInfeedRateStmt.from_excellon("F2") + assert stmt.rate == 2 + stmt = ZAxisInfeedRateStmt.from_excellon("F03") + assert stmt.rate == 3 + + +def test_z_axis_infeed_rate_dump(): + """ Test ZAxisInfeedRateStmt to_excellon() + """ + inputs = [("F01", "F01"), ("F2", "F02"), ("F00003", "F03")] + for input_rate, expected_output in inputs: + stmt = ZAxisInfeedRateStmt.from_excellon(input_rate) + assert stmt.to_excellon() == expected_output + + +def test_coordinatestmt_factory(): + """ Test CoordinateStmt factory method + """ + settings = FileSettings( + format=(2, 5), zero_suppression="trailing", units="inch", notation="absolute" + ) + + line = "X0278207Y0065293" + stmt = CoordinateStmt.from_excellon(line, settings) + assert stmt.x == 2.78207 + assert 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) + + settings = FileSettings( + format=(2, 4), zero_suppression="leading", units="inch", notation="absolute" + ) + + line = "X9660Y4639" + stmt = CoordinateStmt.from_excellon(line, settings) + assert stmt.x == 0.9660 + assert stmt.y == 0.4639 + assert stmt.to_excellon(settings) == "X9660Y4639" + assert stmt.units == "inch" + + settings.units = "metric" + stmt = CoordinateStmt.from_excellon(line, settings) + assert stmt.units == "metric" + + +def test_coordinatestmt_dump(): + """ Test CoordinateStmt to_excellon() + """ + lines = [ + "X278207Y65293", + "X243795", + "Y82528", + "Y86028", + "X251295Y81528", + "X2525Y78", + "X255Y575", + "Y52", + "X2675", + "Y575", + "X2425", + "Y52", + "X23", + ] + settings = FileSettings( + format=(2, 4), zero_suppression="leading", units="inch", notation="absolute" + ) + for line in lines: + stmt = CoordinateStmt.from_excellon(line, settings) + assert stmt.to_excellon(settings) == line + + +def test_coordinatestmt_conversion(): + + settings = FileSettings() + settings.units = "metric" + stmt = CoordinateStmt.from_excellon("X254Y254", settings) + + # No effect + stmt.to_metric() + assert stmt.x == 25.4 + assert stmt.y == 25.4 + + stmt.to_inch() + assert stmt.units == "inch" + assert stmt.x == 1.0 + assert stmt.y == 1.0 + + # No effect + stmt.to_inch() + assert stmt.x == 1.0 + assert stmt.y == 1.0 + + settings.units = "inch" + stmt = CoordinateStmt.from_excellon("X01Y01", settings) + + # No effect + stmt.to_inch() + assert stmt.x == 1.0 + assert stmt.y == 1.0 + + stmt.to_metric() + assert stmt.units == "metric" + assert stmt.x == 25.4 + assert stmt.y == 25.4 + + # No effect + stmt.to_metric() + assert stmt.x == 25.4 + assert stmt.y == 25.4 + + +def test_coordinatestmt_offset(): + stmt = CoordinateStmt.from_excellon("X01Y01", FileSettings()) + stmt.offset() + assert stmt.x == 1 + assert stmt.y == 1 + stmt.offset(1, 0) + assert stmt.x == 2.0 + assert stmt.y == 1.0 + stmt.offset(0, 1) + assert stmt.x == 2.0 + assert stmt.y == 2.0 + + +def test_coordinatestmt_string(): + settings = FileSettings( + format=(2, 4), zero_suppression="leading", units="inch", notation="absolute" + ) + stmt = CoordinateStmt.from_excellon("X9660Y4639", settings) + assert str(stmt) == "" + + +def test_repeathole_stmt_factory(): + stmt = RepeatHoleStmt.from_excellon( + "R0004X015Y32", FileSettings(zeros="leading", units="inch") + ) + assert stmt.count == 4 + assert stmt.xdelta == 1.5 + assert stmt.ydelta == 32 + assert stmt.units == "inch" + + stmt = RepeatHoleStmt.from_excellon( + "R0004X015Y32", FileSettings(zeros="leading", units="metric") + ) + assert stmt.units == "metric" + + +def test_repeatholestmt_dump(): + line = "R4X015Y32" + stmt = RepeatHoleStmt.from_excellon(line, FileSettings()) + assert stmt.to_excellon(FileSettings()) == line + + +def test_repeatholestmt_conversion(): + line = "R4X0254Y254" + settings = FileSettings() + settings.units = "metric" + stmt = RepeatHoleStmt.from_excellon(line, settings) + + # No effect + stmt.to_metric() + assert stmt.xdelta == 2.54 + assert stmt.ydelta == 25.4 + + stmt.to_inch() + assert stmt.units == "inch" + assert stmt.xdelta == 0.1 + assert stmt.ydelta == 1.0 + + # no effect + stmt.to_inch() + assert stmt.xdelta == 0.1 + assert stmt.ydelta == 1.0 + + line = "R4X01Y1" + settings.units = "inch" + stmt = RepeatHoleStmt.from_excellon(line, settings) + + # no effect + stmt.to_inch() + assert stmt.xdelta == 1.0 + assert stmt.ydelta == 10.0 + + stmt.to_metric() + assert stmt.units == "metric" + assert stmt.xdelta == 25.4 + assert stmt.ydelta == 254.0 + + # No effect + stmt.to_metric() + assert stmt.xdelta == 25.4 + assert stmt.ydelta == 254.0 + + +def test_repeathole_str(): + stmt = RepeatHoleStmt.from_excellon("R4X015Y32", FileSettings()) + assert str(stmt) == "" + + +def test_commentstmt_factory(): + """ Test CommentStmt factory method + """ + line = ";Layer_Color=9474304" + stmt = CommentStmt.from_excellon(line) + assert stmt.comment == line[1:] + + line = ";FILE_FORMAT=2:5" + stmt = CommentStmt.from_excellon(line) + assert stmt.comment == line[1:] + + line = ";TYPE=PLATED" + stmt = CommentStmt.from_excellon(line) + assert stmt.comment == line[1:] + + +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) + assert stmt.to_excellon() == line + + +def test_header_begin_stmt(): + stmt = HeaderBeginStmt() + assert stmt.to_excellon(None) == "M48" + + +def test_header_end_stmt(): + stmt = HeaderEndStmt() + assert stmt.to_excellon(None) == "M95" + + +def test_rewindstop_stmt(): + stmt = RewindStopStmt() + assert stmt.to_excellon(None) == "%" + + +def test_z_axis_rout_position_stmt(): + stmt = ZAxisRoutPositionStmt() + assert stmt.to_excellon(None) == "M15" + + +def test_retract_with_clamping_stmt(): + stmt = RetractWithClampingStmt() + assert stmt.to_excellon(None) == "M16" + + +def test_retract_without_clamping_stmt(): + stmt = RetractWithoutClampingStmt() + assert stmt.to_excellon(None) == "M17" + + +def test_cutter_compensation_off_stmt(): + stmt = CutterCompensationOffStmt() + assert stmt.to_excellon(None) == "G40" + + +def test_cutter_compensation_left_stmt(): + stmt = CutterCompensationLeftStmt() + assert stmt.to_excellon(None) == "G41" + + +def test_cutter_compensation_right_stmt(): + stmt = CutterCompensationRightStmt() + assert stmt.to_excellon(None) == "G42" + + +def test_endofprogramstmt_factory(): + settings = FileSettings(units="inch") + stmt = EndOfProgramStmt.from_excellon("M30X01Y02", settings) + assert stmt.x == 1.0 + assert stmt.y == 2.0 + assert stmt.units == "inch" + settings.units = "metric" + stmt = EndOfProgramStmt.from_excellon("M30X01", settings) + assert stmt.x == 1.0 + assert stmt.y == None + assert stmt.units == "metric" + stmt = EndOfProgramStmt.from_excellon("M30Y02", FileSettings()) + assert stmt.x == None + assert stmt.y == 2.0 + + +def test_endofprogramStmt_dump(): + lines = ["M30X01Y02"] + for line in lines: + stmt = EndOfProgramStmt.from_excellon(line, FileSettings()) + assert stmt.to_excellon(FileSettings()) == line + + +def test_endofprogramstmt_conversion(): + settings = FileSettings() + settings.units = "metric" + stmt = EndOfProgramStmt.from_excellon("M30X0254Y254", settings) + # No effect + stmt.to_metric() + assert stmt.x == 2.54 + assert stmt.y == 25.4 + + stmt.to_inch() + assert stmt.units == "inch" + assert stmt.x == 0.1 + assert stmt.y == 1.0 + + # No effect + stmt.to_inch() + assert stmt.x == 0.1 + assert stmt.y == 1.0 + + settings.units = "inch" + stmt = EndOfProgramStmt.from_excellon("M30X01Y1", settings) + + # No effect + stmt.to_inch() + assert stmt.x == 1.0 + assert stmt.y == 10.0 + + stmt.to_metric() + assert stmt.units == "metric" + assert stmt.x == 25.4 + assert stmt.y == 254.0 + + # No effect + stmt.to_metric() + assert stmt.x == 25.4 + assert stmt.y == 254.0 + + +def test_endofprogramstmt_offset(): + stmt = EndOfProgramStmt(1, 1) + stmt.offset() + assert stmt.x == 1 + assert stmt.y == 1 + stmt.offset(1, 0) + assert stmt.x == 2.0 + assert stmt.y == 1.0 + stmt.offset(0, 1) + assert stmt.x == 2.0 + assert stmt.y == 2.0 + + +def test_unitstmt_factory(): + """ Test UnitStmt factory method + """ + line = "INCH,LZ" + stmt = UnitStmt.from_excellon(line) + assert stmt.units == "inch" + assert stmt.zeros == "leading" + + line = "INCH,TZ" + stmt = UnitStmt.from_excellon(line) + assert stmt.units == "inch" + assert stmt.zeros == "trailing" + + line = "METRIC,LZ" + stmt = UnitStmt.from_excellon(line) + assert stmt.units == "metric" + assert stmt.zeros == "leading" + + line = "METRIC,TZ" + stmt = UnitStmt.from_excellon(line) + assert stmt.units == "metric" + assert stmt.zeros == "trailing" + + +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) + assert stmt.to_excellon() == line + + +def test_unitstmt_conversion(): + stmt = UnitStmt.from_excellon("METRIC,TZ") + stmt.to_inch() + assert stmt.units == "inch" + + stmt = UnitStmt.from_excellon("INCH,TZ") + stmt.to_metric() + assert stmt.units == "metric" + + +def test_incrementalmode_factory(): + """ Test IncrementalModeStmt factory method + """ + line = "ICI,ON" + stmt = IncrementalModeStmt.from_excellon(line) + assert stmt.mode == "on" + + line = "ICI,OFF" + stmt = IncrementalModeStmt.from_excellon(line) + assert stmt.mode == "off" + + +def test_incrementalmode_dump(): + """ Test IncrementalModeStmt to_excellon() + """ + lines = ["ICI,ON", "ICI,OFF"] + for line in lines: + stmt = IncrementalModeStmt.from_excellon(line) + assert stmt.to_excellon() == line + + +def test_incrementalmode_validation(): + """ Test IncrementalModeStmt input validation + """ + pytest.raises(ValueError, IncrementalModeStmt, "OFF-ISH") + + +def test_versionstmt_factory(): + """ Test VersionStmt factory method + """ + line = "VER,1" + stmt = VersionStmt.from_excellon(line) + assert stmt.version == 1 + + line = "VER,2" + stmt = VersionStmt.from_excellon(line) + assert stmt.version == 2 + + +def test_versionstmt_dump(): + """ Test VersionStmt to_excellon() + """ + lines = ["VER,1", "VER,2"] + for line in lines: + stmt = VersionStmt.from_excellon(line) + assert stmt.to_excellon() == line + + +def test_versionstmt_validation(): + """ Test VersionStmt input validation + """ + pytest.raises(ValueError, VersionStmt, 3) + + +def test_formatstmt_factory(): + """ Test FormatStmt factory method + """ + line = "FMAT,1" + stmt = FormatStmt.from_excellon(line) + assert stmt.format == 1 + + line = "FMAT,2" + stmt = FormatStmt.from_excellon(line) + assert stmt.format == 2 + + +def test_formatstmt_dump(): + """ Test FormatStmt to_excellon() + """ + lines = ["FMAT,1", "FMAT,2"] + for line in lines: + stmt = FormatStmt.from_excellon(line) + assert stmt.to_excellon() == line + + +def test_formatstmt_validation(): + """ Test FormatStmt input validation + """ + pytest.raises(ValueError, FormatStmt, 3) + + +def test_linktoolstmt_factory(): + """ Test LinkToolStmt factory method + """ + line = "1/2/3/4" + stmt = LinkToolStmt.from_excellon(line) + assert stmt.linked_tools == [1, 2, 3, 4] + + line = "01/02/03/04" + stmt = LinkToolStmt.from_excellon(line) + assert stmt.linked_tools == [1, 2, 3, 4] + + +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 stmt.to_excellon() == line + + +def test_measmodestmt_factory(): + """ Test MeasuringModeStmt factory method + """ + line = "M72" + stmt = MeasuringModeStmt.from_excellon(line) + assert stmt.units == "inch" + + line = "M71" + stmt = MeasuringModeStmt.from_excellon(line) + assert stmt.units == "metric" + + +def test_measmodestmt_dump(): + """ Test MeasuringModeStmt to_excellon() + """ + lines = ["M71", "M72"] + for line in lines: + stmt = MeasuringModeStmt.from_excellon(line) + assert stmt.to_excellon() == line + + +def test_measmodestmt_validation(): + """ Test MeasuringModeStmt input validation + """ + pytest.raises(ValueError, MeasuringModeStmt.from_excellon, "M70") + pytest.raises(ValueError, MeasuringModeStmt, "millimeters") + + +def test_measmodestmt_conversion(): + line = "M72" + stmt = MeasuringModeStmt.from_excellon(line) + assert stmt.units == "inch" + stmt.to_metric() + assert stmt.units == "metric" + + line = "M71" + stmt = MeasuringModeStmt.from_excellon(line) + assert stmt.units == "metric" + stmt.to_inch() + assert stmt.units == "inch" + + +def test_routemode_stmt(): + stmt = RouteModeStmt() + assert stmt.to_excellon(FileSettings()) == "G00" + + +def test_linearmode_stmt(): + stmt = LinearModeStmt() + assert stmt.to_excellon(FileSettings()) == "G01" + + +def test_drillmode_stmt(): + stmt = DrillModeStmt() + assert stmt.to_excellon(FileSettings()) == "G05" + + +def test_absolutemode_stmt(): + stmt = AbsoluteModeStmt() + assert stmt.to_excellon(FileSettings()) == "G90" + + +def test_unknownstmt(): + stmt = UnknownStmt("TEST") + assert stmt.stmt == "TEST" + assert str(stmt) == "" + + +def test_unknownstmt_dump(): + stmt = UnknownStmt("TEST") + assert stmt.to_excellon(FileSettings()) == "TEST" diff --git a/gerbonara/gerber/tests/test_gerber_statements.py b/gerbonara/gerber/tests/test_gerber_statements.py new file mode 100644 index 0000000..140cbd1 --- /dev/null +++ b/gerbonara/gerber/tests/test_gerber_statements.py @@ -0,0 +1,959 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +import pytest +from ..gerber_statements import * +from ..cam import FileSettings + + +def test_Statement_smoketest(): + stmt = Statement("Test") + assert stmt.type == "Test" + stmt.to_metric() + assert "units=metric" in str(stmt) + stmt.to_inch() + assert "units=inch" in str(stmt) + stmt.to_metric() + stmt.offset(1, 1) + assert "type=Test" in str(stmt) + + +def test_FSParamStmt_factory(): + """ Test FSParamStruct factory + """ + stmt = {"param": "FS", "zero": "L", "notation": "A", "x": "27"} + fs = FSParamStmt.from_dict(stmt) + assert fs.param == "FS" + assert fs.zero_suppression == "leading" + assert fs.notation == "absolute" + assert fs.format == (2, 7) + + stmt = {"param": "FS", "zero": "T", "notation": "I", "x": "27"} + fs = FSParamStmt.from_dict(stmt) + assert fs.param == "FS" + assert fs.zero_suppression == "trailing" + assert fs.notation == "incremental" + assert 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 stmt.param == param + assert stmt.zero_suppression == zeros + assert stmt.notation == notation + assert stmt.format == fmt + + +def test_FSParamStmt_dump(): + """ Test FSParamStmt to_gerber() + """ + stmt = {"param": "FS", "zero": "L", "notation": "A", "x": "27"} + fs = FSParamStmt.from_dict(stmt) + assert fs.to_gerber() == "%FSLAX27Y27*%" + + stmt = {"param": "FS", "zero": "T", "notation": "I", "x": "25"} + fs = FSParamStmt.from_dict(stmt) + assert fs.to_gerber() == "%FSTIX25Y25*%" + + settings = FileSettings(zero_suppression="leading", notation="absolute") + assert fs.to_gerber(settings) == "%FSLAX25Y25*%" + + +def test_FSParamStmt_string(): + """ Test FSParamStmt.__str__() + """ + stmt = {"param": "FS", "zero": "L", "notation": "A", "x": "27"} + fs = FSParamStmt.from_dict(stmt) + assert str(fs) == "" + + stmt = {"param": "FS", "zero": "T", "notation": "I", "x": "25"} + fs = FSParamStmt.from_dict(stmt) + assert ( + str(fs) == "" + ) + + +def test_MOParamStmt_factory(): + """ Test MOParamStruct factory + """ + stmts = [{"param": "MO", "mo": "IN"}, {"param": "MO", "mo": "in"}] + for stmt in stmts: + mo = MOParamStmt.from_dict(stmt) + assert mo.param == "MO" + assert mo.mode == "inch" + + stmts = [{"param": "MO", "mo": "MM"}, {"param": "MO", "mo": "mm"}] + for stmt in stmts: + mo = MOParamStmt.from_dict(stmt) + assert mo.param == "MO" + assert mo.mode == "metric" + + stmt = {"param": "MO"} + mo = MOParamStmt.from_dict(stmt) + assert mo.mode == None + stmt = {"param": "MO", "mo": "degrees kelvin"} + pytest.raises(ValueError, MOParamStmt.from_dict, stmt) + + +def test_MOParamStmt(): + """ Test MOParamStmt initialization + """ + param = "MO" + mode = "inch" + stmt = MOParamStmt(param, mode) + assert stmt.param == param + + for mode in ["inch", "metric"]: + stmt = MOParamStmt(param, mode) + assert stmt.mode == mode + + +def test_MOParamStmt_dump(): + """ Test MOParamStmt to_gerber() + """ + stmt = {"param": "MO", "mo": "IN"} + mo = MOParamStmt.from_dict(stmt) + assert mo.to_gerber() == "%MOIN*%" + + stmt = {"param": "MO", "mo": "MM"} + mo = MOParamStmt.from_dict(stmt) + assert mo.to_gerber() == "%MOMM*%" + + +def test_MOParamStmt_conversion(): + stmt = {"param": "MO", "mo": "MM"} + mo = MOParamStmt.from_dict(stmt) + mo.to_inch() + assert mo.mode == "inch" + + stmt = {"param": "MO", "mo": "IN"} + mo = MOParamStmt.from_dict(stmt) + mo.to_metric() + assert mo.mode == "metric" + + +def test_MOParamStmt_string(): + """ Test MOParamStmt.__str__() + """ + stmt = {"param": "MO", "mo": "IN"} + mo = MOParamStmt.from_dict(stmt) + assert str(mo) == "" + + stmt = {"param": "MO", "mo": "MM"} + mo = MOParamStmt.from_dict(stmt) + assert str(mo) == "" + + +def test_IPParamStmt_factory(): + """ Test IPParamStruct factory + """ + stmt = {"param": "IP", "ip": "POS"} + ip = IPParamStmt.from_dict(stmt) + assert ip.ip == "positive" + + stmt = {"param": "IP", "ip": "NEG"} + ip = IPParamStmt.from_dict(stmt) + assert ip.ip == "negative" + + +def test_IPParamStmt(): + """ Test IPParamStmt initialization + """ + param = "IP" + for ip in ["positive", "negative"]: + stmt = IPParamStmt(param, ip) + assert stmt.param == param + assert stmt.ip == ip + + +def test_IPParamStmt_dump(): + """ Test IPParamStmt to_gerber() + """ + stmt = {"param": "IP", "ip": "POS"} + ip = IPParamStmt.from_dict(stmt) + assert ip.to_gerber() == "%IPPOS*%" + + stmt = {"param": "IP", "ip": "NEG"} + ip = IPParamStmt.from_dict(stmt) + assert ip.to_gerber() == "%IPNEG*%" + + +def test_IPParamStmt_string(): + stmt = {"param": "IP", "ip": "POS"} + ip = IPParamStmt.from_dict(stmt) + assert str(ip) == "" + + stmt = {"param": "IP", "ip": "NEG"} + ip = IPParamStmt.from_dict(stmt) + assert str(ip) == "" + + +def test_IRParamStmt_factory(): + stmt = {"param": "IR", "angle": "45"} + ir = IRParamStmt.from_dict(stmt) + assert ir.param == "IR" + assert ir.angle == 45 + + +def test_IRParamStmt_dump(): + stmt = {"param": "IR", "angle": "45"} + ir = IRParamStmt.from_dict(stmt) + assert ir.to_gerber() == "%IR45*%" + + +def test_IRParamStmt_string(): + stmt = {"param": "IR", "angle": "45"} + ir = IRParamStmt.from_dict(stmt) + assert str(ir) == "" + + +def test_OFParamStmt_factory(): + """ Test OFParamStmt factory + """ + stmt = {"param": "OF", "a": "0.1234567", "b": "0.1234567"} + of = OFParamStmt.from_dict(stmt) + assert of.a == 0.1234567 + assert 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 stmt.param == param + assert stmt.a == val + assert stmt.b == val + + +def test_OFParamStmt_dump(): + """ Test OFParamStmt to_gerber() + """ + stmt = {"param": "OF", "a": "0.123456", "b": "0.123456"} + of = OFParamStmt.from_dict(stmt) + assert of.to_gerber() == "%OFA0.12345B0.12345*%" + + +def test_OFParamStmt_conversion(): + stmt = {"param": "OF", "a": "2.54", "b": "25.4"} + of = OFParamStmt.from_dict(stmt) + of.units = "metric" + + # No effect + of.to_metric() + assert of.a == 2.54 + assert of.b == 25.4 + + of.to_inch() + assert of.units == "inch" + assert of.a == 0.1 + assert of.b == 1.0 + + # No effect + of.to_inch() + assert of.a == 0.1 + assert of.b == 1.0 + + stmt = {"param": "OF", "a": "0.1", "b": "1.0"} + of = OFParamStmt.from_dict(stmt) + of.units = "inch" + + # No effect + of.to_inch() + assert of.a == 0.1 + assert of.b == 1.0 + + of.to_metric() + assert of.units == "metric" + assert of.a == 2.54 + assert of.b == 25.4 + + # No effect + of.to_metric() + assert of.a == 2.54 + assert of.b == 25.4 + + +def test_OFParamStmt_offset(): + s = OFParamStmt("OF", 0, 0) + s.offset(1, 0) + assert s.a == 1.0 + assert s.b == 0.0 + s.offset(0, 1) + assert s.a == 1.0 + assert s.b == 1.0 + + +def test_OFParamStmt_string(): + """ Test OFParamStmt __str__ + """ + stmt = {"param": "OF", "a": "0.123456", "b": "0.123456"} + of = OFParamStmt.from_dict(stmt) + assert str(of) == "" + + +def test_SFParamStmt_factory(): + stmt = {"param": "SF", "a": "1.4", "b": "0.9"} + sf = SFParamStmt.from_dict(stmt) + assert sf.param == "SF" + assert sf.a == 1.4 + assert sf.b == 0.9 + + +def test_SFParamStmt_dump(): + stmt = {"param": "SF", "a": "1.4", "b": "0.9"} + sf = SFParamStmt.from_dict(stmt) + assert sf.to_gerber() == "%SFA1.4B0.9*%" + + +def test_SFParamStmt_conversion(): + stmt = {"param": "OF", "a": "2.54", "b": "25.4"} + of = SFParamStmt.from_dict(stmt) + of.units = "metric" + of.to_metric() + + # No effect + assert of.a == 2.54 + assert of.b == 25.4 + + of.to_inch() + assert of.units == "inch" + assert of.a == 0.1 + assert of.b == 1.0 + + # No effect + of.to_inch() + assert of.a == 0.1 + assert of.b == 1.0 + + stmt = {"param": "OF", "a": "0.1", "b": "1.0"} + of = SFParamStmt.from_dict(stmt) + of.units = "inch" + + # No effect + of.to_inch() + assert of.a == 0.1 + assert of.b == 1.0 + + of.to_metric() + assert of.units == "metric" + assert of.a == 2.54 + assert of.b == 25.4 + + # No effect + of.to_metric() + assert of.a == 2.54 + assert of.b == 25.4 + + +def test_SFParamStmt_offset(): + s = SFParamStmt("OF", 0, 0) + s.offset(1, 0) + assert s.a == 1.0 + assert s.b == 0.0 + s.offset(0, 1) + assert s.a == 1.0 + assert s.b == 1.0 + + +def test_SFParamStmt_string(): + stmt = {"param": "SF", "a": "1.4", "b": "0.9"} + sf = SFParamStmt.from_dict(stmt) + assert str(sf) == "" + + +def test_LPParamStmt_factory(): + """ Test LPParamStmt factory + """ + stmt = {"param": "LP", "lp": "C"} + lp = LPParamStmt.from_dict(stmt) + assert lp.lp == "clear" + + stmt = {"param": "LP", "lp": "D"} + lp = LPParamStmt.from_dict(stmt) + assert lp.lp == "dark" + + +def test_LPParamStmt_dump(): + """ Test LPParamStmt to_gerber() + """ + stmt = {"param": "LP", "lp": "C"} + lp = LPParamStmt.from_dict(stmt) + assert lp.to_gerber() == "%LPC*%" + + stmt = {"param": "LP", "lp": "D"} + lp = LPParamStmt.from_dict(stmt) + assert lp.to_gerber() == "%LPD*%" + + +def test_LPParamStmt_string(): + """ Test LPParamStmt.__str__() + """ + stmt = {"param": "LP", "lp": "D"} + lp = LPParamStmt.from_dict(stmt) + assert str(lp) == "" + + stmt = {"param": "LP", "lp": "C"} + lp = LPParamStmt.from_dict(stmt) + assert str(lp) == "" + + +def test_AMParamStmt_factory(): + name = "DONUTVAR" + macro = """0 Test Macro. * +1,1,1.5,0,0* +20,1,0.9,0,0.45,12,0.45,0* +21,1,6.8,1.2,3.4,0.6,0* +22,1,6.8,1.2,0,0,0* +4,1,4,0.1,0.1,0.5,0.1,0.5,0.5,0.1,0.5,0.1,0.1,0* +5,1,8,0,0,8,0* +6,0,0,5,0.5,0.5,2,0.1,6,0* +7,0,0,7,6,0.2,0* +8,THIS IS AN UNSUPPORTED PRIMITIVE* +""" + s = AMParamStmt.from_dict({"param": "AM", "name": name, "macro": macro}) + s.build() + assert len(s.primitives) == 10 + assert isinstance(s.primitives[0], AMCommentPrimitive) + assert isinstance(s.primitives[1], AMCirclePrimitive) + assert isinstance(s.primitives[2], AMVectorLinePrimitive) + assert isinstance(s.primitives[3], AMCenterLinePrimitive) + assert isinstance(s.primitives[4], AMLowerLeftLinePrimitive) + assert isinstance(s.primitives[5], AMOutlinePrimitive) + assert isinstance(s.primitives[6], AMPolygonPrimitive) + assert isinstance(s.primitives[7], AMMoirePrimitive) + assert isinstance(s.primitives[8], AMThermalPrimitive) + assert isinstance(s.primitives[9], AMUnsupportPrimitive) + + +def testAMParamStmt_conversion(): + name = "POLYGON" + macro = "5,1,8,25.4,25.4,25.4,0*" + s = AMParamStmt.from_dict({"param": "AM", "name": name, "macro": macro}) + + s.build() + s.units = "metric" + + # No effect + s.to_metric() + assert s.primitives[0].position == (25.4, 25.4) + assert s.primitives[0].diameter == 25.4 + + s.to_inch() + assert s.units == "inch" + assert s.primitives[0].position == (1.0, 1.0) + assert s.primitives[0].diameter == 1.0 + + # No effect + s.to_inch() + assert s.primitives[0].position == (1.0, 1.0) + assert s.primitives[0].diameter == 1.0 + + macro = "5,1,8,1,1,1,0*" + s = AMParamStmt.from_dict({"param": "AM", "name": name, "macro": macro}) + s.build() + s.units = "inch" + + # No effect + s.to_inch() + assert s.primitives[0].position == (1.0, 1.0) + assert s.primitives[0].diameter == 1.0 + + s.to_metric() + assert s.units == "metric" + assert s.primitives[0].position == (25.4, 25.4) + assert s.primitives[0].diameter == 25.4 + + # No effect + s.to_metric() + assert s.primitives[0].position == (25.4, 25.4) + assert s.primitives[0].diameter == 25.4 + + +def test_AMParamStmt_dump(): + name = "POLYGON" + macro = "5,1,8,25.4,25.4,25.4,0.0" + s = AMParamStmt.from_dict({"param": "AM", "name": name, "macro": macro}) + s.build() + assert s.to_gerber() == "%AMPOLYGON*5,1,8,25.4,25.4,25.4,0.0*%" + + # TODO - Store Equations and update on unit change... + s = AMParamStmt.from_dict( + {"param": "AM", "name": "OC8", "macro": "5,1,8,0,0,1.08239X$1,22.5"} + ) + s.build() + # assert_equal(s.to_gerber(), '%AMOC8*5,1,8,0,0,1.08239X$1,22.5*%') + assert s.to_gerber() == "%AMOC8*5,1,8,0,0,0,22.5*%" + + +def test_AMParamStmt_string(): + name = "POLYGON" + macro = "5,1,8,25.4,25.4,25.4,0*" + s = AMParamStmt.from_dict({"param": "AM", "name": name, "macro": macro}) + s.build() + assert str(s) == "" + + +def test_ASParamStmt_factory(): + stmt = {"param": "AS", "mode": "AXBY"} + s = ASParamStmt.from_dict(stmt) + assert s.param == "AS" + assert s.mode == "AXBY" + + +def test_ASParamStmt_dump(): + stmt = {"param": "AS", "mode": "AXBY"} + s = ASParamStmt.from_dict(stmt) + assert s.to_gerber() == "%ASAXBY*%" + + +def test_ASParamStmt_string(): + stmt = {"param": "AS", "mode": "AXBY"} + s = ASParamStmt.from_dict(stmt) + assert str(s) == "" + + +def test_INParamStmt_factory(): + """ Test INParamStmt factory + """ + stmt = {"param": "IN", "name": "test"} + inp = INParamStmt.from_dict(stmt) + assert inp.name == "test" + + +def test_INParamStmt_dump(): + """ Test INParamStmt to_gerber() + """ + stmt = {"param": "IN", "name": "test"} + inp = INParamStmt.from_dict(stmt) + assert inp.to_gerber() == "%INtest*%" + + +def test_INParamStmt_string(): + stmt = {"param": "IN", "name": "test"} + inp = INParamStmt.from_dict(stmt) + assert str(inp) == "" + + +def test_LNParamStmt_factory(): + """ Test LNParamStmt factory + """ + stmt = {"param": "LN", "name": "test"} + lnp = LNParamStmt.from_dict(stmt) + assert lnp.name == "test" + + +def test_LNParamStmt_dump(): + """ Test LNParamStmt to_gerber() + """ + stmt = {"param": "LN", "name": "test"} + lnp = LNParamStmt.from_dict(stmt) + assert lnp.to_gerber() == "%LNtest*%" + + +def test_LNParamStmt_string(): + stmt = {"param": "LN", "name": "test"} + lnp = LNParamStmt.from_dict(stmt) + assert str(lnp) == "" + + +def test_comment_stmt(): + """ Test comment statement + """ + stmt = CommentStmt("A comment") + assert stmt.type == "COMMENT" + assert stmt.comment == "A comment" + + +def test_comment_stmt_dump(): + """ Test CommentStmt to_gerber() + """ + stmt = CommentStmt("A comment") + assert stmt.to_gerber() == "G04A comment*" + + +def test_comment_stmt_string(): + stmt = CommentStmt("A comment") + assert str(stmt) == "" + + +def test_eofstmt(): + """ Test EofStmt + """ + stmt = EofStmt() + assert stmt.type == "EOF" + + +def test_eofstmt_dump(): + """ Test EofStmt to_gerber() + """ + stmt = EofStmt() + assert stmt.to_gerber() == "M02*" + + +def test_eofstmt_string(): + assert str(EofStmt()) == "" + + +def test_quadmodestmt_factory(): + """ Test QuadrantModeStmt.from_gerber() + """ + line = "G74*" + stmt = QuadrantModeStmt.from_gerber(line) + assert stmt.type == "QuadrantMode" + assert stmt.mode == "single-quadrant" + + line = "G75*" + stmt = QuadrantModeStmt.from_gerber(line) + assert stmt.mode == "multi-quadrant" + + +def test_quadmodestmt_validation(): + """ Test QuadrantModeStmt input validation + """ + line = "G76*" + pytest.raises(ValueError, QuadrantModeStmt.from_gerber, line) + pytest.raises(ValueError, QuadrantModeStmt, "quadrant-ful") + + +def test_quadmodestmt_dump(): + """ Test QuadrantModeStmt.to_gerber() + """ + for line in ("G74*", "G75*"): + stmt = QuadrantModeStmt.from_gerber(line) + assert stmt.to_gerber() == line + + +def test_regionmodestmt_factory(): + """ Test RegionModeStmt.from_gerber() + """ + line = "G36*" + stmt = RegionModeStmt.from_gerber(line) + assert stmt.type == "RegionMode" + assert stmt.mode == "on" + + line = "G37*" + stmt = RegionModeStmt.from_gerber(line) + assert stmt.mode == "off" + + +def test_regionmodestmt_validation(): + """ Test RegionModeStmt input validation + """ + line = "G38*" + pytest.raises(ValueError, RegionModeStmt.from_gerber, line) + pytest.raises(ValueError, RegionModeStmt, "off-ish") + + +def test_regionmodestmt_dump(): + """ Test RegionModeStmt.to_gerber() + """ + for line in ("G36*", "G37*"): + stmt = RegionModeStmt.from_gerber(line) + assert stmt.to_gerber() == line + + +def test_unknownstmt(): + """ Test UnknownStmt + """ + line = "G696969*" + stmt = UnknownStmt(line) + assert stmt.type == "UNKNOWN" + assert stmt.line == line + + +def test_unknownstmt_dump(): + """ Test UnknownStmt.to_gerber() + """ + lines = ("G696969*", "M03*") + for line in lines: + stmt = UnknownStmt(line) + assert stmt.to_gerber() == line + + +def test_statement_string(): + """ Test Statement.__str__() + """ + stmt = Statement("PARAM") + assert "type=PARAM" in str(stmt) + stmt.test = "PASS" + assert "test=PASS" in str(stmt) + assert "type=PARAM" in str(stmt) + + +def test_ADParamStmt_factory(): + """ Test ADParamStmt factory + """ + stmt = {"param": "AD", "d": 0, "shape": "C"} + ad = ADParamStmt.from_dict(stmt) + assert ad.d == 0 + assert ad.shape == "C" + + stmt = {"param": "AD", "d": 1, "shape": "R"} + ad = ADParamStmt.from_dict(stmt) + assert ad.d == 1 + assert ad.shape == "R" + + stmt = {"param": "AD", "d": 1, "shape": "C", "modifiers": "1.42"} + ad = ADParamStmt.from_dict(stmt) + assert ad.d == 1 + assert ad.shape == "C" + assert ad.modifiers == [(1.42,)] + + stmt = {"param": "AD", "d": 1, "shape": "C", "modifiers": "1.42X"} + ad = ADParamStmt.from_dict(stmt) + assert ad.d == 1 + assert ad.shape == "C" + assert ad.modifiers == [(1.42,)] + + stmt = {"param": "AD", "d": 1, "shape": "R", "modifiers": "1.42X1.24"} + ad = ADParamStmt.from_dict(stmt) + assert ad.d == 1 + assert ad.shape == "R" + assert ad.modifiers == [(1.42, 1.24)] + + +def test_ADParamStmt_conversion(): + stmt = {"param": "AD", "d": 0, "shape": "C", "modifiers": "25.4X25.4,25.4X25.4"} + ad = ADParamStmt.from_dict(stmt) + ad.units = "metric" + + # No effect + ad.to_metric() + assert ad.modifiers[0] == (25.4, 25.4) + assert ad.modifiers[1] == (25.4, 25.4) + + ad.to_inch() + assert ad.units == "inch" + assert ad.modifiers[0] == (1.0, 1.0) + assert ad.modifiers[1] == (1.0, 1.0) + + # No effect + ad.to_inch() + assert ad.modifiers[0] == (1.0, 1.0) + assert ad.modifiers[1] == (1.0, 1.0) + + stmt = {"param": "AD", "d": 0, "shape": "C", "modifiers": "1X1,1X1"} + ad = ADParamStmt.from_dict(stmt) + ad.units = "inch" + + # No effect + ad.to_inch() + assert ad.modifiers[0] == (1.0, 1.0) + assert ad.modifiers[1] == (1.0, 1.0) + + ad.to_metric() + assert ad.modifiers[0] == (25.4, 25.4) + assert ad.modifiers[1] == (25.4, 25.4) + + # No effect + ad.to_metric() + assert ad.modifiers[0] == (25.4, 25.4) + assert ad.modifiers[1] == (25.4, 25.4) + + +def test_ADParamStmt_dump(): + stmt = {"param": "AD", "d": 0, "shape": "C"} + ad = ADParamStmt.from_dict(stmt) + assert ad.to_gerber() == "%ADD0C*%" + stmt = {"param": "AD", "d": 0, "shape": "C", "modifiers": "1X1,1X1"} + ad = ADParamStmt.from_dict(stmt) + assert ad.to_gerber() == "%ADD0C,1X1,1X1*%" + + +def test_ADPamramStmt_string(): + stmt = {"param": "AD", "d": 0, "shape": "C"} + ad = ADParamStmt.from_dict(stmt) + assert str(ad) == "" + + stmt = {"param": "AD", "d": 0, "shape": "R"} + ad = ADParamStmt.from_dict(stmt) + assert str(ad) == "" + + stmt = {"param": "AD", "d": 0, "shape": "O"} + ad = ADParamStmt.from_dict(stmt) + assert str(ad) == "" + + stmt = {"param": "AD", "d": 0, "shape": "test"} + ad = ADParamStmt.from_dict(stmt) + assert str(ad) == "" + + +def test_MIParamStmt_factory(): + stmt = {"param": "MI", "a": 1, "b": 1} + mi = MIParamStmt.from_dict(stmt) + assert mi.a == 1 + assert mi.b == 1 + + +def test_MIParamStmt_dump(): + stmt = {"param": "MI", "a": 1, "b": 1} + mi = MIParamStmt.from_dict(stmt) + assert mi.to_gerber() == "%MIA1B1*%" + stmt = {"param": "MI", "a": 1} + mi = MIParamStmt.from_dict(stmt) + assert mi.to_gerber() == "%MIA1B0*%" + stmt = {"param": "MI", "b": 1} + mi = MIParamStmt.from_dict(stmt) + assert mi.to_gerber() == "%MIA0B1*%" + + +def test_MIParamStmt_string(): + stmt = {"param": "MI", "a": 1, "b": 1} + mi = MIParamStmt.from_dict(stmt) + assert str(mi) == "" + + stmt = {"param": "MI", "b": 1} + mi = MIParamStmt.from_dict(stmt) + assert str(mi) == "" + + stmt = {"param": "MI", "a": 1} + mi = MIParamStmt.from_dict(stmt) + assert str(mi) == "" + + +def test_coordstmt_ctor(): + cs = CoordStmt("G04", 0.0, 0.1, 0.2, 0.3, "D01", FileSettings()) + assert cs.function == "G04" + assert cs.x == 0.0 + assert cs.y == 0.1 + assert cs.i == 0.2 + assert cs.j == 0.3 + assert cs.op == "D01" + + +def test_coordstmt_factory(): + stmt = { + "function": "G04", + "x": "0", + "y": "001", + "i": "002", + "j": "003", + "op": "D01", + } + cs = CoordStmt.from_dict(stmt, FileSettings()) + assert cs.function == "G04" + assert cs.x == 0.0 + assert cs.y == 0.1 + assert cs.i == 0.2 + assert cs.j == 0.3 + assert cs.op == "D01" + + +def test_coordstmt_dump(): + cs = CoordStmt("G04", 0.0, 0.1, 0.2, 0.3, "D01", FileSettings()) + assert cs.to_gerber(FileSettings()) == "G04X0Y001I002J003D01*" + + +def test_coordstmt_conversion(): + cs = CoordStmt("G71", 25.4, 25.4, 25.4, 25.4, "D01", FileSettings()) + cs.units = "metric" + + # No effect + cs.to_metric() + assert cs.x == 25.4 + assert cs.y == 25.4 + assert cs.i == 25.4 + assert cs.j == 25.4 + assert cs.function == "G71" + + cs.to_inch() + assert cs.units == "inch" + assert cs.x == 1.0 + assert cs.y == 1.0 + assert cs.i == 1.0 + assert cs.j == 1.0 + assert cs.function == "G70" + + # No effect + cs.to_inch() + assert cs.x == 1.0 + assert cs.y == 1.0 + assert cs.i == 1.0 + assert cs.j == 1.0 + assert cs.function == "G70" + + cs = CoordStmt("G70", 1.0, 1.0, 1.0, 1.0, "D01", FileSettings()) + cs.units = "inch" + + # No effect + cs.to_inch() + assert cs.x == 1.0 + assert cs.y == 1.0 + assert cs.i == 1.0 + assert cs.j == 1.0 + assert cs.function == "G70" + + cs.to_metric() + assert cs.x == 25.4 + assert cs.y == 25.4 + assert cs.i == 25.4 + assert cs.j == 25.4 + assert cs.function == "G71" + + # No effect + cs.to_metric() + assert cs.x == 25.4 + assert cs.y == 25.4 + assert cs.i == 25.4 + assert cs.j == 25.4 + assert cs.function == "G71" + + +def test_coordstmt_offset(): + c = CoordStmt("G71", 0, 0, 0, 0, "D01", FileSettings()) + c.offset(1, 0) + assert c.x == 1.0 + assert c.y == 0.0 + assert c.i == 1.0 + assert c.j == 0.0 + c.offset(0, 1) + assert c.x == 1.0 + assert c.y == 1.0 + assert c.i == 1.0 + assert c.j == 1.0 + + +def test_coordstmt_string(): + cs = CoordStmt("G04", 0, 1, 2, 3, "D01", FileSettings()) + assert ( + str(cs) == "" + ) + cs = CoordStmt("G04", None, None, None, None, "D02", FileSettings()) + assert str(cs) == "" + cs = CoordStmt("G04", None, None, None, None, "D03", FileSettings()) + assert str(cs) == "" + cs = CoordStmt("G04", None, None, None, None, "TEST", FileSettings()) + assert str(cs) == "" + + +def test_aperturestmt_ctor(): + ast = ApertureStmt(3, False) + assert ast.d == 3 + assert ast.deprecated == False + ast = ApertureStmt(4, True) + assert ast.d == 4 + assert ast.deprecated == True + ast = ApertureStmt(4, 1) + assert ast.d == 4 + assert ast.deprecated == True + ast = ApertureStmt(3) + assert ast.d == 3 + assert ast.deprecated == False + + +def test_aperturestmt_dump(): + ast = ApertureStmt(3, False) + assert ast.to_gerber() == "D3*" + ast = ApertureStmt(3, True) + assert ast.to_gerber() == "G54D3*" + assert str(ast) == "" diff --git a/gerbonara/gerber/tests/test_ipc356.py b/gerbonara/gerber/tests/test_ipc356.py new file mode 100644 index 0000000..77f0782 --- /dev/null +++ b/gerbonara/gerber/tests/test_ipc356.py @@ -0,0 +1,148 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +import pytest +from ..ipc356 import * +from ..cam import FileSettings + +import os + +IPC_D_356_FILE = os.path.join(os.path.dirname(__file__), "resources/ipc-d-356.ipc") + + +def test_read(): + ipcfile = read(IPC_D_356_FILE) + assert isinstance(ipcfile, IPCNetlist) + + +def test_parser(): + ipcfile = read(IPC_D_356_FILE) + assert ipcfile.settings.units == "inch" + assert ipcfile.settings.angle_units == "degrees" + assert len(ipcfile.comments) == 3 + assert len(ipcfile.parameters) == 4 + assert len(ipcfile.test_records) == 105 + assert len(ipcfile.components) == 21 + assert len(ipcfile.vias) == 14 + assert ipcfile.test_records[-1].net_name == "A_REALLY_LONG_NET_NAME" + assert ipcfile.outlines[0].type == "BOARD_EDGE" + assert set(ipcfile.outlines[0].points) == { + (0.0, 0.0), + (2.25, 0.0), + (2.25, 1.5), + (0.0, 1.5), + (0.13, 0.024), + } + + +def test_comment(): + c = IPC356_Comment("Layer Stackup:") + assert c.comment == "Layer Stackup:" + c = IPC356_Comment.from_line("C Layer Stackup: ") + assert c.comment == "Layer Stackup:" + pytest.raises(ValueError, IPC356_Comment.from_line, "P JOB") + assert str(c) == "" + + +def test_parameter(): + p = IPC356_Parameter("VER", "IPC-D-356A") + assert p.parameter == "VER" + assert p.value == "IPC-D-356A" + p = IPC356_Parameter.from_line("P VER IPC-D-356A ") + assert p.parameter == "VER" + assert p.value == "IPC-D-356A" + pytest.raises(ValueError, IPC356_Parameter.from_line, "C Layer Stackup: ") + assert str(p) == "" + + +def test_eof(): + e = IPC356_EndOfFile() + assert e.to_netlist() == "999" + assert str(e) == "" + + +def test_outline(): + type = "BOARD_EDGE" + points = [(0.01, 0.01), (2.0, 2.0), (4.0, 2.0), (4.0, 6.0)] + b = IPC356_Outline(type, points) + assert b.type == type + assert b.points == points + b = IPC356_Outline.from_line( + "389BOARD_EDGE X100Y100 X20000Y20000 X40000 Y60000", + FileSettings(units="inch"), + ) + assert b.type == "BOARD_EDGE" + assert b.points == points + + +def test_test_record(): + pytest.raises(ValueError, IPC356_TestRecord.from_line, "P JOB", FileSettings()) + record_string = ( + "317+5VDC VIA - D0150PA00X 006647Y 012900X0000 S3" + ) + r = IPC356_TestRecord.from_line(record_string, FileSettings(units="inch")) + assert r.feature_type == "through-hole" + assert r.net_name == "+5VDC" + assert r.id == "VIA" + pytest.approx(r.hole_diameter, 0.015) + assert r.plated + assert r.access == "both" + pytest.approx(r.x_coord, 0.6647) + pytest.approx(r.y_coord, 1.29) + assert r.rect_x == 0.0 + assert r.soldermask_info == "both" + r = IPC356_TestRecord.from_line(record_string, FileSettings(units="metric")) + pytest.approx(r.hole_diameter, 0.15) + pytest.approx(r.x_coord, 6.647) + pytest.approx(r.y_coord, 12.9) + assert r.rect_x == 0.0 + assert str(r) == "" + + record_string = ( + "327+3.3VDC R40 -1 PA01X 032100Y 007124X0236Y0315R180 S0" + ) + r = IPC356_TestRecord.from_line(record_string, FileSettings(units="inch")) + assert r.feature_type == "smt" + assert r.net_name == "+3.3VDC" + assert r.id == "R40" + assert r.pin == "1" + assert r.plated + assert r.access == "top" + pytest.approx(r.x_coord, 3.21) + pytest.approx(r.y_coord, 0.7124) + pytest.approx(r.rect_x, 0.0236) + pytest.approx(r.rect_y, 0.0315) + assert r.rect_rotation == 180 + assert r.soldermask_info == "none" + r = IPC356_TestRecord.from_line(record_string, FileSettings(units="metric")) + pytest.approx(r.x_coord, 32.1) + pytest.approx(r.y_coord, 7.124) + pytest.approx(r.rect_x, 0.236) + pytest.approx(r.rect_y, 0.315) + + record_string = ( + "317 J4 -M2 D0330PA00X 012447Y 008030X0000 S1" + ) + r = IPC356_TestRecord.from_line(record_string, FileSettings(units="inch")) + assert r.feature_type == "through-hole" + assert r.id == "J4" + assert r.pin == "M2" + pytest.approx(r.hole_diameter, 0.033) + assert r.plated + assert r.access == "both" + pytest.approx(r.x_coord, 1.2447) + pytest.approx(r.y_coord, 0.8030) + pytest.approx(r.rect_x, 0.0) + assert r.soldermask_info == "primary side" + + record_string = "317SCL COMMUNICATION-1 D 40PA00X 34000Y 20000X 600Y1200R270 " + r = IPC356_TestRecord.from_line(record_string, FileSettings(units="inch")) + assert r.feature_type == "through-hole" + assert r.net_name == "SCL" + assert r.id == "COMMUNICATION" + assert r.pin == "1" + pytest.approx(r.hole_diameter, 0.004) + assert r.plated + pytest.approx(r.x_coord, 3.4) + pytest.approx(r.y_coord, 2.0) diff --git a/gerbonara/gerber/tests/test_layers.py b/gerbonara/gerber/tests/test_layers.py new file mode 100644 index 0000000..2178787 --- /dev/null +++ b/gerbonara/gerber/tests/test_layers.py @@ -0,0 +1,158 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2016 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 os + +from ..layers import * +from ..common import read + +NCDRILL_FILE = os.path.join(os.path.dirname(__file__), "resources/ncdrill.DRD") +NETLIST_FILE = os.path.join(os.path.dirname(__file__), "resources/ipc-d-356.ipc") +COPPER_FILE = os.path.join(os.path.dirname(__file__), "resources/top_copper.GTL") + + +def test_guess_layer_class(): + """ Test layer type inferred correctly from filename + """ + + # Add any specific test cases here (filename, layer_class) + test_vectors = [ + (None, "unknown"), + ("NCDRILL.TXT", "unknown"), + ("example_board.gtl", "top"), + ("exampmle_board.sst", "topsilk"), + ("ipc-d-356.ipc", "ipc_netlist"), + ] + + for hint in hints: + for ext in hint.ext: + assert hint.layer == guess_layer_class("board.{}".format(ext)) + for name in hint.name: + assert hint.layer == guess_layer_class("{}.pho".format(name)) + + for filename, layer_class in test_vectors: + assert layer_class == guess_layer_class(filename) + + +def test_guess_layer_class_regex(): + """ Test regular expressions for layer matching + """ + + # Add any specific test case (filename, layer_class) + test_vectors = [("test - top copper.gbr", "top"), ("test - copper top.gbr", "top")] + + # Add custom regular expressions + layer_hints = [ + Hint( + layer="top", + ext=[], + name=[], + regex=r"(.*)(\scopper top|\stop copper).gbr", + content=[], + ) + ] + hints.extend(layer_hints) + + for filename, layer_class in test_vectors: + assert layer_class == guess_layer_class(filename) + + +def test_guess_layer_class_by_content(): + """ Test layer class by checking content + """ + + expected_layer_class = "bottom" + filename = os.path.join( + os.path.dirname(__file__), "resources/example_guess_by_content.g0" + ) + + layer_hints = [ + Hint( + layer="bottom", + ext=[], + name=[], + regex="", + content=["G04 Layer name: Bottom"], + ) + ] + hints.extend(layer_hints) + + assert expected_layer_class == guess_layer_class_by_content(filename) + + +def test_sort_layers(): + """ Test layer ordering + """ + layers = [ + PCBLayer(layer_class="drawing"), + PCBLayer(layer_class="drill"), + PCBLayer(layer_class="bottompaste"), + PCBLayer(layer_class="bottomsilk"), + PCBLayer(layer_class="bottommask"), + PCBLayer(layer_class="bottom"), + PCBLayer(layer_class="internal"), + PCBLayer(layer_class="top"), + PCBLayer(layer_class="topmask"), + PCBLayer(layer_class="topsilk"), + PCBLayer(layer_class="toppaste"), + PCBLayer(layer_class="outline"), + ] + + layer_order = [ + "outline", + "toppaste", + "topsilk", + "topmask", + "top", + "internal", + "bottom", + "bottommask", + "bottomsilk", + "bottompaste", + "drill", + "drawing", + ] + bottom_order = list(reversed(layer_order[:10])) + layer_order[10:] + assert [l.layer_class for l in sort_layers(layers)] == layer_order + assert [l.layer_class for l in sort_layers(layers, from_top=False)] == bottom_order + + +def test_PCBLayer_from_file(): + layer = PCBLayer.from_cam(read(COPPER_FILE)) + assert isinstance(layer, PCBLayer) + layer = PCBLayer.from_cam(read(NCDRILL_FILE)) + assert isinstance(layer, DrillLayer) + layer = PCBLayer.from_cam(read(NETLIST_FILE)) + assert isinstance(layer, PCBLayer) + assert layer.layer_class == "ipc_netlist" + + +def test_PCBLayer_bounds(): + source = read(COPPER_FILE) + layer = PCBLayer.from_cam(source) + assert source.bounds == layer.bounds + + +def test_DrillLayer_from_cam(): + no_exceptions = True + try: + layer = DrillLayer.from_cam(read(NCDRILL_FILE)) + assert isinstance(layer, DrillLayer) + except: + no_exceptions = False + assert no_exceptions diff --git a/gerbonara/gerber/tests/test_primitives.py b/gerbonara/gerber/tests/test_primitives.py new file mode 100644 index 0000000..ad5b34f --- /dev/null +++ b/gerbonara/gerber/tests/test_primitives.py @@ -0,0 +1,1429 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +import pytest +from operator import add +from ..primitives import * + + +def test_primitive_smoketest(): + p = Primitive() + try: + p.bounding_box + assert not True, "should have thrown the exception" + except NotImplementedError: + pass + # pytest.raises(NotImplementedError, p.bounding_box) + + p.to_metric() + p.to_inch() + # try: + # p.offset(1, 1) + # assert_false(True, 'should have thrown the exception') + # except NotImplementedError: + # pass + + +def test_line_angle(): + """ Test Line primitive angle calculation + """ + cases = [ + ((0, 0), (1, 0), math.radians(0)), + ((0, 0), (1, 1), math.radians(45)), + ((0, 0), (0, 1), math.radians(90)), + ((0, 0), (-1, 1), math.radians(135)), + ((0, 0), (-1, 0), math.radians(180)), + ((0, 0), (-1, -1), math.radians(225)), + ((0, 0), (0, -1), math.radians(270)), + ((0, 0), (1, -1), math.radians(315)), + ] + for start, end, expected in cases: + l = Line(start, end, 0) + line_angle = (l.angle + 2 * math.pi) % (2 * math.pi) + pytest.approx(line_angle, expected) + + +def test_line_bounds(): + """ Test Line primitive bounding box calculation + """ + cases = [ + ((0, 0), (1, 1), ((-1, 2), (-1, 2))), + ((-1, -1), (1, 1), ((-2, 2), (-2, 2))), + ((1, 1), (-1, -1), ((-2, 2), (-2, 2))), + ((-1, 1), (1, -1), ((-2, 2), (-2, 2))), + ] + + c = Circle((0, 0), 2) + r = Rectangle((0, 0), 2, 2) + for shape in (c, r): + for start, end, expected in cases: + l = Line(start, end, shape) + assert l.bounding_box == expected + # Test a non-square rectangle + r = Rectangle((0, 0), 3, 2) + cases = [ + ((0, 0), (1, 1), ((-1.5, 2.5), (-1, 2))), + ((-1, -1), (1, 1), ((-2.5, 2.5), (-2, 2))), + ((1, 1), (-1, -1), ((-2.5, 2.5), (-2, 2))), + ((-1, 1), (1, -1), ((-2.5, 2.5), (-2, 2))), + ] + for start, end, expected in cases: + l = Line(start, end, r) + assert l.bounding_box == expected + + +def test_line_vertices(): + c = Circle((0, 0), 2) + l = Line((0, 0), (1, 1), c) + assert l.vertices == None + + # All 4 compass points, all 4 quadrants and the case where start == end + test_cases = [ + ((0, 0), (1, 0), ((-1, -1), (-1, 1), (2, 1), (2, -1))), + ((0, 0), (1, 1), ((-1, -1), (-1, 1), (0, 2), (2, 2), (2, 0), (1, -1))), + ((0, 0), (0, 1), ((-1, -1), (-1, 2), (1, 2), (1, -1))), + ((0, 0), (-1, 1), ((-1, -1), (-2, 0), (-2, 2), (0, 2), (1, 1), (1, -1))), + ((0, 0), (-1, 0), ((-2, -1), (-2, 1), (1, 1), (1, -1))), + ((0, 0), (-1, -1), ((-2, -2), (1, -1), (1, 1), (-1, 1), (-2, 0), (0, -2))), + ((0, 0), (0, -1), ((-1, -2), (-1, 1), (1, 1), (1, -2))), + ((0, 0), (1, -1), ((-1, -1), (0, -2), (2, -2), (2, 0), (1, 1), (-1, 1))), + ((0, 0), (0, 0), ((-1, -1), (-1, 1), (1, 1), (1, -1))), + ] + r = Rectangle((0, 0), 2, 2) + + for start, end, vertices in test_cases: + l = Line(start, end, r) + assert set(vertices) == set(l.vertices) + + +def test_line_conversion(): + c = Circle((0, 0), 25.4, units="metric") + l = Line((2.54, 25.4), (254.0, 2540.0), c, units="metric") + + # No effect + l.to_metric() + assert l.start == (2.54, 25.4) + assert l.end == (254.0, 2540.0) + assert l.aperture.diameter == 25.4 + + l.to_inch() + assert l.start == (0.1, 1.0) + assert l.end == (10.0, 100.0) + assert l.aperture.diameter == 1.0 + + # No effect + l.to_inch() + assert l.start == (0.1, 1.0) + assert l.end == (10.0, 100.0) + assert l.aperture.diameter == 1.0 + + c = Circle((0, 0), 1.0, units="inch") + l = Line((0.1, 1.0), (10.0, 100.0), c, units="inch") + + # No effect + l.to_inch() + assert l.start == (0.1, 1.0) + assert l.end == (10.0, 100.0) + assert l.aperture.diameter == 1.0 + + l.to_metric() + assert l.start == (2.54, 25.4) + assert l.end == (254.0, 2540.0) + assert l.aperture.diameter == 25.4 + + # No effect + l.to_metric() + assert l.start == (2.54, 25.4) + assert l.end == (254.0, 2540.0) + assert l.aperture.diameter == 25.4 + + r = Rectangle((0, 0), 25.4, 254.0, units="metric") + l = Line((2.54, 25.4), (254.0, 2540.0), r, units="metric") + l.to_inch() + assert l.start == (0.1, 1.0) + assert l.end == (10.0, 100.0) + assert l.aperture.width == 1.0 + assert l.aperture.height == 10.0 + + r = Rectangle((0, 0), 1.0, 10.0, units="inch") + l = Line((0.1, 1.0), (10.0, 100.0), r, units="inch") + l.to_metric() + assert l.start == (2.54, 25.4) + assert l.end == (254.0, 2540.0) + assert l.aperture.width == 25.4 + assert l.aperture.height == 254.0 + + +def test_line_offset(): + c = Circle((0, 0), 1) + l = Line((0, 0), (1, 1), c) + l.offset(1, 0) + assert l.start == (1.0, 0.0) + assert l.end == (2.0, 1.0) + l.offset(0, 1) + assert l.start == (1.0, 1.0) + assert l.end == (2.0, 2.0) + + +def test_arc_radius(): + """ Test Arc primitive radius calculation + """ + cases = [((-3, 4), (5, 0), (0, 0), 5), ((0, 1), (1, 0), (0, 0), 1)] + + for start, end, center, radius in cases: + a = Arc(start, end, center, "clockwise", 0, "single-quadrant") + assert a.radius == radius + + +def test_arc_sweep_angle(): + """ Test Arc primitive sweep angle calculation + """ + cases = [ + ((1, 0), (0, 1), (0, 0), "counterclockwise", math.radians(90)), + ((1, 0), (0, 1), (0, 0), "clockwise", math.radians(270)), + ((1, 0), (-1, 0), (0, 0), "clockwise", math.radians(180)), + ((1, 0), (-1, 0), (0, 0), "counterclockwise", math.radians(180)), + ] + + for start, end, center, direction, sweep in cases: + c = Circle((0, 0), 1) + a = Arc(start, end, center, direction, c, "single-quadrant") + assert a.sweep_angle == sweep + + +def test_arc_bounds(): + """ Test Arc primitive bounding box calculation + """ + cases = [ + ((1, 0), (0, 1), (0, 0), "clockwise", ((-1.5, 1.5), (-1.5, 1.5))), + ((1, 0), (0, 1), (0, 0), "counterclockwise", ((-0.5, 1.5), (-0.5, 1.5))), + ((0, 1), (-1, 0), (0, 0), "clockwise", ((-1.5, 1.5), (-1.5, 1.5))), + ((0, 1), (-1, 0), (0, 0), "counterclockwise", ((-1.5, 0.5), (-0.5, 1.5))), + ((-1, 0), (0, -1), (0, 0), "clockwise", ((-1.5, 1.5), (-1.5, 1.5))), + ((-1, 0), (0, -1), (0, 0), "counterclockwise", ((-1.5, 0.5), (-1.5, 0.5))), + ((0, -1), (1, 0), (0, 0), "clockwise", ((-1.5, 1.5), (-1.5, 1.5))), + ((0, -1), (1, 0), (0, 0), "counterclockwise", ((-0.5, 1.5), (-1.5, 0.5))), + # Arcs with the same start and end point render a full circle + ((1, 0), (1, 0), (0, 0), "clockwise", ((-1.5, 1.5), (-1.5, 1.5))), + ((1, 0), (1, 0), (0, 0), "counterclockwise", ((-1.5, 1.5), (-1.5, 1.5))), + ] + for start, end, center, direction, bounds in cases: + c = Circle((0, 0), 1) + a = Arc(start, end, center, direction, c, "multi-quadrant") + assert a.bounding_box == bounds + + +def test_arc_bounds_no_aperture(): + """ Test Arc primitive bounding box calculation ignoring aperture + """ + cases = [ + ((1, 0), (0, 1), (0, 0), "clockwise", ((-1.0, 1.0), (-1.0, 1.0))), + ((1, 0), (0, 1), (0, 0), "counterclockwise", ((0.0, 1.0), (0.0, 1.0))), + ((0, 1), (-1, 0), (0, 0), "clockwise", ((-1.0, 1.0), (-1.0, 1.0))), + ((0, 1), (-1, 0), (0, 0), "counterclockwise", ((-1.0, 0.0), (0.0, 1.0))), + ((-1, 0), (0, -1), (0, 0), "clockwise", ((-1.0, 1.0), (-1.0, 1.0))), + ((-1, 0), (0, -1), (0, 0), "counterclockwise", ((-1.0, 0.0), (-1.0, 0.0))), + ((0, -1), (1, 0), (0, 0), "clockwise", ((-1.0, 1.0), (-1.0, 1.0))), + ((0, -1), (1, 0), (0, 0), "counterclockwise", ((-0.0, 1.0), (-1.0, 0.0))), + # Arcs with the same start and end point render a full circle + ((1, 0), (1, 0), (0, 0), "clockwise", ((-1.0, 1.0), (-1.0, 1.0))), + ((1, 0), (1, 0), (0, 0), "counterclockwise", ((-1.0, 1.0), (-1.0, 1.0))), + ] + for start, end, center, direction, bounds in cases: + c = Circle((0, 0), 1) + a = Arc(start, end, center, direction, c, "multi-quadrant") + assert a.bounding_box_no_aperture == bounds + + +def test_arc_conversion(): + c = Circle((0, 0), 25.4, units="metric") + a = Arc( + (2.54, 25.4), + (254.0, 2540.0), + (25400.0, 254000.0), + "clockwise", + c, + "single-quadrant", + units="metric", + ) + + # No effect + a.to_metric() + assert a.start == (2.54, 25.4) + assert a.end == (254.0, 2540.0) + assert a.center == (25400.0, 254000.0) + assert a.aperture.diameter == 25.4 + + a.to_inch() + assert a.start == (0.1, 1.0) + assert a.end == (10.0, 100.0) + assert a.center == (1000.0, 10000.0) + assert a.aperture.diameter == 1.0 + + # no effect + a.to_inch() + assert a.start == (0.1, 1.0) + assert a.end == (10.0, 100.0) + assert a.center == (1000.0, 10000.0) + assert a.aperture.diameter == 1.0 + + c = Circle((0, 0), 1.0, units="inch") + a = Arc( + (0.1, 1.0), + (10.0, 100.0), + (1000.0, 10000.0), + "clockwise", + c, + "single-quadrant", + units="inch", + ) + a.to_metric() + assert a.start == (2.54, 25.4) + assert a.end == (254.0, 2540.0) + assert a.center == (25400.0, 254000.0) + assert a.aperture.diameter == 25.4 + + +def test_arc_offset(): + c = Circle((0, 0), 1) + a = Arc((0, 0), (1, 1), (2, 2), "clockwise", c, "single-quadrant") + a.offset(1, 0) + assert a.start == (1.0, 0.0) + assert a.end == (2.0, 1.0) + assert a.center == (3.0, 2.0) + a.offset(0, 1) + assert a.start == (1.0, 1.0) + assert a.end == (2.0, 2.0) + assert a.center == (3.0, 3.0) + + +def test_circle_radius(): + """ Test Circle primitive radius calculation + """ + c = Circle((1, 1), 2) + assert c.radius == 1 + + +def test_circle_hole_radius(): + """ Test Circle primitive hole radius calculation + """ + c = Circle((1, 1), 4, 2) + assert c.hole_radius == 1 + + +def test_circle_bounds(): + """ Test Circle bounding box calculation + """ + c = Circle((1, 1), 2) + assert c.bounding_box == ((0, 2), (0, 2)) + + +def test_circle_conversion(): + """Circle conversion of units""" + # Circle initially metric, no hole + c = Circle((2.54, 25.4), 254.0, units="metric") + + c.to_metric() # shouldn't do antyhing + assert c.position == (2.54, 25.4) + assert c.diameter == 254.0 + assert c.hole_diameter == None + + c.to_inch() + assert c.position == (0.1, 1.0) + assert c.diameter == 10.0 + assert c.hole_diameter == None + + # no effect + c.to_inch() + assert c.position == (0.1, 1.0) + assert c.diameter == 10.0 + assert c.hole_diameter == None + + # Circle initially metric, with hole + c = Circle((2.54, 25.4), 254.0, 127.0, units="metric") + + c.to_metric() # shouldn't do antyhing + assert c.position == (2.54, 25.4) + assert c.diameter == 254.0 + assert c.hole_diameter == 127.0 + + c.to_inch() + assert c.position == (0.1, 1.0) + assert c.diameter == 10.0 + assert c.hole_diameter == 5.0 + + # no effect + c.to_inch() + assert c.position == (0.1, 1.0) + assert c.diameter == 10.0 + assert c.hole_diameter == 5.0 + + # Circle initially inch, no hole + c = Circle((0.1, 1.0), 10.0, units="inch") + # No effect + c.to_inch() + assert c.position == (0.1, 1.0) + assert c.diameter == 10.0 + assert c.hole_diameter == None + + c.to_metric() + assert c.position == (2.54, 25.4) + assert c.diameter == 254.0 + assert c.hole_diameter == None + + # no effect + c.to_metric() + assert c.position == (2.54, 25.4) + assert c.diameter == 254.0 + assert c.hole_diameter == None + + c = Circle((0.1, 1.0), 10.0, 5.0, units="inch") + # No effect + c.to_inch() + assert c.position == (0.1, 1.0) + assert c.diameter == 10.0 + assert c.hole_diameter == 5.0 + + c.to_metric() + assert c.position == (2.54, 25.4) + assert c.diameter == 254.0 + assert c.hole_diameter == 127.0 + + # no effect + c.to_metric() + assert c.position == (2.54, 25.4) + assert c.diameter == 254.0 + assert c.hole_diameter == 127.0 + + +def test_circle_offset(): + c = Circle((0, 0), 1) + c.offset(1, 0) + assert c.position == (1.0, 0.0) + c.offset(0, 1) + assert c.position == (1.0, 1.0) + + +def test_ellipse_ctor(): + """ Test ellipse creation + """ + e = Ellipse((2, 2), 3, 2) + assert e.position == (2, 2) + assert e.width == 3 + assert e.height == 2 + + +def test_ellipse_bounds(): + """ Test ellipse bounding box calculation + """ + e = Ellipse((2, 2), 4, 2) + assert e.bounding_box == ((0, 4), (1, 3)) + e = Ellipse((2, 2), 4, 2, rotation=90) + assert e.bounding_box == ((1, 3), (0, 4)) + e = Ellipse((2, 2), 4, 2, rotation=180) + assert e.bounding_box == ((0, 4), (1, 3)) + e = Ellipse((2, 2), 4, 2, rotation=270) + assert e.bounding_box == ((1, 3), (0, 4)) + + +def test_ellipse_conversion(): + e = Ellipse((2.54, 25.4), 254.0, 2540.0, units="metric") + + # No effect + e.to_metric() + assert e.position == (2.54, 25.4) + assert e.width == 254.0 + assert e.height == 2540.0 + + e.to_inch() + assert e.position == (0.1, 1.0) + assert e.width == 10.0 + assert e.height == 100.0 + + # No effect + e.to_inch() + assert e.position == (0.1, 1.0) + assert e.width == 10.0 + assert e.height == 100.0 + + e = Ellipse((0.1, 1.0), 10.0, 100.0, units="inch") + + # no effect + e.to_inch() + assert e.position == (0.1, 1.0) + assert e.width == 10.0 + assert e.height == 100.0 + + e.to_metric() + assert e.position == (2.54, 25.4) + assert e.width == 254.0 + assert e.height == 2540.0 + + # No effect + e.to_metric() + assert e.position == (2.54, 25.4) + assert e.width == 254.0 + assert e.height == 2540.0 + + +def test_ellipse_offset(): + e = Ellipse((0, 0), 1, 2) + e.offset(1, 0) + assert e.position == (1.0, 0.0) + e.offset(0, 1) + assert e.position == (1.0, 1.0) + + +def test_rectangle_ctor(): + """ Test rectangle creation + """ + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2)) + for pos, width, height in test_cases: + r = Rectangle(pos, width, height) + assert r.position == pos + assert r.width == width + assert r.height == height + + +def test_rectangle_hole_radius(): + """ Test rectangle hole diameter calculation + """ + r = Rectangle((0, 0), 2, 2) + assert 0 == r.hole_radius + + r = Rectangle((0, 0), 2, 2, 1) + assert 0.5 == r.hole_radius + + +def test_rectangle_bounds(): + """ Test rectangle bounding box calculation + """ + r = Rectangle((0, 0), 2, 2) + xbounds, ybounds = r.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + r = Rectangle((0, 0), 2, 2, rotation=45) + xbounds, ybounds = r.bounding_box + pytest.approx(xbounds, (-math.sqrt(2), math.sqrt(2))) + pytest.approx(ybounds, (-math.sqrt(2), math.sqrt(2))) + + +def test_rectangle_vertices(): + sqrt2 = math.sqrt(2.0) + TEST_VECTORS = [ + ((0, 0), 2.0, 2.0, 0.0, ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0))), + ((0, 0), 2.0, 3.0, 0.0, ((-1.0, -1.5), (-1.0, 1.5), (1.0, 1.5), (1.0, -1.5))), + ((0, 0), 2.0, 2.0, 90.0, ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0))), + ((0, 0), 3.0, 2.0, 90.0, ((-1.0, -1.5), (-1.0, 1.5), (1.0, 1.5), (1.0, -1.5))), + ( + (0, 0), + 2.0, + 2.0, + 45.0, + ((-sqrt2, 0.0), (0.0, sqrt2), (sqrt2, 0), (0, -sqrt2)), + ), + ] + for pos, width, height, rotation, expected in TEST_VECTORS: + r = Rectangle(pos, width, height, rotation=rotation) + for test, expect in zip(sorted(r.vertices), sorted(expected)): + pytest.approx(test, expect) + + r = Rectangle((0, 0), 2.0, 2.0, rotation=0.0) + r.rotation = 45.0 + for test, expect in zip( + sorted(r.vertices), + sorted(((-sqrt2, 0.0), (0.0, sqrt2), (sqrt2, 0), (0, -sqrt2))), + ): + pytest.approx(test, expect) + + +def test_rectangle_segments(): + + r = Rectangle((0, 0), 2.0, 2.0) + expected = [vtx for segment in r.segments for vtx in segment] + for vertex in r.vertices: + assert vertex in expected + + +def test_rectangle_conversion(): + """Test converting rectangles between units""" + + # Initially metric no hole + r = Rectangle((2.54, 25.4), 254.0, 2540.0, units="metric") + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + + # Initially metric with hole + r = Rectangle((2.54, 25.4), 254.0, 2540.0, 127.0, units="metric") + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.hole_diameter == 127.0 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.hole_diameter == 5.0 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.hole_diameter == 5.0 + + # Initially inch, no hole + r = Rectangle((0.1, 1.0), 10.0, 100.0, units="inch") + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + + # Initially inch with hole + r = Rectangle((0.1, 1.0), 10.0, 100.0, 5.0, units="inch") + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.hole_diameter == 5.0 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.hole_diameter == 127.0 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.hole_diameter == 127.0 + + +def test_rectangle_offset(): + r = Rectangle((0, 0), 1, 2) + r.offset(1, 0) + assert r.position == (1.0, 0.0) + r.offset(0, 1) + assert r.position == (1.0, 1.0) + + +def test_diamond_ctor(): + """ Test diamond creation + """ + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2)) + for pos, width, height in test_cases: + d = Diamond(pos, width, height) + assert d.position == pos + assert d.width == width + assert d.height == height + + +def test_diamond_bounds(): + """ Test diamond bounding box calculation + """ + d = Diamond((0, 0), 2, 2) + xbounds, ybounds = d.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + d = Diamond((0, 0), math.sqrt(2), math.sqrt(2), rotation=45) + xbounds, ybounds = d.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + + +def test_diamond_conversion(): + d = Diamond((2.54, 25.4), 254.0, 2540.0, units="metric") + + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.width == 254.0 + assert d.height == 2540.0 + + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.width == 10.0 + assert d.height == 100.0 + + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.width == 10.0 + assert d.height == 100.0 + + d = Diamond((0.1, 1.0), 10.0, 100.0, units="inch") + + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.width == 10.0 + assert d.height == 100.0 + + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.width == 254.0 + assert d.height == 2540.0 + + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.width == 254.0 + assert d.height == 2540.0 + + +def test_diamond_offset(): + d = Diamond((0, 0), 1, 2) + d.offset(1, 0) + assert d.position == (1.0, 0.0) + d.offset(0, 1) + assert d.position == (1.0, 1.0) + + +def test_chamfer_rectangle_ctor(): + """ Test chamfer rectangle creation + """ + test_cases = ( + ((0, 0), 1, 1, 0.2, (True, True, False, False)), + ((0, 0), 1, 2, 0.3, (True, True, True, True)), + ((1, 1), 1, 2, 0.4, (False, False, False, False)), + ) + for pos, width, height, chamfer, corners in test_cases: + r = ChamferRectangle(pos, width, height, chamfer, corners) + assert r.position == pos + assert r.width == width + assert r.height == height + assert r.chamfer == chamfer + pytest.approx(r.corners, corners) + + +def test_chamfer_rectangle_bounds(): + """ Test chamfer rectangle bounding box calculation + """ + r = ChamferRectangle((0, 0), 2, 2, 0.2, (True, True, False, False)) + xbounds, ybounds = r.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + r = ChamferRectangle((0, 0), 2, 2, 0.2, (True, True, False, False), rotation=45) + xbounds, ybounds = r.bounding_box + pytest.approx(xbounds, (-math.sqrt(2), math.sqrt(2))) + pytest.approx(ybounds, (-math.sqrt(2), math.sqrt(2))) + + +def test_chamfer_rectangle_conversion(): + r = ChamferRectangle( + (2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False), units="metric" + ) + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.chamfer == 0.254 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.chamfer == 0.01 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.chamfer == 0.01 + + r = ChamferRectangle( + (0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False), units="inch" + ) + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.chamfer == 0.01 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.chamfer == 0.254 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.chamfer == 0.254 + + +def test_chamfer_rectangle_offset(): + r = ChamferRectangle((0, 0), 1, 2, 0.01, (True, True, False, False)) + r.offset(1, 0) + assert r.position == (1.0, 0.0) + r.offset(0, 1) + assert r.position == (1.0, 1.0) + + +def test_chamfer_rectangle_vertices(): + TEST_VECTORS = [ + ( + 1.0, + (True, True, True, True), + ( + (-2.5, -1.5), + (-2.5, 1.5), + (-1.5, 2.5), + (1.5, 2.5), + (2.5, 1.5), + (2.5, -1.5), + (1.5, -2.5), + (-1.5, -2.5), + ), + ), + ( + 1.0, + (True, False, False, False), + ((-2.5, -2.5), (-2.5, 2.5), (1.5, 2.5), (2.5, 1.5), (2.5, -2.5)), + ), + ( + 1.0, + (False, True, False, False), + ((-2.5, -2.5), (-2.5, 1.5), (-1.5, 2.5), (2.5, 2.5), (2.5, -2.5)), + ), + ( + 1.0, + (False, False, True, False), + ((-2.5, -1.5), (-2.5, 2.5), (2.5, 2.5), (2.5, -2.5), (-1.5, -2.5)), + ), + ( + 1.0, + (False, False, False, True), + ((-2.5, -2.5), (-2.5, 2.5), (2.5, 2.5), (2.5, -1.5), (1.5, -2.5)), + ), + ] + for chamfer, corners, expected in TEST_VECTORS: + r = ChamferRectangle((0, 0), 5, 5, chamfer, corners) + assert set(r.vertices) == set(expected) + + +def test_round_rectangle_ctor(): + """ Test round rectangle creation + """ + test_cases = ( + ((0, 0), 1, 1, 0.2, (True, True, False, False)), + ((0, 0), 1, 2, 0.3, (True, True, True, True)), + ((1, 1), 1, 2, 0.4, (False, False, False, False)), + ) + for pos, width, height, radius, corners in test_cases: + r = RoundRectangle(pos, width, height, radius, corners) + assert r.position == pos + assert r.width == width + assert r.height == height + assert r.radius == radius + pytest.approx(r.corners, corners) + + +def test_round_rectangle_bounds(): + """ Test round rectangle bounding box calculation + """ + r = RoundRectangle((0, 0), 2, 2, 0.2, (True, True, False, False)) + xbounds, ybounds = r.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + r = RoundRectangle((0, 0), 2, 2, 0.2, (True, True, False, False), rotation=45) + xbounds, ybounds = r.bounding_box + pytest.approx(xbounds, (-math.sqrt(2), math.sqrt(2))) + pytest.approx(ybounds, (-math.sqrt(2), math.sqrt(2))) + + +def test_round_rectangle_conversion(): + r = RoundRectangle( + (2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False), units="metric" + ) + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.radius == 0.254 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.radius == 0.01 + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.radius == 0.01 + + r = RoundRectangle( + (0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False), units="inch" + ) + + r.to_inch() + assert r.position == (0.1, 1.0) + assert r.width == 10.0 + assert r.height == 100.0 + assert r.radius == 0.01 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.radius == 0.254 + + r.to_metric() + assert r.position == (2.54, 25.4) + assert r.width == 254.0 + assert r.height == 2540.0 + assert r.radius == 0.254 + + +def test_round_rectangle_offset(): + r = RoundRectangle((0, 0), 1, 2, 0.01, (True, True, False, False)) + r.offset(1, 0) + assert r.position == (1.0, 0.0) + r.offset(0, 1) + assert r.position == (1.0, 1.0) + + +def test_obround_ctor(): + """ Test obround creation + """ + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2)) + for pos, width, height in test_cases: + o = Obround(pos, width, height) + assert o.position == pos + assert o.width == width + assert o.height == height + + +def test_obround_bounds(): + """ Test obround bounding box calculation + """ + o = Obround((2, 2), 2, 4) + xbounds, ybounds = o.bounding_box + pytest.approx(xbounds, (1, 3)) + pytest.approx(ybounds, (0, 4)) + o = Obround((2, 2), 4, 2) + xbounds, ybounds = o.bounding_box + pytest.approx(xbounds, (0, 4)) + pytest.approx(ybounds, (1, 3)) + + +def test_obround_orientation(): + o = Obround((0, 0), 2, 1) + assert o.orientation == "horizontal" + o = Obround((0, 0), 1, 2) + assert o.orientation == "vertical" + + +def test_obround_subshapes(): + o = Obround((0, 0), 1, 4) + ss = o.subshapes + pytest.approx(ss["rectangle"].position, (0, 0)) + pytest.approx(ss["circle1"].position, (0, 1.5)) + pytest.approx(ss["circle2"].position, (0, -1.5)) + o = Obround((0, 0), 4, 1) + ss = o.subshapes + pytest.approx(ss["rectangle"].position, (0, 0)) + pytest.approx(ss["circle1"].position, (1.5, 0)) + pytest.approx(ss["circle2"].position, (-1.5, 0)) + + +def test_obround_conversion(): + o = Obround((2.54, 25.4), 254.0, 2540.0, units="metric") + + # No effect + o.to_metric() + assert o.position == (2.54, 25.4) + assert o.width == 254.0 + assert o.height == 2540.0 + + o.to_inch() + assert o.position == (0.1, 1.0) + assert o.width == 10.0 + assert o.height == 100.0 + + # No effect + o.to_inch() + assert o.position == (0.1, 1.0) + assert o.width == 10.0 + assert o.height == 100.0 + + o = Obround((0.1, 1.0), 10.0, 100.0, units="inch") + + # No effect + o.to_inch() + assert o.position == (0.1, 1.0) + assert o.width == 10.0 + assert o.height == 100.0 + + o.to_metric() + assert o.position == (2.54, 25.4) + assert o.width == 254.0 + assert o.height == 2540.0 + + # No effect + o.to_metric() + assert o.position == (2.54, 25.4) + assert o.width == 254.0 + assert o.height == 2540.0 + + +def test_obround_offset(): + o = Obround((0, 0), 1, 2) + o.offset(1, 0) + assert o.position == (1.0, 0.0) + o.offset(0, 1) + assert o.position == (1.0, 1.0) + + +def test_polygon_ctor(): + """ Test polygon creation + """ + test_cases = (((0, 0), 3, 5, 0), ((0, 0), 5, 6, 0), ((1, 1), 7, 7, 45)) + for pos, sides, radius, hole_diameter in test_cases: + p = Polygon(pos, sides, radius, hole_diameter) + assert p.position == pos + assert p.sides == sides + assert p.radius == radius + assert p.hole_diameter == hole_diameter + + +def test_polygon_bounds(): + """ Test polygon bounding box calculation + """ + p = Polygon((2, 2), 3, 2, 0) + xbounds, ybounds = p.bounding_box + pytest.approx(xbounds, (0, 4)) + pytest.approx(ybounds, (0, 4)) + p = Polygon((2, 2), 3, 4, 0) + xbounds, ybounds = p.bounding_box + pytest.approx(xbounds, (-2, 6)) + pytest.approx(ybounds, (-2, 6)) + + +def test_polygon_conversion(): + p = Polygon((2.54, 25.4), 3, 254.0, 0, units="metric") + + # No effect + p.to_metric() + assert p.position == (2.54, 25.4) + assert p.radius == 254.0 + + p.to_inch() + assert p.position == (0.1, 1.0) + assert p.radius == 10.0 + + # No effect + p.to_inch() + assert p.position == (0.1, 1.0) + assert p.radius == 10.0 + + p = Polygon((0.1, 1.0), 3, 10.0, 0, units="inch") + + # No effect + p.to_inch() + assert p.position == (0.1, 1.0) + assert p.radius == 10.0 + + p.to_metric() + assert p.position == (2.54, 25.4) + assert p.radius == 254.0 + + # No effect + p.to_metric() + assert p.position == (2.54, 25.4) + assert p.radius == 254.0 + + +def test_polygon_offset(): + p = Polygon((0, 0), 5, 10, 0) + p.offset(1, 0) + assert p.position == (1.0, 0.0) + p.offset(0, 1) + assert p.position == (1.0, 1.0) + + +def test_region_ctor(): + """ Test Region creation + """ + apt = Circle((0, 0), 0) + lines = ( + Line((0, 0), (1, 0), apt), + Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), + Line((0, 1), (0, 0), apt), + ) + points = ((0, 0), (1, 0), (1, 1), (0, 1)) + r = Region(lines) + for i, p in enumerate(lines): + assert r.primitives[i] == p + + +def test_region_bounds(): + """ Test region bounding box calculation + """ + apt = Circle((0, 0), 0) + lines = ( + Line((0, 0), (1, 0), apt), + Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), + Line((0, 1), (0, 0), apt), + ) + r = Region(lines) + xbounds, ybounds = r.bounding_box + pytest.approx(xbounds, (0, 1)) + pytest.approx(ybounds, (0, 1)) + + +def test_region_offset(): + apt = Circle((0, 0), 0) + lines = ( + Line((0, 0), (1, 0), apt), + Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), + Line((0, 1), (0, 0), apt), + ) + r = Region(lines) + xlim, ylim = r.bounding_box + r.offset(0, 1) + new_xlim, new_ylim = r.bounding_box + pytest.approx(new_xlim, xlim) + pytest.approx(new_ylim, tuple([y + 1 for y in ylim])) + + +def test_round_butterfly_ctor(): + """ Test round butterfly creation + """ + test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7)) + for pos, diameter in test_cases: + b = RoundButterfly(pos, diameter) + assert b.position == pos + assert b.diameter == diameter + assert b.radius == diameter / 2.0 + + +def test_round_butterfly_ctor_validation(): + """ Test RoundButterfly argument validation + """ + pytest.raises(TypeError, RoundButterfly, 3, 5) + pytest.raises(TypeError, RoundButterfly, (3, 4, 5), 5) + + +def test_round_butterfly_conversion(): + b = RoundButterfly((2.54, 25.4), 254.0, units="metric") + + # No Effect + b.to_metric() + assert b.position == (2.54, 25.4) + assert b.diameter == (254.0) + + b.to_inch() + assert b.position == (0.1, 1.0) + assert b.diameter == 10.0 + + # No effect + b.to_inch() + assert b.position == (0.1, 1.0) + assert b.diameter == 10.0 + + b = RoundButterfly((0.1, 1.0), 10.0, units="inch") + + # No effect + b.to_inch() + assert b.position == (0.1, 1.0) + assert b.diameter == 10.0 + + b.to_metric() + assert b.position == (2.54, 25.4) + assert b.diameter == (254.0) + + # No Effect + b.to_metric() + assert b.position == (2.54, 25.4) + assert b.diameter == (254.0) + + +def test_round_butterfly_offset(): + b = RoundButterfly((0, 0), 1) + b.offset(1, 0) + assert b.position == (1.0, 0.0) + b.offset(0, 1) + assert b.position == (1.0, 1.0) + + +def test_round_butterfly_bounds(): + """ Test RoundButterfly bounding box calculation + """ + b = RoundButterfly((0, 0), 2) + xbounds, ybounds = b.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + + +def test_square_butterfly_ctor(): + """ Test SquareButterfly creation + """ + test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7)) + for pos, side in test_cases: + b = SquareButterfly(pos, side) + assert b.position == pos + assert b.side == side + + +def test_square_butterfly_ctor_validation(): + """ Test SquareButterfly argument validation + """ + pytest.raises(TypeError, SquareButterfly, 3, 5) + pytest.raises(TypeError, SquareButterfly, (3, 4, 5), 5) + + +def test_square_butterfly_bounds(): + """ Test SquareButterfly bounding box calculation + """ + b = SquareButterfly((0, 0), 2) + xbounds, ybounds = b.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + + +def test_squarebutterfly_conversion(): + b = SquareButterfly((2.54, 25.4), 254.0, units="metric") + + # No effect + b.to_metric() + assert b.position == (2.54, 25.4) + assert b.side == (254.0) + + b.to_inch() + assert b.position == (0.1, 1.0) + assert b.side == 10.0 + + # No effect + b.to_inch() + assert b.position == (0.1, 1.0) + assert b.side == 10.0 + + b = SquareButterfly((0.1, 1.0), 10.0, units="inch") + + # No effect + b.to_inch() + assert b.position == (0.1, 1.0) + assert b.side == 10.0 + + b.to_metric() + assert b.position == (2.54, 25.4) + assert b.side == (254.0) + + # No effect + b.to_metric() + assert b.position == (2.54, 25.4) + assert b.side == (254.0) + + +def test_square_butterfly_offset(): + b = SquareButterfly((0, 0), 1) + b.offset(1, 0) + assert b.position == (1.0, 0.0) + b.offset(0, 1) + assert b.position == (1.0, 1.0) + + +def test_donut_ctor(): + """ Test Donut primitive creation + """ + test_cases = ( + ((0, 0), "round", 3, 5), + ((0, 0), "square", 5, 7), + ((1, 1), "hexagon", 7, 9), + ((2, 2), "octagon", 9, 11), + ) + for pos, shape, in_d, out_d in test_cases: + d = Donut(pos, shape, in_d, out_d) + assert d.position == pos + assert d.shape == shape + assert d.inner_diameter == in_d + assert d.outer_diameter == out_d + + +def test_donut_ctor_validation(): + pytest.raises(TypeError, Donut, 3, "round", 5, 7) + pytest.raises(TypeError, Donut, (3, 4, 5), "round", 5, 7) + pytest.raises(ValueError, Donut, (0, 0), "triangle", 3, 5) + pytest.raises(ValueError, Donut, (0, 0), "round", 5, 3) + + +def test_donut_bounds(): + d = Donut((0, 0), "round", 0.0, 2.0) + xbounds, ybounds = d.bounding_box + assert xbounds == (-1.0, 1.0) + assert ybounds == (-1.0, 1.0) + + +def test_donut_conversion(): + d = Donut((2.54, 25.4), "round", 254.0, 2540.0, units="metric") + + # No effect + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.inner_diameter == 254.0 + assert d.outer_diameter == 2540.0 + + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.inner_diameter == 10.0 + assert d.outer_diameter == 100.0 + + # No effect + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.inner_diameter == 10.0 + assert d.outer_diameter == 100.0 + + d = Donut((0.1, 1.0), "round", 10.0, 100.0, units="inch") + + # No effect + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.inner_diameter == 10.0 + assert d.outer_diameter == 100.0 + + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.inner_diameter == 254.0 + assert d.outer_diameter == 2540.0 + + # No effect + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.inner_diameter == 254.0 + assert d.outer_diameter == 2540.0 + + +def test_donut_offset(): + d = Donut((0, 0), "round", 1, 10) + d.offset(1, 0) + assert d.position == (1.0, 0.0) + d.offset(0, 1) + assert d.position == (1.0, 1.0) + + +def test_drill_ctor(): + """ Test drill primitive creation + """ + test_cases = (((0, 0), 2), ((1, 1), 3), ((2, 2), 5)) + for position, diameter in test_cases: + d = Drill(position, diameter) + assert d.position == position + assert d.diameter == diameter + assert d.radius == diameter / 2.0 + + +def test_drill_ctor_validation(): + """ Test drill argument validation + """ + pytest.raises(TypeError, Drill, 3, 5) + pytest.raises(TypeError, Drill, (3, 4, 5), 5) + + +def test_drill_bounds(): + d = Drill((0, 0), 2) + xbounds, ybounds = d.bounding_box + pytest.approx(xbounds, (-1, 1)) + pytest.approx(ybounds, (-1, 1)) + d = Drill((1, 2), 2) + xbounds, ybounds = d.bounding_box + pytest.approx(xbounds, (0, 2)) + pytest.approx(ybounds, (1, 3)) + + +def test_drill_conversion(): + d = Drill((2.54, 25.4), 254.0, units="metric") + + # No effect + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.diameter == 254.0 + + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.diameter == 10.0 + + # No effect + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.diameter == 10.0 + + d = Drill((0.1, 1.0), 10.0, units="inch") + + # No effect + d.to_inch() + assert d.position == (0.1, 1.0) + assert d.diameter == 10.0 + + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.diameter == 254.0 + + # No effect + d.to_metric() + assert d.position == (2.54, 25.4) + assert d.diameter == 254.0 + + +def test_drill_offset(): + d = Drill((0, 0), 1.0) + d.offset(1, 0) + assert d.position == (1.0, 0.0) + d.offset(0, 1) + assert d.position == (1.0, 1.0) + + +def test_drill_equality(): + d = Drill((2.54, 25.4), 254.0) + d1 = Drill((2.54, 25.4), 254.0) + assert d == d1 + d1 = Drill((2.54, 25.4), 254.2) + assert d != d1 + + +def test_slot_bounds(): + """ Test Slot primitive bounding box calculation + """ + cases = [ + ((0, 0), (1, 1), ((-1, 2), (-1, 2))), + ((-1, -1), (1, 1), ((-2, 2), (-2, 2))), + ((1, 1), (-1, -1), ((-2, 2), (-2, 2))), + ((-1, 1), (1, -1), ((-2, 2), (-2, 2))), + ] + + for start, end, expected in cases: + s = Slot(start, end, 2.0) + assert s.bounding_box == expected diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py new file mode 100644 index 0000000..e7baf11 --- /dev/null +++ b/gerbonara/gerber/tests/test_rs274x.py @@ -0,0 +1,55 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +import os +import pytest + +from ..rs274x import read, GerberFile + + +TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), "resources/top_copper.GTL") + +MULTILINE_READ_FILE = os.path.join( + os.path.dirname(__file__), "resources/multiline_read.ger" +) + + +def test_read(): + top_copper = read(TOP_COPPER_FILE) + assert isinstance(top_copper, GerberFile) + + +def test_multiline_read(): + multiline = read(MULTILINE_READ_FILE) + assert isinstance(multiline, GerberFile) + assert 10 == len(multiline.statements) + + +def test_comments_parameter(): + top_copper = read(TOP_COPPER_FILE) + assert top_copper.comments[0] == "This is a comment,:" + + +def test_size_parameter(): + top_copper = read(TOP_COPPER_FILE) + size = top_copper.size + pytest.approx(size[0], 2.256900, 6) + pytest.approx(size[1], 1.500000, 6) + + +def test_conversion(): + top_copper = read(TOP_COPPER_FILE) + assert top_copper.units == "inch" + top_copper_inch = read(TOP_COPPER_FILE) + top_copper.to_metric() + for statement in top_copper_inch.statements: + statement.to_metric() + for primitive in top_copper_inch.primitives: + primitive.to_metric() + assert top_copper.units == "metric" + for i, m in zip(top_copper.statements, top_copper_inch.statements): + assert i == m + + for i, m in zip(top_copper.primitives, top_copper_inch.primitives): + assert i == m diff --git a/gerbonara/gerber/tests/test_rs274x_backend.py b/gerbonara/gerber/tests/test_rs274x_backend.py new file mode 100644 index 0000000..13347c5 --- /dev/null +++ b/gerbonara/gerber/tests/test_rs274x_backend.py @@ -0,0 +1,232 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Garret Fick + +import os + +from ..render.rs274x_backend import Rs274xContext +from ..rs274x import read + + +def test_render_two_boxes(): + """Umaco exapmle of two boxes""" + _test_render( + "resources/example_two_square_boxes.gbr", "golden/example_two_square_boxes.gbr" + ) + + +def _test_render_single_quadrant(): + """Umaco exapmle of a single quadrant arc""" + + # TODO there is probably a bug here + _test_render( + "resources/example_single_quadrant.gbr", "golden/example_single_quadrant.gbr" + ) + + +def _test_render_simple_contour(): + """Umaco exapmle of a simple arrow-shaped contour""" + _test_render( + "resources/example_simple_contour.gbr", "golden/example_simple_contour.gbr" + ) + + +def _test_render_single_contour_1(): + """Umaco example of a single contour + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render( + "resources/example_single_contour_1.gbr", "golden/example_single_contour.gbr" + ) + + +def _test_render_single_contour_2(): + """Umaco exapmle of a single contour, alternate contour end order + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render( + "resources/example_single_contour_2.gbr", "golden/example_single_contour.gbr" + ) + + +def _test_render_single_contour_3(): + """Umaco exapmle of a single contour with extra line""" + _test_render( + "resources/example_single_contour_3.gbr", "golden/example_single_contour_3.gbr" + ) + + +def _test_render_not_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_not_overlapping_contour.gbr", + "golden/example_not_overlapping_contour.gbr", + ) + + +def _test_render_not_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_not_overlapping_touching.gbr", + "golden/example_not_overlapping_touching.gbr", + ) + + +def _test_render_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_overlapping_touching.gbr", + "golden/example_overlapping_touching.gbr", + ) + + +def _test_render_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render( + "resources/example_overlapping_contour.gbr", + "golden/example_overlapping_contour.gbr", + ) + + +def _DISABLED_test_render_level_holes(): + """Umaco example of using multiple levels to create multiple holes""" + + # TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more + # rendering fixes in the related repository that may resolve these. + _test_render( + "resources/example_level_holes.gbr", "golden/example_overlapping_contour.gbr" + ) + + +def _DISABLED_test_render_cutin(): + """Umaco example of using a cutin""" + + # TODO This is clearly rendering wrong. + _test_render("resources/example_cutin.gbr", "golden/example_cutin.gbr") + + +def _test_render_fully_coincident(): + """Umaco example of coincident lines rendering two contours""" + + _test_render( + "resources/example_fully_coincident.gbr", "golden/example_fully_coincident.gbr" + ) + + +def _test_render_coincident_hole(): + """Umaco example of coincident lines rendering a hole in the contour""" + + _test_render( + "resources/example_coincident_hole.gbr", "golden/example_coincident_hole.gbr" + ) + + +def _test_render_cutin_multiple(): + """Umaco example of a region with multiple cutins""" + + _test_render( + "resources/example_cutin_multiple.gbr", "golden/example_cutin_multiple.gbr" + ) + + +def _test_flash_circle(): + """Umaco example a simple circular flash with and without a hole""" + + _test_render( + "resources/example_flash_circle.gbr", "golden/example_flash_circle.gbr" + ) + + +def _test_flash_rectangle(): + """Umaco example a simple rectangular flash with and without a hole""" + + _test_render( + "resources/example_flash_rectangle.gbr", "golden/example_flash_rectangle.gbr" + ) + + +def _test_flash_obround(): + """Umaco example a simple obround flash with and without a hole""" + + _test_render( + "resources/example_flash_obround.gbr", "golden/example_flash_obround.gbr" + ) + + +def _test_flash_polygon(): + """Umaco example a simple polygon flash with and without a hole""" + + _test_render( + "resources/example_flash_polygon.gbr", "golden/example_flash_polygon.gbr" + ) + + +def _test_holes_dont_clear(): + """Umaco example that an aperture with a hole does not clear the area""" + + _test_render( + "resources/example_holes_dont_clear.gbr", "golden/example_holes_dont_clear.gbr" + ) + + +def _test_render_am_exposure_modifier(): + """Umaco example that an aperture macro with a hole does not clear the area""" + + _test_render( + "resources/example_am_exposure_modifier.gbr", + "golden/example_am_exposure_modifier.gbr", + ) + + +def _resolve_path(path): + return os.path.join(os.path.dirname(__file__), path) + + +def _test_render(gerber_path, png_expected_path, create_output_path=None): + """Render the gerber file and compare to the expected PNG output. + + Parameters + ---------- + gerber_path : string + Path to Gerber file to open + png_expected_path : string + Path to the PNG file to compare to + create_output : string|None + If not None, write the generated PNG to the specified path. + This is primarily to help with + """ + + gerber_path = _resolve_path(gerber_path) + png_expected_path = _resolve_path(png_expected_path) + if create_output_path: + create_output_path = _resolve_path(create_output_path) + + gerber = read(gerber_path) + + # Create GBR output from the input file + ctx = Rs274xContext(gerber.settings) + gerber.render(ctx) + + actual_contents = ctx.dump() + + # If we want to write the file bytes, do it now. This happens + if create_output_path: + with open(create_output_path, "wb") as out_file: + out_file.write(actual_contents.getvalue()) + # Creating the output is dangerous - it could overwrite the expected result. + # So if we are creating the output, we make the test fail on purpose so you + # won't forget to disable this + assert not True, ( + "Test created the output %s. This needs to be disabled to make sure the test behaves correctly" + % (create_output_path,) + ) + + # Read the expected PNG file + + with open(png_expected_path, "r") as expected_file: + expected_contents = expected_file.read() + + assert expected_contents == actual_contents.getvalue() + + return gerber diff --git a/gerbonara/gerber/tests/test_utils.py b/gerbonara/gerber/tests/test_utils.py new file mode 100644 index 0000000..68484d1 --- /dev/null +++ b/gerbonara/gerber/tests/test_utils.py @@ -0,0 +1,167 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +import pytest +from ..utils import * + + +def test_zero_suppression(): + """ Test gerber value parser and writer handle zero suppression correctly. + """ + # Default format + 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), + ("0", 0.0), + ] + for string, value in test_cases: + assert value == parse_gerber_value(string, fmt, zero_suppression) + assert string == write_gerber_value(value, fmt, zero_suppression) + + # 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), + ("0", 0.0), + ] + for string, value in test_cases: + assert value == parse_gerber_value(string, fmt, zero_suppression) + assert string == write_gerber_value(value, fmt, zero_suppression) + + assert write_gerber_value(0.000000001, fmt, "leading") == "0" + assert write_gerber_value(0.000000001, fmt, "trailing") == "0" + + +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), + ((2, 6), "0", 0), + ] + for fmt, string, value in test_cases: + assert value == parse_gerber_value(string, fmt, zero_suppression) + assert string == write_gerber_value(value, fmt, zero_suppression) + + 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), + ((2, 5), "0", 0), + ] + for fmt, string, value in test_cases: + assert value == parse_gerber_value(string, fmt, zero_suppression) + assert string == write_gerber_value(value, fmt, zero_suppression) + + +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 + + +def test_decimal_padding(): + """ Test decimal_string padding + """ + value = 1.123 + assert decimal_string(value, precision=3, padding=True) == "1.123" + assert decimal_string(value, precision=4, padding=True) == "1.1230" + assert decimal_string(value, precision=5, padding=True) == "1.12300" + assert decimal_string(value, precision=6, padding=True) == "1.123000" + assert decimal_string(0, precision=6, padding=True) == "0.000000" + + +def test_parse_format_validation(): + """ Test parse_gerber_value() format validation + """ + pytest.raises(ValueError, parse_gerber_value, "00001111", (7, 5)) + pytest.raises(ValueError, parse_gerber_value, "00001111", (5, 8)) + pytest.raises(ValueError, parse_gerber_value, "00001111", (13, 1)) + + +def test_write_format_validation(): + """ Test write_gerber_value() format validation + """ + pytest.raises(ValueError, write_gerber_value, 69.0, (7, 5)) + pytest.raises(ValueError, write_gerber_value, 69.0, (5, 8)) + pytest.raises(ValueError, write_gerber_value, 69.0, (13, 1)) + + +def test_detect_format_with_short_file(): + """ Verify file format detection works with short files + """ + assert "unknown" == detect_file_format("gerber/tests/__init__.py") + + +def test_validate_coordinates(): + pytest.raises(TypeError, validate_coordinates, 3) + pytest.raises(TypeError, validate_coordinates, 3.1) + pytest.raises(TypeError, validate_coordinates, "14") + pytest.raises(TypeError, validate_coordinates, (0,)) + pytest.raises(TypeError, validate_coordinates, (0, 1, 2)) + pytest.raises(TypeError, validate_coordinates, (0, "string")) + + +def test_convex_hull(): + points = [(0, 0), (1, 0), (1, 1), (0.5, 0.5), (0, 1), (0, 0)] + expected = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)] + assert set(convex_hull(points)) == set(expected) diff --git a/gerbonara/gerber/utils.py b/gerbonara/gerber/utils.py new file mode 100644 index 0000000..3d39df9 --- /dev/null +++ b/gerbonara/gerber/utils.py @@ -0,0 +1,458 @@ +#!/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.utils +============ +**Gerber and Excellon file handling utilities** + +This module provides utility functions for working with Gerber and Excellon +files. +""" + +import os +from math import radians, sin, cos, sqrt, atan2, pi + +MILLIMETERS_PER_INCH = 25.4 + + +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', 'trailing' or 'none' + + Returns + ------- + value : float + 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 + + # 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.lstrip('+') + negative = '-' in value + if negative: + value = value.lstrip('-') + + missing_digits = MAX_DIGITS - len(value) + + if zero_suppression == 'trailing': + digits = list(value + ('0' * missing_digits)) + elif zero_suppression == 'leading': + digits = list(('0' * missing_digits) + value) + else: + digits = list(value) + + result = float( + ''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) + return -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', 'trailing' or 'none' + + Returns + ------- + value : string + The specified value as a Gerber/Excellon-formatted string. + """ + + if format[0] == float: + return "%f" %value + + # 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... (per Gerber spec we should return 0 in all cases, see page + # 77) + if value == 0: + return '0' + + # 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 != '.'] + + # If all the digits are 0, return '0'. + digit_sum = sum([int(digit) for digit in digits]) + if digit_sum == 0: + return '0' + + # Suppression... + if zero_suppression == 'trailing': + while digits and digits[-1] == '0': + digits.pop() + elif zero_suppression == 'leading': + while digits and digits[0] == '0': + digits.pop(0) + + if not digits: + return '0' + + return ''.join(digits) if not negative else ''.join(['-'] + digits) + + +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(',') + else: + integer, decimal = floatstr, "0" + + if len(decimal) > precision: + decimal = decimal[:precision] + elif padding: + decimal = decimal + (precision - len(decimal)) * '0' + + if integer or decimal: + return ''.join([integer, '.', decimal]) + else: + return int(floatstr) + + +def detect_file_format(data): + """ Determine format of a file + + Parameters + ---------- + data : string + string containing file data. + + Returns + ------- + format : string + File format. 'excellon' or 'rs274x' or 'unknown' + """ + lines = data.split('\n') + for line in lines: + if 'M48' in line: + return 'excellon' + elif '%FS' in line: + return 'rs274x' + elif ((len(line.split()) >= 2) and + (line.split()[0] == 'P') and (line.split()[1] == 'JOB')): + return 'ipc_d_356' + return 'unknown' + + +def validate_coordinates(position): + if position is not None: + if len(position) != 2: + raise TypeError('Position must be a tuple (n=2) of coordinates') + else: + for coord in position: + if not (isinstance(coord, int) or isinstance(coord, float)): + raise TypeError('Coordinates must be integers or floats') + + +def metric(value): + """ Convert inch value to millimeters + + Parameters + ---------- + value : float + A value in inches. + + Returns + ------- + value : float + The equivalent value expressed in millimeters. + """ + return value * MILLIMETERS_PER_INCH + + +def inch(value): + """ Convert millimeter value to inches + + Parameters + ---------- + value : float + A value in millimeters. + + Returns + ------- + value : float + The equivalent value expressed in inches. + """ + return value / MILLIMETERS_PER_INCH + + +def rotate_point(point, angle, center=(0.0, 0.0)): + """ Rotate a point about another point. + + Parameters + ----------- + point : tuple(, ) + Point to rotate about origin or center point + + angle : float + Angle to rotate the point [degrees] + + center : tuple(, ) + Coordinates about which the point is rotated. Defaults to the origin. + + Returns + ------- + rotated_point : tuple(, ) + `point` rotated about `center` by `angle` degrees. + """ + angle = radians(angle) + + cos_angle = cos(angle) + sin_angle = sin(angle) + + return ( + cos_angle * (point[0] - center[0]) - sin_angle * (point[1] - center[1]) + center[0], + sin_angle * (point[0] - center[0]) + cos_angle * (point[1] - center[1]) + center[1]) + +def nearly_equal(point1, point2, ndigits = 6): + '''Are the points nearly equal''' + + return round(point1[0] - point2[0], ndigits) == 0 and round(point1[1] - point2[1], ndigits) == 0 + + +def sq_distance(point1, point2): + + diff1 = point1[0] - point2[0] + diff2 = point1[1] - point2[1] + return diff1 * diff1 + diff2 * diff2 + + +def listdir(directory, ignore_hidden=True, ignore_os=True): + """ List files in given directory. + Differs from os.listdir() in that hidden and OS-generated files are ignored + by default. + + Parameters + ---------- + directory : str + path to the directory for which to list files. + + ignore_hidden : bool + If True, ignore files beginning with a leading '.' + + ignore_os : bool + If True, ignore OS-generated files, e.g. Thumbs.db + + Returns + ------- + files : list + list of files in specified directory + """ + os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db') + files = os.listdir(directory) + if ignore_hidden: + files = [f for f in files if not f.startswith('.')] + if ignore_os: + files = [f for f in files if not f in os_files] + return files + +def ConvexHull_qh(points): + #a hull must be a planar shape with nonzero area, so there must be at least 3 points + if(len(points)<3): + raise Exception("not a planar shape") + #find points with lowest and highest X coordinates + minxp=0; + maxxp=0; + for i in range(len(points)): + if(points[i][0]points[maxxp][0]): + maxxp=i; + if minxp==maxxp: + #all points are collinear + raise Exception("not a planar shape") + #separate points into those above and those below the minxp-maxxp line + lpoints=[] + rpoints=[] + #to detemine if point X is on the left or right of dividing line A-B, compare slope of A-B to slope of A-X + #slope is (By-Ay)/(Bx-Ax) + a=points[minxp] + b=points[maxxp] + slopeab=atan2(b[1]-a[1],b[0]-a[0]) + for i in range(len(points)): + p=points[i] + if i == minxp or i == maxxp: + continue + slopep=atan2(p[1]-a[1],p[0]-a[0]) + sdiff=slopep-slopeab + if(sdiffpi):sdiff-=2*pi + if(sdiff>0): + lpoints+=[i] + if(sdiff<0): + rpoints+=[i] + hull=[minxp]+_findhull(rpoints, maxxp, minxp, points)+[maxxp]+_findhull(lpoints, minxp, maxxp, points) + hullo=_optimize(hull,points) + return hullo + +def _optimize(hull,points): + #find triplets that are collinear and remove middle point + toremove=[] + newhull=hull[:] + l=len(hull) + for i in range(l): + p1=hull[i] + p2=hull[(i+1)%l] + p3=hull[(i+2)%l] + #(p1.y-p2.y)*(p1.x-p3.x)==(p1.y-p3.y)*(p1.x-p2.x) + if (points[p1][1]-points[p2][1])*(points[p1][0]-points[p3][0])==(points[p1][1]-points[p3][1])*(points[p1][0]-points[p2][0]): + toremove+=[p2] + for i in toremove: + newhull.remove(i) + return newhull + +def _distance(a, b, x): + #find the distance between point x and line a-b + return abs((b[1]-a[1])*x[0]-(b[0]-a[0])*x[1]+b[0]*a[1]-a[0]*b[1])/sqrt((b[1]-a[1])**2 + (b[0]-a[0])**2 ); + +def _findhull(idxp, a_i, b_i, points): + #if no points in input, return no points in output + if(len(idxp)==0): + return []; + #find point c furthest away from line a-b + farpoint=-1 + fdist=-1.0; + for i in idxp: + d=_distance(points[a_i], points[b_i], points[i]) + if(d>fdist): + fdist=d; + farpoint=i + if(fdist<=0): + #none of the points have a positive distance from line, bad things have happened + return [] + #separate points into those inside triangle, those outside triangle left of far point, and those outside triangle right of far point + a=points[a_i] + b=points[b_i] + c=points[farpoint] + slopeac=atan2(c[1]-a[1],c[0]-a[0]) + slopecb=atan2(b[1]-c[1],b[0]-c[0]) + lpoints=[] + rpoints=[] + for i in idxp: + if i==farpoint: + #ignore triangle vertex + continue + x=points[i] + #if point x is left of line a-c it's in left set + slopeax=atan2(x[1]-a[1],x[0]-a[0]) + if slopeac==slopeax: + continue + sdiff=slopeac-slopeax + if(sdiff<-pi):sdiff+=2*pi + if(sdiff>pi):sdiff-=2*pi + if(sdiff<0): + lpoints+=[i] + else: + #if point x is right of line b-c it's in right set, otherwise it's inside triangle and can be ignored + slopecx=atan2(x[1]-c[1],x[0]-c[0]) + if slopecx==slopecb: + continue + sdiff=slopecx-slopecb + if(sdiff<-pi):sdiff+=2*pi + if(sdiff>pi):sdiff-=2*pi + if(sdiff>0): + rpoints+=[i] + #the hull segment between points a and b consists of the hull segment between a and c, the point c, and the hull segment between c and b + ret=_findhull(rpoints, farpoint, b_i, points)+[farpoint]+_findhull(lpoints, a_i, farpoint, points) + return ret + + +def convex_hull(points): + vertices = ConvexHull_qh(points) + return [points[idx] for idx in vertices] -- cgit