summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.coveragerc8
-rw-r--r--.github/workflows/pcb-tools.yml45
-rw-r--r--.gitignore122
-rw-r--r--.gitlab-ci.yml80
-rw-r--r--.pypirc12
-rw-r--r--LICENSE4
-rw-r--r--Makefile37
-rw-r--r--README.md291
-rw-r--r--doc/Makefile177
-rw-r--r--doc/make.bat242
-rw-r--r--doc/source/about.rst40
-rw-r--r--doc/source/conf.py262
-rw-r--r--doc/source/documentation/excellon.rst42
-rw-r--r--doc/source/documentation/index.rst10
-rw-r--r--doc/source/documentation/operations.rst24
-rw-r--r--doc/source/documentation/render.rst11
-rw-r--r--doc/source/documentation/rs274x.rst37
-rw-r--r--doc/source/features.rst14
-rw-r--r--doc/source/index.rst24
-rw-r--r--examples/cairo_bottom.pngbin0 -> 42180 bytes
-rw-r--r--examples/cairo_example.pngbin0 -> 100252 bytes
-rw-r--r--examples/cairo_example.py78
-rw-r--r--examples/excellon_optimize.py90
-rw-r--r--examples/excellon_optimize_after.PNGbin0 -> 33753 bytes
-rw-r--r--examples/excellon_optimize_before.PNGbin0 -> 90206 bytes
-rw-r--r--examples/gerbers/bottom_copper.GBL1811
-rw-r--r--examples/gerbers/bottom_mask.GBS66
-rw-r--r--examples/gerbers/copper.GTL3457
-rw-r--r--examples/gerbers/ncdrill.DRD51
-rw-r--r--examples/gerbers/silkscreen.GTO2099
-rw-r--r--examples/gerbers/soldermask.GTS162
-rwxr-xr-xexamples/gerbv_test_files/include-file-1.gbx7
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-circle-1.gbx38
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-circle-flash-1.gbx27
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-obround-1.gbx38
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-obround-flash-1.gbx27
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-polygon-1.gbx38
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-polygon-flash-1.gbx27
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-rectangle-1.gbx38
-rwxr-xr-xexamples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx27
-rwxr-xr-xexamples/gerbv_test_files/test-circular-interpolation-1.gbx35
-rwxr-xr-xexamples/gerbv_test_files/test-drill-leading-zero-1.exc8
-rwxr-xr-xexamples/gerbv_test_files/test-drill-repeat-1.exc19
-rwxr-xr-xexamples/gerbv_test_files/test-drill-trailing-zero-1.exc8
-rwxr-xr-xexamples/gerbv_test_files/test-image-justify-1.gbx19
-rwxr-xr-xexamples/gerbv_test_files/test-image-justify-2.gbx19
-rwxr-xr-xexamples/gerbv_test_files/test-image-offset-1.gbx18
-rwxr-xr-xexamples/gerbv_test_files/test-image-offset-2.gbx19
-rwxr-xr-xexamples/gerbv_test_files/test-image-polarity-1.gbx17
-rwxr-xr-xexamples/gerbv_test_files/test-image-rotation-1.gbx21
-rwxr-xr-xexamples/gerbv_test_files/test-include-file-1.gbx11
-rwxr-xr-xexamples/gerbv_test_files/test-layer-axis-select-1.gbx15
-rwxr-xr-xexamples/gerbv_test_files/test-layer-knockout-1.gbx28
-rwxr-xr-xexamples/gerbv_test_files/test-layer-knockout-2.gbx28
-rwxr-xr-xexamples/gerbv_test_files/test-layer-mirror-image-1.gbx23
-rwxr-xr-xexamples/gerbv_test_files/test-layer-mode-1.gbx68
-rwxr-xr-xexamples/gerbv_test_files/test-layer-offset-1.gbx18
-rwxr-xr-xexamples/gerbv_test_files/test-layer-rotation-1.gbx23
-rwxr-xr-xexamples/gerbv_test_files/test-layer-scale-factor-1.gbx17
-rwxr-xr-xexamples/gerbv_test_files/test-layer-step-and_repeat-1.gbx17
-rwxr-xr-xexamples/gerbv_test_files/test-layer-step-and_repeat-2.gbx18
-rwxr-xr-xexamples/gerbv_test_files/test-polygon-fill-1.gbx45
-rw-r--r--examples/pcb_bottom.pngbin0 -> 49825 bytes
-rw-r--r--examples/pcb_example.py53
-rw-r--r--examples/pcb_top.pngbin0 -> 104847 bytes
-rw-r--r--examples/pcb_transparent_copper.pngbin0 -> 110043 bytes
-rwxr-xr-xexamples/render_gerbv_tests.py59
-rw-r--r--gerber/__init__.py28
-rw-r--r--gerber/__main__.py122
-rw-r--r--gerber/am_eval.py109
-rw-r--r--gerber/am_read.py255
-rw-r--r--gerber/am_statements.py1046
-rw-r--r--gerber/cam.py286
-rw-r--r--gerber/common.py71
-rwxr-xr-xgerber/excellon.py904
-rw-r--r--gerber/excellon_report/excellon_drr.py25
-rw-r--r--gerber/excellon_settings.py105
-rw-r--r--gerber/excellon_statements.py979
-rw-r--r--gerber/excellon_tool.py190
-rw-r--r--gerber/exceptions.py36
-rw-r--r--gerber/gerber_statements.py1189
-rw-r--r--gerber/ipc356.py485
-rw-r--r--gerber/layers.py295
-rw-r--r--gerber/ncparam/allegro.py25
-rw-r--r--gerber/operations.py126
-rw-r--r--gerber/pcb.py124
-rw-r--r--gerber/primitives.py1697
-rw-r--r--gerber/render/__init__.py31
-rw-r--r--gerber/render/cairo_backend.py616
-rw-r--r--gerber/render/excellon_backend.py188
-rw-r--r--gerber/render/render.py246
-rw-r--r--gerber/render/rs274x_backend.py510
-rw-r--r--gerber/render/theme.py112
-rw-r--r--gerber/rs274x.py800
-rw-r--r--gerber/tests/__init__.py0
-rw-r--r--gerber/tests/golden/example_am_exposure_modifier.pngbin0 -> 10091 bytes
-rw-r--r--gerber/tests/golden/example_coincident_hole.pngbin0 -> 47261 bytes
-rw-r--r--gerber/tests/golden/example_cutin_multiple.pngbin0 -> 1348 bytes
-rw-r--r--gerber/tests/golden/example_flash_circle.pngbin0 -> 5978 bytes
-rw-r--r--gerber/tests/golden/example_flash_obround.pngbin0 -> 3443 bytes
-rw-r--r--gerber/tests/golden/example_flash_polygon.pngbin0 -> 4087 bytes
-rw-r--r--gerber/tests/golden/example_flash_rectangle.pngbin0 -> 1731 bytes
-rw-r--r--gerber/tests/golden/example_fully_coincident.pngbin0 -> 71825 bytes
-rw-r--r--gerber/tests/golden/example_holes_dont_clear.pngbin0 -> 11552 bytes
-rw-r--r--gerber/tests/golden/example_not_overlapping_contour.pngbin0 -> 71825 bytes
-rw-r--r--gerber/tests/golden/example_not_overlapping_touching.pngbin0 -> 96557 bytes
-rw-r--r--gerber/tests/golden/example_overlapping_contour.pngbin0 -> 33301 bytes
-rw-r--r--gerber/tests/golden/example_overlapping_touching.pngbin0 -> 33301 bytes
-rw-r--r--gerber/tests/golden/example_simple_contour.pngbin0 -> 31830 bytes
-rw-r--r--gerber/tests/golden/example_single_contour.pngbin0 -> 556 bytes
-rw-r--r--gerber/tests/golden/example_single_contour_3.pngbin0 -> 2297 bytes
-rw-r--r--gerber/tests/golden/example_single_quadrant.gbr16
-rw-r--r--gerber/tests/golden/example_single_quadrant.pngbin0 -> 9658 bytes
-rw-r--r--gerber/tests/golden/example_two_square_boxes.gbr16
-rw-r--r--gerber/tests/golden/example_two_square_boxes.pngbin0 -> 18219 bytes
-rw-r--r--gerber/tests/resources/board_outline.GKO503
-rw-r--r--gerber/tests/resources/bottom_copper.GBL1811
-rw-r--r--gerber/tests/resources/bottom_mask.GBS66
-rw-r--r--gerber/tests/resources/bottom_silk.GBO6007
-rw-r--r--gerber/tests/resources/example_am_exposure_modifier.gbr16
-rw-r--r--gerber/tests/resources/example_coincident_hole.gbr24
-rw-r--r--gerber/tests/resources/example_cutin.gbr18
-rw-r--r--gerber/tests/resources/example_cutin_multiple.gbr28
-rw-r--r--gerber/tests/resources/example_flash_circle.gbr10
-rw-r--r--gerber/tests/resources/example_flash_obround.gbr10
-rw-r--r--gerber/tests/resources/example_flash_polygon.gbr10
-rw-r--r--gerber/tests/resources/example_flash_rectangle.gbr10
-rw-r--r--gerber/tests/resources/example_fully_coincident.gbr23
-rw-r--r--gerber/tests/resources/example_guess_by_content.g0166
-rw-r--r--gerber/tests/resources/example_holes_dont_clear.gbr13
-rw-r--r--gerber/tests/resources/example_level_holes.gbr39
-rw-r--r--gerber/tests/resources/example_not_overlapping_contour.gbr20
-rw-r--r--gerber/tests/resources/example_not_overlapping_touching.gbr20
-rw-r--r--gerber/tests/resources/example_overlapping_contour.gbr20
-rw-r--r--gerber/tests/resources/example_overlapping_touching.gbr20
-rw-r--r--gerber/tests/resources/example_simple_contour.gbr16
-rw-r--r--gerber/tests/resources/example_single_contour_1.gbr15
-rw-r--r--gerber/tests/resources/example_single_contour_2.gbr15
-rw-r--r--gerber/tests/resources/example_single_contour_3.gbr15
-rw-r--r--gerber/tests/resources/example_single_quadrant.gbr18
-rw-r--r--gerber/tests/resources/example_two_square_boxes.gbr19
-rw-r--r--gerber/tests/resources/ipc-d-356.ipc115
-rw-r--r--gerber/tests/resources/multiline_read.ger9
-rw-r--r--gerber/tests/resources/ncdrill.DRD51
-rw-r--r--gerber/tests/resources/top_copper.GTL27
-rw-r--r--gerber/tests/resources/top_mask.GTS162
-rw-r--r--gerber/tests/resources/top_silk.GTO2099
-rw-r--r--gerber/tests/test_am_statements.py395
-rw-r--r--gerber/tests/test_cairo_backend.py279
-rw-r--r--gerber/tests/test_cam.py151
-rw-r--r--gerber/tests/test_common.py38
-rw-r--r--gerber/tests/test_excellon.py366
-rw-r--r--gerber/tests/test_excellon_statements.py734
-rw-r--r--gerber/tests/test_gerber_statements.py959
-rw-r--r--gerber/tests/test_ipc356.py148
-rw-r--r--gerber/tests/test_layers.py158
-rw-r--r--gerber/tests/test_primitives.py1429
-rw-r--r--gerber/tests/test_rs274x.py55
-rw-r--r--gerber/tests/test_rs274x_backend.py232
-rw-r--r--gerber/tests/test_utils.py167
-rw-r--r--gerber/utils.py458
-rw-r--r--gerbonara/__init__.py1
-rw-r--r--gerbonara/cli.py39
-rw-r--r--requirements-dev.txt5
-rw-r--r--requirements-docs.txt6
-rw-r--r--requirements.txt2
-rw-r--r--setup.cfg2
-rw-r--r--setup.py130
-rw-r--r--test-requirements.txt5
169 files changed, 37632 insertions, 323 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..3399dbe
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,8 @@
+[run]
+branch = True
+source = gerber
+
+[report]
+ignore_errors = True
+omit =
+ gerber/tests/*
diff --git a/.github/workflows/pcb-tools.yml b/.github/workflows/pcb-tools.yml
new file mode 100644
index 0000000..feb0a74
--- /dev/null
+++ b/.github/workflows/pcb-tools.yml
@@ -0,0 +1,45 @@
+name: pcb-tools
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: [3.5, 3.6, 3.7, 3.8]
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v1
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ pip install -r requirements-dev.txt
+ - name: Test with pytest
+ run: |
+ pytest
+ coverage:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.8
+ - name: Install dependencies
+ run: |
+ pip install -r requirements-dev.txt
+ - name: Run coverage
+ run: |
+ make test-coverage
+ - uses: codecov/codecov-action@v1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./coverage.xml
+ flags: unittest
diff --git a/.gitignore b/.gitignore
index 075c34c..11614af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,115 @@
-.vscode
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
.python-version
-*.pyc
-__pycache__
-pcb_tools_extension.egg-info
-test
-outputs
-build
-dist
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..d6dce87
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,80 @@
+stages:
+ - test
+ - coverage
+ - build
+ - upload
+
+image: python:3-alpine
+
+variables:
+ TWINE_NON_INTERACTIVE: "true"
+ CONFIG_FILE: ".pypirc"
+
+cache:
+ paths:
+ - .pipcache/
+
+before_script:
+ - apk --no-cache add make
+ - pip --cache-dir .pipcache install -r test-requirements.txt
+ - cat /etc/alpine-release
+ - python -V
+
+.test:
+ stage: test
+ script:
+ - make test
+
+test:3.6:
+ extends: .test
+ image: python:3.6-alpine
+
+test:3.7:
+ extends: .test
+ image: python:3.7-alpine
+
+test:3.8:
+ extends: .test
+ image: python:3.8-alpine
+
+test:3.9:
+ extends: .test
+ image: python:3.9-alpine
+
+# 3.10
+test:rc:
+ extends: .test
+ allow_failure: true
+ image: python:rc-alpine
+
+testpypi:
+ stage: upload
+ script:
+ - pip --cache-dir .pipcache install twine
+ - python setup.py sdist bdist_wheel
+ - twine upload --skip-existing --repository testpypi dist/*
+ only:
+ - master
+
+pypi:
+ stage: upload
+ script:
+ - pip --cache-dir .pipcache install twine
+ - python setup.py sdist bdist_wheel
+ - twine upload --skip-existing --repository pypi dist/*
+ only:
+ - tags
+
+aur_git:
+ stage: upload
+ script:
+ - echo "build pygerber-git"
+ only:
+ - master
+
+aur:
+ stage: upload
+ script:
+ - echo "build pygerber"
+ only:
+ - tags
diff --git a/.pypirc b/.pypirc
new file mode 100644
index 0000000..6fc43db
--- /dev/null
+++ b/.pypirc
@@ -0,0 +1,12 @@
+[distutils]
+index-servers =
+ pypi
+ testpypi
+
+[pypi]
+username = __token__
+password = ${env.PYPI_TOKEN}
+
+[testpypi]
+username = __token__
+password = ${env.TESTPYPI_TOKEN}
diff --git a/LICENSE b/LICENSE
index 261eeb9..8dada3e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -178,7 +178,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
+ boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..966f2a0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,37 @@
+
+PYTHON ?= python
+PYTEST ?= pytest
+
+.PHONY: clean
+clean: doc-clean
+ find . -name '*.pyc' -delete
+ rm -rf *.egg-info
+ rm -f .coverage
+ rm -f coverage.xml
+
+.PHONY: test
+test:
+ $(PYTEST)
+
+.PHONY: test-coverage
+test-coverage:
+ rm -f .coverage
+ rm -f coverage.xml
+ $(PYTEST) --cov=./ --cov-report=xml
+
+.PHONY: install
+install:
+ PYTHONPATH=. $(PYTHON) setup.py install
+
+sdist:
+ python3 setup.py sdist
+
+bdist_wheel:
+ python3 setup.py bdist_wheel
+
+upload: sdist bdist_wheel
+ twine upload -s -i contact@gerbonara.io --config-file ~/.pypirc --skip-existing --repository pypi dist/*
+
+testupload: sdist bdist_wheel
+ twine upload --config-file ~/.pypirc --skip-existing --repository testpypi dist/*
+
diff --git a/README.md b/README.md
index a95dc56..e8681f6 100644
--- a/README.md
+++ b/README.md
@@ -1,282 +1,51 @@
-pcb-tools-extension
-===
-pcb-tools-extension is a Python library to panelize gerber files.
-This library is designed based on [pcb-tools](https://github.com/curtacircuitos/pcb-tools) which provides cool functionality to handle PCB such as generationg PCB image from gerber files.
+[![pipeline status](https://gitlab.com/gerbonara/gerbonara/badges/master/pipeline.svg)](https://gitlab.com/gerbonara/gerbonara/commits/master)
+[![coverage report](https://gitlab.com/gerbonara/gerbonara/badges/master/coverage.svg)](https://gitlab.com/gerbonara/gerbonara/commits/master)
+[![pypi](https://img.shields.io/pypi/v/gerbonara)](https://pypi.org/project/gerbonara/)
+[![aur](https://img.shields.io/aur/version/python-gerbonara)](https://aur.archlinux.org/packages/python-gerbonara/)
-pcb-tools-extension adds following function to pcb-tools.
+# gerbonara
-- Rotate PCB data
-- Write back loaded PCB data (original pcb-tools does not work in some condition)
-- Merge multiple PCB data
-- Translate DXF file to PCB data
+Tools to handle Gerber and Excellon files in Python.
-Only RS-274x format and Excellon drill format data can be handled by current version of this library.
+This repository is a friendly fork of [phsilva's pcb-tools](https://github.com/curtacircuitos/pcb-tools) with
+[extensions from opiopan](https://github.com/opiopan/pcb-tools-extension) integrated. We decided to fork pcb-tools since
+we need it as a dependency for [gerbolyze](https://gitlab.com/gerbolyze/gerbolyze) and pcb-tools was sometimes very
+behind on bug fixes.
-## Installation
-You can install a stable version by following step.
+# Installation
-```shell
-$ pip install pcb-tools-extension
-```
-
-If you have a intention to try latest developing version, please install as follows.
+Arch Linux:
-```shell
-$ pip install git+https://github.com/opiopan/pcb-tools-extension.git
```
-
-## How to panelize
-Following code is a example to panelize two top metal layer files.
-
-``` python
-import gerberex
-
-ctx = gerberex.GerberComposition()
-
-metal1 = gerberex.read('board1.gtl')
-ctx.merge(metal1)
-
-metal2 = gerberex.read('board2.gtl')
-metal2.to_metric()
-metal2.rotate(-20)
-metal2.offset(30, 0)
-ctx.merge(metal2)
-
-ctx.dump('panelized-board.gtl')
+yay -S python-gerbonara
```
-```rotate()``` method can be used to rotate PCB data counterclockwise. you have to specify angle in degree.<br>
-```offset()``` method can be used to move PCB data. Specified offset values are interpreted according to unit setting of PCB data. In case of the above code, ```board2.gtl``` move to 30mm left since ```to_metric()``` is called.
-
-In case of Excellon drill data, you have to use ```DrillCompositon``` instead of ```GerberComposition```.
+Python:
-```python
-import gerberex
-
-ctx = gerberex.DrillComposition()
-
-drill1 = gerberex.read('board1.txt')
-ctx.merge(drill1)
-
-drill2 = gerberex.read('board2.txt')
-drill2.to_metric()
-drill2.rotate(-20)
-drill2.offset(30, 0)
-ctx.merge(drill2)
-
-ctx.dump('panelized-board.txt')
```
-
-## DXF file translation
-pcb-tools-extension hsa a function to load a DXF file and handle that as same as RX-274x gerber file or Excellon NC file.<br>
-In this version, Only line, circle, arc, and polyline objects are recognized and are translated to gerber file or NC file.
-
-### Two way to tranlate DXF file
-Both composition objects, ```GerberComposition``` for RX-274x and ```DrillionComposition``` for Excellon, can accept an object created as result of DXF file loaded. When composition object dump text stream, DXF data tranclate to appropriate format data.<br>
-The object which represent DXF file, can also output translated data directly by ```save``` method. In this case output format is specified by ```filetype``` argument. If ```filetype``` argument is ommited, DXF data is translated to RX-274x gerber data.
-
-```python
-import gerberex
-dxf = gerberex.read('sample.dxf')
-
-# translate to RX-274x using composition object
-ctx = gerberex.GerberComposition()
-ctx.merge(dxf)
-ctx.dump('sample.gml')
-
-# translate to Excellon using composition object
-ctx = gerberex.DrillComposition()
-ctx.merge(dxf)
-ctx.dump('sample.txt')
-
-# translate to RX-274x directly
-dxf.save('sample2.gml')
-
-# translate to Excellon directly
-dxf.save('sample2.txt', filetype=dxf.FT_EXCELLON)
-```
-
-### Generating Rectangle
-If you want to arrange simple rectangle for PCB outline, ```gerberex.rectangle()``` is better solution. This generate a object representing a rectangle compatible with DXF file object.<br>
-
-```python
-import gerberex
-
-outline = gerberex.rectangle(width=100, height=100, units='metric')
-outline.write('outline.gml')
+pip install gerbonara
```
-### Drawing Mode
-PCB tools extension provide three type of translation method that affects geometric finish. These method are specified a value for ```draw_mode``` attribute, as ```DM_LINE```, ```DM_MOUSE_BITES```, or ```DM_FILL```.<br>
-```DM_LINE``` and ```DM_MOUSE_BITES``` are used to translate to both of RX-274x and Excellon, however ```DM_FILL``` is used to translate to only RX-274x.
-
-![Drawing Mode](https://raw.githubusercontent.com/wiki/opiopan/pcb-tools-extension/images/draw_mode.jpg)
-
-- **draw_mode = DM_LINE**<br>
- All edge expressed as DXF line object, circle object, arc object and plyline objects are translated to line and arc applied a circular aperture in case of RX-274x. That circular aperture radius is specified by ```width``` attribute. Default value of width is 0.<br>
- In case of Excellon, DXF objects are translated to routing path command sequence.<br>
- This function is useful to generate outline data of pnanelized PCB boad.
-
- ```python
- import gerberex
-
- dxf = gerberex.read('outline.dxf')
- dxf.to_inch()
- dxf.width = 0.004
- dxf.write('outline.gml')
- ```
-
-- **draw_mode = DM_MOUSE_BITES**<br>
- <img alt="mouse bites" src="https://raw.githubusercontent.com/wiki/opiopan/pcb-tools-extension/images/mousebites.png" width=200 align="right">
- If DM_MOUSE_BITES is specified for draw_mode, filled circles are arranged at equal intervals along a paths consisted of DXF line, arc, circle, and plyline objects.
- DXF file object in this state can be merged to excellon file also. That means you can arrange mouse bites easily.
-
- ```python
- import gerberex
+# Usage
- ctx = gerberex.DrillComposition()
- drill = gerberex.read('drill.txt')
- ctx.merge(drill)
-
- dxf = gerberex.read('mousebites.dxf')
- dxf.draw_mode = dxf.DM_MOUSE_BITES
- dxf.to_metric()
- dxf.width = 0.5
- dxf.pitch = 1
- ctx.merge(dxf)
-
- ctx.dump('merged_drill.txt')
- ```
-
-- **draw_mode = DM_FILL**<br>
- You can translate DXF closed shapes such as circle to RX-274x polygon fill sequence.<br>
- In order to fill closed shapes, ```DM_FILL``` has to be set to ```draw_mode``` property. In this mode, All object except closed shapes listed below are ignored.
-
- - circle
- - closed polyline
- - closed path which consists of lines and arcs
-
- If a closed shape is completly included in other closed shape, The inner shape will be draw with reversed polality of container shape as above example image.<br>
-
- I assume there are two typical use cases for this mode.<br>
- One is to arrange logo design on silk layer. This is superior to other method generating raster image data since image data express as vector data.<br>
- The other one is generating gerber data represented cropped area of panelized PCB.
- By merging rectangle and PCB outline data, generate a file represented cropped area as below, and this kind of data is useful to make PCB image look good a little bit.<br>
- [This script](https://github.com/opiopan/pcb-tools-extension/blob/master/examples/genimage.py) which generate example image shown below, also uses this technic.
-
- ```python
- import gerberex
-
- ctx = gerberex.GerberComposition()
-
- rectangle = gerberex.rectangle(width=100, height=100, left=0, bottom=0, units='metric')
- rectangle.draw_mode = rectangle.DM_FILL
- ctx.merge(rectangle)
-
- outline = gerberex.read('outline.dxf')
- outline.draw_mode = outline.DM_FILL
- outline.negate_polarity()
- ctx.merge(outline)
-
- ctx.dump('cropped_area.gml')
- ```
-
- NOTE: ```DM_FILL``` can be used only to generate RX-274x data, it cannot be used to generate Excellon data.
-
-
-## Panelizing Example
-This example board image is generated by following scripts from [these source data](https://github.com/opiopan/pcb-tools-extension/tree/master/examples/inputs).
-
-- [panelizing script](https://github.com/opiopan/pcb-tools-extension/blob/master/examples/panelize.py)
-- [imaging script](https://github.com/opiopan/pcb-tools-extension/blob/master/examples/genimage.py)
-
-<p align="center">
-<img alt="description" src="https://raw.githubusercontent.com/wiki/opiopan/pcb-tools-extension/images/panelized.jpg" width=600>
-</p>
-
-## Notes
-
-### Equivalence of output
-pcb-tools-extension generate data block stream to focus equivalence of final image, but not focus equivalence of data block sequence.
-There are some difference between input data and output data as below.
-
-- **Aperture definition [RS-274x]**<br>
- When gerber data is rotated, it's necessory to rotate not only coordinates whilch indicate locations of drawing aperture, but also aperture geometory itself.
- However, standard aperture templates, such as rectangle, cannot rotate. These standard aperture templates can be placed only horizontally or vertically.<br>
- Threfore, pcb-tools-extension replace aperture definitions using standard aperture template to aperture macro that represent equivalent shape.<br>
- For example, In case of rotating folowing aperture definition 20 degrees counter clockwise,
-
- ```rs-274x
- %ADD10R,1X0.5X0.2*%
- ```
-
- pcb-toolse-extension generate a aperture macro definition and a aperture definition referencing that macro as below.
-
- ```rs-274x
- %AMMACR*
- 21,1,$1,$2,0,0,20*
- 1,0,$3,0,0,20*%
- %ADD10MACR,1X0.5X0.2*%
- ```
-
-- **File Scope Modifier [RS-274x]**<br>
- Commands that affect entire image and should be specified only once in a file, such as ```MI``` (Mirror Image) command, sometimes cause contradiction when multiple gerber file are merged.<br>
- For example, when mergeing a file containing ```%MIA1B0*%``` command and a file containing ```%MIA0B1*``` command, which command should remain as output?
- Of cause, it is impossible that generate correct merged image by specifiing any ```MI``` command.<br>
- pcb-tools-extension translate coordinate data reflecting these file socpe modifier to address this probrem, then ommit these modifier command.<br>
- ```MI```, ```OF```, ```SF```, ```AS```, ```IP```, and ```IR``` are in a this category.
-
-- **Coodinate Normalizing [RS-274x, Excellon]**<br>
- RS-274x specification and Excellon specification allow various notation to express a coordinate. However pcb-tools-extension normalize coordinate notation in order to correct deprecated notation and ease internal process as below.
-
- - Relative coordinates are translated to absolute coordinates.
- - Ommited coordinate values are compensated.
- - Leading zeros are ommited.
-
-- **Single Quadlant mode [RS-274x]**<br>
- Cercular interpolation coordinate data in single quadlant is difficult to rotate, because circular arc may pass across two quadlants after rotation.<br>
- In order to avoid this problem, pcb-tools-extension change single quadlant mode coordinates specification to multi quadlangt mode.
-
-- **NC controll command [Excellon]**<br>
- Form histrical reason, Excellon NC controll format is used to specify drill information to PCB fabricator.<br>
- On the other hand, from PCB fabricator point of view, they don't need information other than geometric information, such as drill speed. Because these NC controll sequence doesn't send to NC machine directly, PCB fabricator import customers excellon NC file to their CAD / CAM to pnaelize and check, then they export NC controll data for their NC machine.<br>
- pcb-tools-extension ommit all NC command which do not contribute to geometry expression. Specifically, only tool definitions (diametor of drill), tool selections, drilling coordinates, and routing paths are output.
-
-- **Unimportant Command [RS-274x, Excellon]**<br>
- Commands not affecting final image such as comment are ommited.
-
-### Negative image polarity
-Sometimes, ```%IPNEG*%``` is specified at header of RS-274x file to create negative image. <br>
-As mentioned [above](#Equivalence%20of%20output), ```IP``` command is ommited when pcb-tools-extension generate output file. In this case, image polarity is nagated by using ```LP``` command. However this generated file doesn't equal to original image since it does'nt contain base dark image.<br>
-Please merge base dark rectangle explicitly when you handle negative image file as below.
+Here's a simple example:
```python
-import gerberex
-
-ctx = gerberex.GerberComposition()
-base = gerberex.rectangle(width=30, height=30, left=-5, bottom=-5, units='metric')
-base.draw_mode = base.DM_FILL
-ctx.merge(base)
-metal = gerberex.read('negative_image.gtl')
-ctx.merge(metal)
-```
-
-## Limitations
+import gerbonara
+from gerbonara.render import GerberCairoContext
-### RS-274x
-pcb-tools-extension cannot handle all commands that the RS-274x parser implemented in
-[pcb-tools](https://github.com/curtacircuitos/pcb-tools) doesn't handle so far.<br>
-From the imaging point of view, pcb-tools-extension has following limitations.
+# Read gerber and Excellon files
+top_copper = gerbonara.read('example.GTL')
+nc_drill = gerbonara.read('example.txt')
-- Files contains ```IJ``` and ```IO``` commands, that affect entire image, cannot be handled correctly.
-- Files contains ```SR``` command to specify repeated pattern cannot be handled correctly.
-- Aperture block defined by ```AB``` command cannot be handled correctly.
+# Rendering context
+ctx = GerberCairoContext()
-### Excellon
-pcb-tools-extension extends excellon parser in [pcb-tools](https://github.com/curtacircuitos/pcb-tools) to support routing operation. However following limitations still remain.
-
-- User defined stored pattern defined by ```M99``` command cannot be handled.
-- Canned text specified by ```M97``` command cannot be handled.
-- Pattern defined by ```M25``` command cannot be handled.
+# Create SVG image
+top_copper.render(ctx)
+nc_drill.render(ctx, 'composite.svg')
+```
+---
+Made with ❤️ and 🐍.
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..5c6b560
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GerberTools.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GerberTools.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/GerberTools"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GerberTools"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/doc/make.bat b/doc/make.bat
new file mode 100644
index 0000000..2860659
--- /dev/null
+++ b/doc/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
+set I18NSPHINXOPTS=%SPHINXOPTS% source
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\GerberTools.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\GerberTools.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/doc/source/about.rst b/doc/source/about.rst
new file mode 100644
index 0000000..85e7184
--- /dev/null
+++ b/doc/source/about.rst
@@ -0,0 +1,40 @@
+About PCB Tools
+===============
+
+
+PCB Tools provides a set of utilities for visualizing and working with PCB
+design files in a variety of formats. The design files are generally referred
+to as Gerber files. This is a generic term that may refer to
+`RS-274X (Gerber) <http://en.wikipedia.org/wiki/Gerber_format>`_,
+`ODB++ <http://en.wikipedia.org/wiki/ODB%2B%2B>`_ ,
+or `Excellon <http://en.wikipedia.org/wiki/Excellon_format>`_ files. These
+file formats are used by the CNC equipment used to manufacutre PCBs.
+
+PCB Tools currently supports the following file formats:
+
+- Gerber (RS-274X)
+- Excellon
+
+with planned support for IPC-2581, ODB++ and more.
+
+Image Rendering
+~~~~~~~~~~~~~~~
+.. image:: ../../examples/cairo_example.png
+ :alt: Rendering Example
+
+The PCB Tools module provides tools to visualize PCBs and export images in a
+variety of formats, including SVG and PNG.
+
+
+
+
+Future Plans
+~~~~~~~~~~~~
+We are working on adding the following features to PCB Tools:
+
+- Design Rules Checking
+- Editing
+- Panelization
+
+
+
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100644
index 0000000..238a0b8
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,262 @@
+# -*- coding: utf-8 -*-
+#
+# Gerber Tools documentation build configuration file, created by
+# sphinx-quickstart on Sun Sep 28 18:16:46 2014.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../../'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.autosummary',
+ 'numpydoc',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'PCB Tools'
+copyright = u'2014 Paulo Henrique Silva <ph.silva@gmail.com>, Hamilton Kibbe <ham@hamiltonkib.be>'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1'
+# The full version, including alpha/beta/rc tags.
+release = '0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = False
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'PCBToolsdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'PCBTools.tex', u'PCB Tools Documentation',
+ u'Hamilton Kibbe', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'pcbtools', u'PCB Tools Documentation',
+ [u'Hamilton Kibbe'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'PCBTools', u'PCB Tools Documentation',
+ u'Hamilton Kibbe', 'PCBTools', 'Tools for working with PCB CAM files.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/doc/source/documentation/excellon.rst b/doc/source/documentation/excellon.rst
new file mode 100644
index 0000000..7ac3b39
--- /dev/null
+++ b/doc/source/documentation/excellon.rst
@@ -0,0 +1,42 @@
+:mod:`excellon` --- Excellon file handling
+==============================================
+
+.. module:: excellon
+ :synopsis: Functions and classes for handling Excellon files
+.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
+
+
+The Excellon format is the most common format for exporting PCB drill
+information. The Excellon format is used to program CNC drilling macines for
+drilling holes in PCBs. As such, excellon files are sometimes refererred to as
+NC-drill files. The Excellon format reference is available
+`here <http://www.excellon.com/manuals/program.htm>`_. The :mod:`excellon`
+submodule implements calsses to read and write excellon files without having
+to know the precise details of the format.
+
+The :mod:`excellon` submodule's :func:`read` function serves as a
+simple interface for parsing excellon files. The :class:`ExcellonFile` class
+stores all the information contained in an Excellon file allowing the file to
+be analyzed, modified, and updated. The :class:`ExcellonParser` class is used
+in the background for parsing RS-274X files.
+
+.. _excellon-contents:
+
+Functions
+---------
+The :mod:`excellon` module defines the following functions:
+
+.. autofunction:: gerber.excellon.read
+
+
+Classes
+-------
+The :mod:`excellon` module defines the following classes:
+
+.. autoclass:: gerber.excellon.ExcellonFile
+ :members:
+
+
+.. autoclass:: gerber.excellon.ExcellonParser
+ :members:
+ \ No newline at end of file
diff --git a/doc/source/documentation/index.rst b/doc/source/documentation/index.rst
new file mode 100644
index 0000000..dec83f9
--- /dev/null
+++ b/doc/source/documentation/index.rst
@@ -0,0 +1,10 @@
+PCB Tools Reference
+======================
+
+.. toctree::
+ :maxdepth: 2
+
+ Gerber (RS-274X) Files <rs274x>
+ Excellon Files <excellon>
+ Operations <operations>
+ Rendering <render>
diff --git a/doc/source/documentation/operations.rst b/doc/source/documentation/operations.rst
new file mode 100644
index 0000000..6551236
--- /dev/null
+++ b/doc/source/documentation/operations.rst
@@ -0,0 +1,24 @@
+:mod:`operations` --- Cam File operations
+=========================================
+
+.. module:: operations
+ :synopsis: Functions for modifying CAM files
+.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
+
+
+The :mod:`operations` module provides functions which modify
+:class:`gerber.cam.CamFile` objects. All of the functions in this module
+return a modified copy of the supplied file.
+
+.. _operations-contents:
+
+Functions
+---------
+The :mod:`operations` module defines the following functions:
+
+.. autofunction:: gerber.operations.to_inch
+.. autofunction:: gerber.operations.to_metric
+.. autofunction:: gerber.operations.offset
+
+
+
diff --git a/doc/source/documentation/render.rst b/doc/source/documentation/render.rst
new file mode 100644
index 0000000..324ef71
--- /dev/null
+++ b/doc/source/documentation/render.rst
@@ -0,0 +1,11 @@
+:mod:`render` --- Gerber file Rendering
+==============================================
+
+.. module:: render
+ :synopsis: Functions and classes for handling Excellon files
+.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
+
+Render Module
+-------------
+.. automodule:: gerber.render.render
+ :members:
diff --git a/doc/source/documentation/rs274x.rst b/doc/source/documentation/rs274x.rst
new file mode 100644
index 0000000..00094d4
--- /dev/null
+++ b/doc/source/documentation/rs274x.rst
@@ -0,0 +1,37 @@
+:mod:`rs274x` --- RS-274X file handling
+==============================================
+
+.. module:: rs274x
+ :synopsis: Functions and classes for handling RS-274X files
+.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
+
+
+The RS-274X (Gerber) format is the most common format for exporting PCB
+artwork. The Specification is published by Ucamco and is available
+`here <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_.
+The :mod:`rs274x` submodule implements calsses to read and write
+RS-274X files without having to know the precise details of the format.
+
+The :mod:`rs274x` submodule's :func:`read` function serves as a
+simple interface for parsing gerber files. The :class:`GerberFile` class
+stores all the information contained in a gerber file allowing the file to be
+analyzed, modified, and updated. The :class:`GerberParser` class is used in
+the background for parsing RS-274X files.
+
+.. _gerber-contents:
+
+Functions
+---------
+The :mod:`rs274x` module defines the following functions:
+
+.. autofunction:: gerber.rs274x.read
+
+Classes
+-------
+The :mod:`rs274x` module defines the following classes:
+
+.. autoclass:: gerber.rs274x.GerberFile
+ :members:
+
+.. autoclass:: gerber.rs274x.GerberParser
+ :members: \ No newline at end of file
diff --git a/doc/source/features.rst b/doc/source/features.rst
new file mode 100644
index 0000000..67d9e2a
--- /dev/null
+++ b/doc/source/features.rst
@@ -0,0 +1,14 @@
+Feature Suppport
+================
+
+Currently supported features are as follows:
+
+============ ======== =========== ================ ====== ======= =======
+File Format Parsing Rendering Unit Conversion Scale Offset Rotate
+============ ======== =========== ================ ====== ======= =======
+RS274-X Yes Yes Yes No Yes No
+Excellon Yes Yes Yes No Yes No
+ODB++ No No No No No No
+============ ======== =========== ================ ====== ======= =======
+
+
diff --git a/doc/source/index.rst b/doc/source/index.rst
new file mode 100644
index 0000000..c96ff8a
--- /dev/null
+++ b/doc/source/index.rst
@@ -0,0 +1,24 @@
+.. PCB-tools documentation master file, created by
+ sphinx-quickstart on Sun Sep 28 18:16:46 2014.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+PCB-Tools
+========================================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 1
+
+ about
+ features
+ documentation/index
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/examples/cairo_bottom.png b/examples/cairo_bottom.png
new file mode 100644
index 0000000..70f7551
--- /dev/null
+++ b/examples/cairo_bottom.png
Binary files differ
diff --git a/examples/cairo_example.png b/examples/cairo_example.png
new file mode 100644
index 0000000..4b4ee0a
--- /dev/null
+++ b/examples/cairo_example.png
Binary files differ
diff --git a/examples/cairo_example.py b/examples/cairo_example.py
new file mode 100644
index 0000000..ecfed4d
--- /dev/null
+++ b/examples/cairo_example.py
@@ -0,0 +1,78 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""
+This example demonstrates the use of pcb-tools with cairo to render a composite
+image from a set of gerber files. Each layer is loaded and drawn using a
+GerberCairoContext. The color and opacity of each layer can be set individually.
+Once all thedesired layers are drawn on the context, the context is written to
+a .png file.
+"""
+
+import os
+from gerber import load_layer
+from gerber.render import RenderSettings, theme
+from gerber.render.cairo_backend import GerberCairoContext
+
+GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
+
+
+# Open the gerber files
+copper = load_layer(os.path.join(GERBER_FOLDER, 'copper.GTL'))
+mask = load_layer(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
+silk = load_layer(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
+drill = load_layer(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
+
+# Create a new drawing context
+ctx = GerberCairoContext()
+
+# Draw the copper layer. render_layer() uses the default color scheme for the
+# layer, based on the layer type. Copper layers are rendered as
+ctx.render_layer(copper)
+
+# Draw the soldermask layer
+ctx.render_layer(mask)
+
+
+# The default style can be overridden by passing a RenderSettings instance to
+# render_layer().
+# First, create a settings object:
+our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85)
+
+# Draw the silkscreen layer, and specify the rendering settings to use
+ctx.render_layer(silk, settings=our_settings)
+
+# Draw the drill layer
+ctx.render_layer(drill)
+
+# Write output to png file
+ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_example.png'))
+
+# Load the bottom layers
+copper = load_layer(os.path.join(GERBER_FOLDER, 'bottom_copper.GBL'))
+mask = load_layer(os.path.join(GERBER_FOLDER, 'bottom_mask.GBS'))
+
+# Clear the drawing
+ctx.clear()
+
+# Render bottom layers
+ctx.render_layer(copper)
+ctx.render_layer(mask)
+ctx.render_layer(drill)
+
+# Write png file
+ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_bottom.png'))
diff --git a/examples/excellon_optimize.py b/examples/excellon_optimize.py
new file mode 100644
index 0000000..5f0adbc
--- /dev/null
+++ b/examples/excellon_optimize.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Example using pcb-tools with tsp-solver (github.com/dmishin/tsp-solver) to
+# optimize tool paths in an Excellon file.
+#
+#
+# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
+# Based on a script by https://github.com/koppi
+#
+# 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 sys
+import math
+import gerber
+from operator import sub
+from gerber.excellon import DrillHit
+
+try:
+ from tsp_solver.greedy import solve_tsp
+except ImportError:
+ print('\n=================================================================\n'
+ 'This example requires tsp-solver be installed in order to run.\n\n'
+ 'tsp-solver can be downloaded from:\n'
+ ' http://github.com/dmishin/tsp-solver.\n'
+ '=================================================================')
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+
+ # Get file name to open
+ if len(sys.argv) < 2:
+ fname = 'gerbers/shld.drd'
+ else:
+ fname = sys.argv[1]
+
+ # Read the excellon file
+ f = gerber.read(fname)
+
+ positions = {}
+ tools = {}
+ hit_counts = f.hit_count()
+ oldpath = sum(f.path_length().values())
+
+ #Get hit positions
+ for hit in f.hits:
+ tool_num = hit.tool.number
+ if tool_num not in positions.keys():
+ positions[tool_num] = []
+ positions[tool_num].append(hit.position)
+
+ hits = []
+
+ # Optimize tool path for each tool
+ for tool, count in iter(hit_counts.items()):
+
+ # Calculate distance matrix
+ distance_matrix = [[math.hypot(*tuple(map(sub,
+ positions[tool][i],
+ positions[tool][j])))
+ for j in iter(range(count))]
+ for i in iter(range(count))]
+
+ # Calculate new path
+ path = solve_tsp(distance_matrix, 50)
+
+ # Create new hits list
+ hits += [DrillHit(f.tools[tool], positions[tool][p]) for p in path]
+
+ # Update the file
+ f.hits = hits
+ f.filename = f.filename + '.optimized'
+ f.write()
+
+ # Print drill report
+ print(f.report())
+ print('Original path length: %1.4f' % oldpath)
+ print('Optimized path length: %1.4f' % sum(f.path_length().values()))
+
diff --git a/examples/excellon_optimize_after.PNG b/examples/excellon_optimize_after.PNG
new file mode 100644
index 0000000..0f16387
--- /dev/null
+++ b/examples/excellon_optimize_after.PNG
Binary files differ
diff --git a/examples/excellon_optimize_before.PNG b/examples/excellon_optimize_before.PNG
new file mode 100644
index 0000000..26a36ab
--- /dev/null
+++ b/examples/excellon_optimize_before.PNG
Binary files differ
diff --git a/examples/gerbers/bottom_copper.GBL b/examples/gerbers/bottom_copper.GBL
new file mode 100644
index 0000000..0d98da3
--- /dev/null
+++ b/examples/gerbers/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/examples/gerbers/bottom_mask.GBS b/examples/gerbers/bottom_mask.GBS
new file mode 100644
index 0000000..b06654f
--- /dev/null
+++ b/examples/gerbers/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/examples/gerbers/copper.GTL b/examples/gerbers/copper.GTL
new file mode 100644
index 0000000..87e6ded
--- /dev/null
+++ b/examples/gerbers/copper.GTL
@@ -0,0 +1,3457 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%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/examples/gerbers/ncdrill.DRD b/examples/gerbers/ncdrill.DRD
new file mode 100644
index 0000000..ced00ca
--- /dev/null
+++ b/examples/gerbers/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/examples/gerbers/silkscreen.GTO b/examples/gerbers/silkscreen.GTO
new file mode 100644
index 0000000..2ff92f9
--- /dev/null
+++ b/examples/gerbers/silkscreen.GTO
@@ -0,0 +1,2099 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239,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/examples/gerbers/soldermask.GTS b/examples/gerbers/soldermask.GTS
new file mode 100644
index 0000000..0d50141
--- /dev/null
+++ b/examples/gerbers/soldermask.GTS
@@ -0,0 +1,162 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239,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/examples/gerbv_test_files/include-file-1.gbx b/examples/gerbv_test_files/include-file-1.gbx
new file mode 100755
index 0000000..75b5325
--- /dev/null
+++ b/examples/gerbv_test_files/include-file-1.gbx
@@ -0,0 +1,7 @@
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+X0Y-1000D02*
+G54D10*
+X0Y1000D01*
+
diff --git a/examples/gerbv_test_files/test-aperture-circle-1.gbx b/examples/gerbv_test_files/test-aperture-circle-1.gbx
new file mode 100755
index 0000000..ab56aae
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-circle-1.gbx
@@ -0,0 +1,38 @@
+G04 Test drawing with circular apertures*
+G04 Hand coded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10C,0.050*%
+G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
+G04 drawing with it (only should show in flashes)*
+%ADD11C,0.075X0.050*%
+
+G04 No hole, centered at 0,0 *
+G54D10*
+G04 Recenter to 0,0
+G01X0Y0D02*
+G04 Draw a line segment*
+X00100Y0D01*
+G04 Turn off for a segment*
+X00200Y0D02*
+G04 Draw another line at angle*
+G54D11*
+X00300Y00100D01*
+G04 Turn off for a segment*
+X0Y00100D02*
+G54D10*
+G04 Turn on circular interpolation*
+G75*
+G03X0Y00300I0J00100D01*
+
+G04 Turn off for a segment*
+X00500Y00D02*
+G04 Draw a larger radius arc*
+G03X00350Y00150I-00250J-00050D01*
+
+G04 Turn off for a segment*
+X00250Y00200D02*
+G04 Draw a larger clockwise radius arc*
+G02X00350Y00350I00250J-00050D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-aperture-circle-flash-1.gbx b/examples/gerbv_test_files/test-aperture-circle-flash-1.gbx
new file mode 100755
index 0000000..91c4bfc
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-circle-flash-1.gbx
@@ -0,0 +1,27 @@
+G04 Test flashing of circular apertures*
+G04 Four groups of circular apertures are arranged in a square*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10C,0.050*%
+%ADD11C,0.050X0.025*%
+%ADD12C,0.050X0.025X0.030*%
+
+G04 No hole, centered at 0,0 *
+G54D10*
+X0Y0D03*
+
+G04 Round hole, centered at 0.1,0 *
+G54D11*
+X00100Y0D03*
+
+G04 Square hole, centered at 0,0.1 *
+G54D12*
+X0Y00100D03*
+
+G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
+G54D11*
+X00100Y00090D03*
+X00100Y00110D03*
+
+M02*
diff --git a/examples/gerbv_test_files/test-aperture-obround-1.gbx b/examples/gerbv_test_files/test-aperture-obround-1.gbx
new file mode 100755
index 0000000..763232b
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-obround-1.gbx
@@ -0,0 +1,38 @@
+G04 Test drawing with rectangular apertures*
+G04 Hand coded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10O,0.050X0.025*%
+G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
+G04 drawing with it (only should show in flashes)*
+%ADD11O,0.075X0.050X0.025*%
+
+G04 No hole, centered at 0,0 *
+G54D10*
+G04 Recenter to 0,0
+G01X0Y0D02*
+G04 Draw a line segment*
+X00100Y0D01*
+G04 Turn off for a segment*
+X00200Y0D02*
+G04 Draw another line at angle*
+G54D11*
+X00300Y00100D01*
+G04 Turn off for a segment*
+X0Y00100D02*
+G54D10*
+G04 Turn on circular interpolation*
+G75*
+G03X0Y00300I0J00100D01*
+
+G04 Turn off for a segment*
+X00500Y00D02*
+G04 Draw a larger radius arc*
+G03X00350Y00150I-00250J-00050D01*
+
+G04 Turn off for a segment*
+X00250Y00200D02*
+G04 Draw a larger clockwise radius arc*
+G02X00350Y00350I00250J-00050D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-aperture-obround-flash-1.gbx b/examples/gerbv_test_files/test-aperture-obround-flash-1.gbx
new file mode 100755
index 0000000..b09bf43
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-obround-flash-1.gbx
@@ -0,0 +1,27 @@
+G04 Test flashing of obround apertures*
+G04 Four groups of obround apertures are arranged in a square*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10O,0.050X0.080*%
+%ADD11O,0.080X0.050X0.025*%
+%ADD12O,0.050X0.025X0.025X0.0150*%
+
+G04 No hole, centered at 0,0 *
+G54D10*
+X0Y0D03*
+
+G04 Round hole, centered at 0.1,0 *
+G54D11*
+X00100Y0D03*
+
+G04 Square hole, centered at 0,0.1 *
+G54D12*
+X0Y00100D03*
+
+G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
+G54D11*
+X00100Y00090D03*
+X00100Y00110D03*
+
+M02*
diff --git a/examples/gerbv_test_files/test-aperture-polygon-1.gbx b/examples/gerbv_test_files/test-aperture-polygon-1.gbx
new file mode 100755
index 0000000..27f7e8e
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-polygon-1.gbx
@@ -0,0 +1,38 @@
+G04 Test drawing with polygon apertures*
+G04 Hand coded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10O,0.050X0.025*%
+G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
+G04 drawing with it (only should show in flashes)*
+%ADD11O,0.075X0.050X0.025*%
+
+G04 No hole, centered at 0,0 *
+G54D10*
+G04 Recenter to 0,0
+G01X0Y0D02*
+G04 Draw a line segment*
+X00100Y0D01*
+G04 Turn off for a segment*
+X00200Y0D02*
+G04 Draw another line at angle*
+G54D11*
+X00300Y00100D01*
+G04 Turn off for a segment*
+X0Y00100D02*
+G54D10*
+G04 Turn on circular interpolation*
+G75*
+G03X0Y00300I0J00100D01*
+
+G04 Turn off for a segment*
+X00500Y00D02*
+G04 Draw a larger radius arc*
+G03X00350Y00150I-00250J-00050D01*
+
+G04 Turn off for a segment*
+X00250Y00200D02*
+G04 Draw a larger clockwise radius arc*
+G02X00350Y00350I00250J-00050D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-aperture-polygon-flash-1.gbx b/examples/gerbv_test_files/test-aperture-polygon-flash-1.gbx
new file mode 100755
index 0000000..788a755
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-polygon-flash-1.gbx
@@ -0,0 +1,27 @@
+G04 Test flashing of polygon apertures*
+G04 Four groups of polygon apertures are arranged in a square*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10P,0.050X3*%
+%ADD11P,0.050X6X-45X0.035*%
+%ADD12P,0.040X10X25X0.025X0.025X0.0150*%
+
+G04 Triangle, centered at 0,0 *
+G54D10*
+X0Y0D03*
+
+G04 Hexagon with round hole rotate 45 degreed ccwise, centered at 0.1,0 *
+G54D11*
+X00100Y0D03*
+
+G04 10-sided with square hole rotated 25 degrees, centered at 0,0.1 *
+G54D12*
+X0Y00100D03*
+
+G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
+G54D11*
+X00100Y00090D03*
+X00100Y00110D03*
+
+M02*
diff --git a/examples/gerbv_test_files/test-aperture-rectangle-1.gbx b/examples/gerbv_test_files/test-aperture-rectangle-1.gbx
new file mode 100755
index 0000000..77a8c2e
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-rectangle-1.gbx
@@ -0,0 +1,38 @@
+G04 Test drawing with rectangular apertures*
+G04 Hand coded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10R,0.050X0.025*%
+G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
+G04 drawing with it (only should show in flashes)*
+%ADD11R,0.075X0.050X0.025*%
+
+G04 No hole, centered at 0,0 *
+G54D10*
+G04 Recenter to 0,0
+G01X0Y0D02*
+G04 Draw a line segment*
+X00100Y0D01*
+G04 Turn off for a segment*
+X00200Y0D02*
+G04 Draw another line at angle*
+G54D11*
+X00300Y00100D01*
+G04 Turn off for a segment*
+X0Y00100D02*
+G54D10*
+G04 Turn on circular interpolation*
+G75*
+G03X0Y00300I0J00100D01*
+
+G04 Turn off for a segment*
+X00500Y00D02*
+G04 Draw a larger radius arc*
+G03X00350Y00150I-00250J-00050D01*
+
+G04 Turn off for a segment*
+X00250Y00200D02*
+G04 Draw a larger clockwise radius arc*
+G02X00350Y00350I00250J-00050D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx b/examples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx
new file mode 100755
index 0000000..8aeb79c
--- /dev/null
+++ b/examples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx
@@ -0,0 +1,27 @@
+G04 Test flashing of rectangular apertures*
+G04 Four groups of rectangular apertures are arranged in a square*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10R,0.050X0.080*%
+%ADD11R,0.080X0.050X0.025*%
+%ADD12R,0.050X0.025X0.025X0.0150*%
+
+G04 No hole, centered at 0,0 *
+G54D10*
+X0Y0D03*
+
+G04 Round hole, centered at 0.1,0 *
+G54D11*
+X00100Y0D03*
+
+G04 Square hole, centered at 0,0.1 *
+G54D12*
+X0Y00100D03*
+
+G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
+G54D11*
+X00100Y00090D03*
+X00100Y00110D03*
+
+M02*
diff --git a/examples/gerbv_test_files/test-circular-interpolation-1.gbx b/examples/gerbv_test_files/test-circular-interpolation-1.gbx
new file mode 100755
index 0000000..75cffc9
--- /dev/null
+++ b/examples/gerbv_test_files/test-circular-interpolation-1.gbx
@@ -0,0 +1,35 @@
+G04 Test circular interpolation*
+G04 Hand coded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10C,0.050*%
+G54D10*
+
+G04 Recenter to 0,0*
+G01X0Y0D02*
+
+G04 Turn on multi-quadrant mode*
+G75*
+G03X0Y00200I0J00100D01*
+
+G04 Switch to quadrant mode, draw ccwise*
+G74*
+G01X00400Y0D02*
+G03X00470Y00080I0J00100D01*
+
+G04 Draw things clockwise on the top two objects*
+
+G04 Turn on multi-quadrant mode*
+G75*
+G01X00100Y00300D02*
+G02X00100Y00500I0J00100D01*
+
+G04 Switch to quadrant mode, draw clockwise*
+G04 Note: since this is single quadrant mode, I and J must be*
+G04 positive, and the parser should automatically negate the J value*
+G04 to make the curve travel in the clockwise direction*
+G74*
+G01X00400Y00300D02*
+G02X00500Y00300I00150J00300D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-drill-leading-zero-1.exc b/examples/gerbv_test_files/test-drill-leading-zero-1.exc
new file mode 100755
index 0000000..7e53954
--- /dev/null
+++ b/examples/gerbv_test_files/test-drill-leading-zero-1.exc
@@ -0,0 +1,8 @@
+M48
+INCH,LZ
+T13C0.05
+%
+T13
+X-001000Y030000
+X00000Y03000
+X001Y03 M30
diff --git a/examples/gerbv_test_files/test-drill-repeat-1.exc b/examples/gerbv_test_files/test-drill-repeat-1.exc
new file mode 100755
index 0000000..8a87d5a
--- /dev/null
+++ b/examples/gerbv_test_files/test-drill-repeat-1.exc
@@ -0,0 +1,19 @@
+M48
+INCH,TZ
+T01C0.050
+%
+T01
+X0000Y0000
+X10000Y10000
+R5X1000
+X20000Y10000
+R5Y1000
+X30000Y10000
+R5X1000Y1500
+X10000Y00000
+R5X-1000
+X20000Y00000
+R5Y-1000
+X30000Y00000
+R5X-1000Y-1500
+M30
diff --git a/examples/gerbv_test_files/test-drill-trailing-zero-1.exc b/examples/gerbv_test_files/test-drill-trailing-zero-1.exc
new file mode 100755
index 0000000..5218b81
--- /dev/null
+++ b/examples/gerbv_test_files/test-drill-trailing-zero-1.exc
@@ -0,0 +1,8 @@
+M48
+INCH,TZ
+T13C0.05
+%
+T13
+X-001000Y030000
+X0Y030000
+X01000Y30000 M30
diff --git a/examples/gerbv_test_files/test-image-justify-1.gbx b/examples/gerbv_test_files/test-image-justify-1.gbx
new file mode 100755
index 0000000..6e71d62
--- /dev/null
+++ b/examples/gerbv_test_files/test-image-justify-1.gbx
@@ -0,0 +1,19 @@
+G04 Test image justify 1*
+G04 Crosshairs should be justified to the X axis *
+G04 and 0.5 inches offset from Y axis *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%IJB.5*%
+%ADD10C,0.050*%
+
+G04 Crosshairs *
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+
+X0Y-1000D02*
+G54D10*
+X0Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-image-justify-2.gbx b/examples/gerbv_test_files/test-image-justify-2.gbx
new file mode 100755
index 0000000..12fa617
--- /dev/null
+++ b/examples/gerbv_test_files/test-image-justify-2.gbx
@@ -0,0 +1,19 @@
+G04 Test image justify 2*
+G04 Crosshairs should be centered in X and Y (platen size *
+G04 is assumed to be 2x the overall size of the image) *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%IJACBC*%
+%ADD10C,0.050*%
+
+G04 Crosshairs *
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+
+X0Y-1000D02*
+G54D10*
+X0Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-image-offset-1.gbx b/examples/gerbv_test_files/test-image-offset-1.gbx
new file mode 100755
index 0000000..6f79713
--- /dev/null
+++ b/examples/gerbv_test_files/test-image-offset-1.gbx
@@ -0,0 +1,18 @@
+G04 Test image polarity *
+G04 Crosshairs should be centered on 0,0 in final rendering*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%IOA-2.0B-1.0*%
+%ADD10C,0.050*%
+
+G04 Crosshairs to be on 0,0 *
+X1000Y1000D02*
+G54D10*
+X3000Y1000D01*
+
+X2000Y0D02*
+G54D10*
+X2000Y2000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-image-offset-2.gbx b/examples/gerbv_test_files/test-image-offset-2.gbx
new file mode 100755
index 0000000..34e56e1
--- /dev/null
+++ b/examples/gerbv_test_files/test-image-offset-2.gbx
@@ -0,0 +1,19 @@
+G04 Test image offset uses current units *
+G04 Crosshairs should be centered on 0,0 in final rendering*
+G04 Handcoded by Julian Lamb *
+%MOMM*%
+%FSLAX23Y23*%
+%IOB-25.4*%
+%MOIN*%
+%ADD10C,0.050*%
+
+G04 Crosshairs to be on 0,0 *
+X-1000Y1000D02*
+G54D10*
+X1000Y1000D01*
+
+X0Y0D02*
+G54D10*
+X0Y2000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-image-polarity-1.gbx b/examples/gerbv_test_files/test-image-polarity-1.gbx
new file mode 100755
index 0000000..e012966
--- /dev/null
+++ b/examples/gerbv_test_files/test-image-polarity-1.gbx
@@ -0,0 +1,17 @@
+G04 Test image polarity *
+G04 Crosshairs should be cut out of a positive background*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%IPNEG*%
+%ADD10C,0.050*%
+
+G04 Draw crosshairs *
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+X0Y-1000D02*
+G54D10*
+X0Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-image-rotation-1.gbx b/examples/gerbv_test_files/test-image-rotation-1.gbx
new file mode 100755
index 0000000..c136b5b
--- /dev/null
+++ b/examples/gerbv_test_files/test-image-rotation-1.gbx
@@ -0,0 +1,21 @@
+G04 Test image rotation *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%IR270*%
+%ADD10C,0.050*%
+
+G04 Quarter star *
+X1000Y0D02*
+G54D10*
+X2000Y0D01*
+
+X1000Y0D02*
+G54D10*
+X2000Y1000D01*
+
+X1000Y0D02*
+G54D10*
+X1000Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-include-file-1.gbx b/examples/gerbv_test_files/test-include-file-1.gbx
new file mode 100755
index 0000000..9576c79
--- /dev/null
+++ b/examples/gerbv_test_files/test-include-file-1.gbx
@@ -0,0 +1,11 @@
+G04 Test include file 1 *
+G04 Crosshairs should be drawn at 0,0 in final rendering*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%IOA-2.0B-1.0*%
+%ADD10C,0.050*%
+
+G04 Crosshairs to be on 0,0 *
+%IFinclude-file-1.gbx*%
+M02*
diff --git a/examples/gerbv_test_files/test-layer-axis-select-1.gbx b/examples/gerbv_test_files/test-layer-axis-select-1.gbx
new file mode 100755
index 0000000..d5f003e
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-axis-select-1.gbx
@@ -0,0 +1,15 @@
+G04 Test layer axis select *
+G04 Line is drawn along A axis, then axis select switches it and renders *
+G04 line along y axis *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ASAYBX*%
+%ADD10C,0.050*%
+
+G04 Draw line *
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-knockout-1.gbx b/examples/gerbv_test_files/test-layer-knockout-1.gbx
new file mode 100755
index 0000000..1d400e5
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-knockout-1.gbx
@@ -0,0 +1,28 @@
+G04 Test layer knockout 1*
+G04 A cleared 3x3 square should surround the crosshairs *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10C,0.050*%
+
+G04 Create a large dark area *
+G36*
+X-2000Y-2000D02*
+X2000Y-2000D01*
+X2000Y2000D01*
+X-2000Y2000D01*
+X-2000Y-2000D01*
+G37*
+
+G04 Create the knockout region *
+%KOCX-1.5Y-1.5I3J3*%
+
+G04 Draw crosshairs *
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+X0Y-1000D02*
+G54D10*
+X0Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-knockout-2.gbx b/examples/gerbv_test_files/test-layer-knockout-2.gbx
new file mode 100755
index 0000000..2b331c3
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-knockout-2.gbx
@@ -0,0 +1,28 @@
+G04 Test layer knockout 2*
+G04 A cleared 0.5 inch border should surround the crosshairs *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10C,0.050*%
+
+G04 Create a large dark area *
+G36*
+X-2000Y-2000D02*
+X2000Y-2000D01*
+X2000Y2000D01*
+X-2000Y2000D01*
+X-2000Y-2000D01*
+G37*
+
+G04 Create the knockout region *
+%KOCK0.5*%
+
+G04 Draw crosshairs *
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+X0Y-1000D02*
+G54D10*
+X0Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-mirror-image-1.gbx b/examples/gerbv_test_files/test-layer-mirror-image-1.gbx
new file mode 100755
index 0000000..6c76132
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-mirror-image-1.gbx
@@ -0,0 +1,23 @@
+G04 Test layer mirror image 1 *
+G04 Quarter star is drawn pointing towards +X, +Y. Mirror
+G04 flips around the Y axis and the star should point towards -X, -Y *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%MIA1B1*%
+%ADD10C,0.050*%
+
+G04 Draw quarter star *
+X0Y0D02*
+G54D10*
+X1000Y0D01*
+
+X0Y0D02*
+G54D10*
+X1000Y1000D01*
+
+X0Y0D02*
+G54D10*
+X0Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-mode-1.gbx b/examples/gerbv_test_files/test-layer-mode-1.gbx
new file mode 100755
index 0000000..95b8555
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-mode-1.gbx
@@ -0,0 +1,68 @@
+G04 Test handling of unit changes within a RS274X file *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+G04 Aperture 10 should be in Inches *
+%ADD10C,0.050*%
+%MOMM*%
+G04 Aperture 11 should be in MMs *
+%ADD11C,1.250*%
+G04 Aperture 12 should be in MMs *
+%AMTHERMAL*
+7,0,0,25.4,12.7,2.54,0*%
+%MOIN*%
+G04 Aperture 13 is in inches *
+%AMTHERMALTWO*
+7,0,0,1,0.5,0.1,0*%
+%MOMM*%
+%ADD12THERMAL*%
+%MOIN*%
+%ADD13THERMALTWO*%
+
+%MOIN*%
+G04 Box 1, using aperture 10*
+X0Y0D02*
+G54D10*
+X0Y0D01*
+X1000D01*
+Y1000D01*
+X0D01*
+Y0D01*
+
+G04 Box 2, using aperture 11*
+X2000Y0D02*
+G54D11*
+X2000Y0D01*
+X3000D01*
+Y1000D01*
+X2000D01*
+Y0D01*
+
+%MOMM*%
+G04 Box 3, using aperture 10*
+X100000Y0D02*
+G54D10*
+X100000Y0D01*
+X125000D01*
+Y25000D01*
+X100000D01*
+Y0D01*
+
+G04 Draw Thermal in box 1*
+G54D12*
+Y12000X12700D03*
+
+G04 Draw Thermal in box 2*
+G04 ..switch to inches for coordinates*
+G70*
+Y500X2500D02*
+G54D12*
+Y500X2500D03*
+
+G04 ..switch to mms for coordinates*
+G71*
+G04 Draw Thermal in box 3*
+G54D13*
+Y12000X112000D03*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-offset-1.gbx b/examples/gerbv_test_files/test-layer-offset-1.gbx
new file mode 100755
index 0000000..df26e4d
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-offset-1.gbx
@@ -0,0 +1,18 @@
+G04 Test layer offset 1 *
+G04 Crosshairs should be centered on 0,0*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%OFA-2.0B-1.0*%
+%ADD10C,0.050*%
+
+G04 Crosshairs to be on 0,0 *
+X1000Y1000D02*
+G54D10*
+X3000Y1000D01*
+
+X2000Y0D02*
+G54D10*
+X2000Y2000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-rotation-1.gbx b/examples/gerbv_test_files/test-layer-rotation-1.gbx
new file mode 100755
index 0000000..d49e39c
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-rotation-1.gbx
@@ -0,0 +1,23 @@
+G04 Test layer rotation 1 *
+G04 Quarter star should be rotated 45 degrees counterclockwise, pointing*
+G04 the center line straight up *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%RO45*%
+%ADD10C,0.025*%
+
+G04 Quarter star *
+X1000Y0D02*
+G54D10*
+X2000Y0D01*
+
+X1000Y0D02*
+G54D10*
+X2000Y1000D01*
+
+X1000Y0D02*
+G54D10*
+X1000Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-scale-factor-1.gbx b/examples/gerbv_test_files/test-layer-scale-factor-1.gbx
new file mode 100755
index 0000000..02a7b7d
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-scale-factor-1.gbx
@@ -0,0 +1,17 @@
+G04 Test layer scale factor 1 *
+G04 Crosshairs should be centered on 0,0 and 2 inches wide and 1 inch tall*
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%SFA2B1*%
+%ADD10C,0.025*%
+
+G04 Crosshairs to be on 0,0 *
+X-500Y0D02*
+G54D10*
+X500Y0D01*
+X0Y-500D02*
+G54D10*
+X0Y500D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-step-and_repeat-1.gbx b/examples/gerbv_test_files/test-layer-step-and_repeat-1.gbx
new file mode 100755
index 0000000..4f69391
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-step-and_repeat-1.gbx
@@ -0,0 +1,17 @@
+G04 Test step and repeat 1*
+G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%SRX3Y2I5.0J2*%
+%ADD10C,0.050*%
+
+G04 Draw crosshairs *
+X-1000Y0D02*
+G54D10*
+X1000Y0D01*
+X0Y-1000D02*
+G54D10*
+X0Y1000D01*
+
+M02*
diff --git a/examples/gerbv_test_files/test-layer-step-and_repeat-2.gbx b/examples/gerbv_test_files/test-layer-step-and_repeat-2.gbx
new file mode 100755
index 0000000..392036f
--- /dev/null
+++ b/examples/gerbv_test_files/test-layer-step-and_repeat-2.gbx
@@ -0,0 +1,18 @@
+G04 Test step and repeat 1*
+G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y *
+G04 Handcoded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%SRX3Y2I1J1*%
+%ADD10C,0.050*%
+
+G04 Draw a simple square*
+G36*
+G01X00400Y0D02*
+X00600Y0D01*
+X00600Y00200D01*
+X00400Y00200D01*
+X00400Y0D01*
+G37*
+
+M02*
diff --git a/examples/gerbv_test_files/test-polygon-fill-1.gbx b/examples/gerbv_test_files/test-polygon-fill-1.gbx
new file mode 100755
index 0000000..ec25644
--- /dev/null
+++ b/examples/gerbv_test_files/test-polygon-fill-1.gbx
@@ -0,0 +1,45 @@
+G04 Test drawing with polygon apertures*
+G04 Four small polygon fills aranged in a square
+G04 Hand coded by Julian Lamb *
+%MOIN*%
+%FSLAX23Y23*%
+%ADD10C,0.050*%
+
+G04 Draw a rectangle with a rounded right side*
+G36*
+G01X0Y0D02*
+X00200Y0D01*
+G75*
+G03X00200Y00200I0J00100D01*
+X0Y00200D01*
+G04 Do not close with a final line, so let gerbv automatically close*
+G37*
+
+G04 Draw a simple square*
+G36*
+G01X00400Y0D02*
+X00600Y0D01*
+X00600Y00200D01*
+X00400Y00200D01*
+X00400Y0D01*
+G37*
+
+G04 Draw a small diamond*
+G36*
+G01X00100Y00300D02*
+X00200Y00400D01*
+X00100Y00500D01*
+X0Y00400D01*
+X00100Y00300D01*
+G37*
+
+G04 Draw a very-narrow slit*
+G36*
+G01X00500Y00300D02*
+X00510Y00300D01*
+X00510Y00500D01*
+X00500Y00500D01*
+X00500Y00300D01*
+G37*
+
+M02*
diff --git a/examples/pcb_bottom.png b/examples/pcb_bottom.png
new file mode 100644
index 0000000..e7fc616
--- /dev/null
+++ b/examples/pcb_bottom.png
Binary files differ
diff --git a/examples/pcb_example.py b/examples/pcb_example.py
new file mode 100644
index 0000000..34afee6
--- /dev/null
+++ b/examples/pcb_example.py
@@ -0,0 +1,53 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# 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 example demonstrates the use of pcb-tools with cairo to render composite
+images using the PCB interface
+"""
+
+import os
+from gerber import PCB
+from gerber.render import theme
+from gerber.render.cairo_backend import GerberCairoContext
+
+
+GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
+
+
+# Create a new drawing context
+ctx = GerberCairoContext()
+
+# Create a new PCB instance
+pcb = PCB.from_directory(GERBER_FOLDER)
+
+# Render PCB top view
+ctx.render_layers(pcb.top_layers,
+ os.path.join(os.path.dirname(__file__), 'pcb_top.png',),
+ theme.THEMES['OSH Park'], max_width=800, max_height=600)
+
+# Render PCB bottom view
+ctx.render_layers(pcb.bottom_layers,
+ os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'),
+ theme.THEMES['OSH Park'], max_width=800, max_height=600)
+
+# Render copper layers only
+ctx.render_layers(pcb.copper_layers + pcb.drill_layers,
+ os.path.join(os.path.dirname(__file__),
+ 'pcb_transparent_copper.png'),
+ theme.THEMES['Transparent Copper'], max_width=800, max_height=600)
+
diff --git a/examples/pcb_top.png b/examples/pcb_top.png
new file mode 100644
index 0000000..ea0cc5b
--- /dev/null
+++ b/examples/pcb_top.png
Binary files differ
diff --git a/examples/pcb_transparent_copper.png b/examples/pcb_transparent_copper.png
new file mode 100644
index 0000000..0c8642f
--- /dev/null
+++ b/examples/pcb_transparent_copper.png
Binary files differ
diff --git a/examples/render_gerbv_tests.py b/examples/render_gerbv_tests.py
new file mode 100755
index 0000000..d29ec11
--- /dev/null
+++ b/examples/render_gerbv_tests.py
@@ -0,0 +1,59 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""
+This example renders the gerber files from the gerbv test suite
+"""
+
+import os
+from gerber.rs274x import read as gerber_read
+from gerber.excellon import read as excellon_read
+from gerber.render import GerberCairoContext
+from gerber.utils import listdir
+
+GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbv_test_files'))
+
+if not os.path.isdir(os.path.join(os.path.dirname(__file__), 'outputs')):
+ os.mkdir(os.path.join(os.path.dirname(__file__), 'outputs'))
+
+for infile in listdir(GERBER_FOLDER):
+ if infile.startswith('test'):
+ try:
+ outfile = os.path.splitext(infile)[0] + '.png'
+ if infile.endswith('gbx'):
+ layer = gerber_read(os.path.join(GERBER_FOLDER, infile))
+ print("Loaded Gerber file: {}".format(infile))
+ elif infile.endswith('exc'):
+ layer = excellon_read(os.path.join(GERBER_FOLDER, infile))
+ print("Loaded Excellon file: {}".format(infile))
+ else:
+ continue
+
+ # Create a new drawing context
+ ctx = GerberCairoContext(1200)
+ ctx.color = (80./255, 80/255., 154/255.)
+ ctx.drill_color = ctx.color
+
+ # Draw the layer, and specify the rendering settings to use
+ layer.render(ctx)
+
+ # Write output to png file
+ print("Writing output to: {}".format(outfile))
+ ctx.dump(os.path.join(os.path.dirname(__file__), 'outputs', outfile))
+ except Exception as exc:
+ import traceback
+ traceback.print_exc()
diff --git a/gerber/__init__.py b/gerber/__init__.py
new file mode 100644
index 0000000..1faba53
--- /dev/null
+++ b/gerber/__init__.py
@@ -0,0 +1,28 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Gerber Tools
+============
+**Gerber Tools**
+
+gerber-tools 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/gerber/__main__.py b/gerber/__main__.py
new file mode 100644
index 0000000..988adff
--- /dev/null
+++ b/gerber/__main__.py
@@ -0,0 +1,122 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+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/gerber/am_eval.py b/gerber/am_eval.py
new file mode 100644
index 0000000..3a7e1ed
--- /dev/null
+++ b/gerber/am_eval.py
@@ -0,0 +1,109 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+""" This module provides RS-274-X AM macro evaluation.
+"""
+
+
+class OpCode:
+ PUSH = 1
+ LOAD = 2
+ STORE = 3
+ ADD = 4
+ SUB = 5
+ MUL = 6
+ DIV = 7
+ PRIM = 8
+
+ @staticmethod
+ def str(opcode):
+ if opcode == OpCode.PUSH:
+ return "OPCODE_PUSH"
+ elif opcode == OpCode.LOAD:
+ return "OPCODE_LOAD"
+ elif opcode == OpCode.STORE:
+ return "OPCODE_STORE"
+ elif opcode == OpCode.ADD:
+ return "OPCODE_ADD"
+ elif opcode == OpCode.SUB:
+ return "OPCODE_SUB"
+ elif opcode == OpCode.MUL:
+ return "OPCODE_MUL"
+ elif opcode == OpCode.DIV:
+ return "OPCODE_DIV"
+ elif opcode == OpCode.PRIM:
+ return "OPCODE_PRIM"
+ else:
+ return "UNKNOWN"
+
+
+def eval_macro(instructions, parameters={}):
+
+ if not isinstance(parameters, type({})):
+ p = {}
+ for i, val in enumerate(parameters):
+ p[i + 1] = val
+
+ parameters = p
+
+ stack = []
+
+ def pop():
+ return stack.pop()
+
+ def push(op):
+ stack.append(op)
+
+ def top():
+ return stack[-1]
+
+ def empty():
+ return len(stack) == 0
+
+ for opcode, argument in instructions:
+ if opcode == OpCode.PUSH:
+ push(argument)
+
+ elif opcode == OpCode.LOAD:
+ push(parameters.get(argument, 0))
+
+ elif opcode == OpCode.STORE:
+ parameters[argument] = pop()
+
+ elif opcode == OpCode.ADD:
+ op1 = pop()
+ op2 = pop()
+ push(op2 + op1)
+
+ elif opcode == OpCode.SUB:
+ op1 = pop()
+ op2 = pop()
+ push(op2 - op2)
+
+ elif opcode == OpCode.MUL:
+ op1 = pop()
+ op2 = pop()
+ push(op2 * op1)
+
+ elif opcode == OpCode.DIV:
+ op1 = pop()
+ op2 = pop()
+ push(op2 / op1)
+
+ elif opcode == OpCode.PRIM:
+ yield "%d,%s" % (argument, ",".join([str(x) for x in stack]))
+ stack = []
diff --git a/gerber/am_read.py b/gerber/am_read.py
new file mode 100644
index 0000000..4aff00b
--- /dev/null
+++ b/gerber/am_read.py
@@ -0,0 +1,255 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+""" This module provides RS-274-X AM macro modifiers parsing.
+"""
+
+from .am_eval import OpCode, eval_macro
+
+import string
+
+
+class Token:
+ ADD = "+"
+ SUB = "-"
+ # compatibility as many gerber writes do use non compliant X
+ MULT = ("x", "X")
+ DIV = "/"
+ OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV)
+ LEFT_PARENS = "("
+ RIGHT_PARENS = ")"
+ EQUALS = "="
+ EOF = "EOF"
+
+
+def token_to_opcode(token):
+ if token == Token.ADD:
+ return OpCode.ADD
+ elif token == Token.SUB:
+ return OpCode.SUB
+ elif token in Token.MULT:
+ return OpCode.MUL
+ elif token == Token.DIV:
+ return OpCode.DIV
+ else:
+ return None
+
+
+def precedence(token):
+ if token == Token.ADD or token == Token.SUB:
+ return 1
+ elif token in Token.MULT or token == Token.DIV:
+ return 2
+ else:
+ return 0
+
+
+def is_op(token):
+ return token in Token.OPERATORS
+
+
+class Scanner:
+
+ def __init__(self, s):
+ self.buff = s
+ self.n = 0
+
+ def eof(self):
+ return self.n == len(self.buff)
+
+ def peek(self):
+ if not self.eof():
+ return self.buff[self.n]
+
+ return Token.EOF
+
+ def ungetc(self):
+ if self.n > 0:
+ self.n -= 1
+
+ def getc(self):
+ if self.eof():
+ return ""
+
+ c = self.buff[self.n]
+ self.n += 1
+ return c
+
+ def readint(self):
+ n = ""
+ while not self.eof() and (self.peek() in string.digits):
+ n += self.getc()
+ return int(n)
+
+ def readfloat(self):
+ n = ""
+ while not self.eof() and (self.peek() in string.digits or self.peek() == "."):
+ n += self.getc()
+ # weird case where zero is ommited inthe last modifider, like in ',0.'
+ if n == ".":
+ return 0
+ return float(n)
+
+ def readstr(self, end="*"):
+ s = ""
+ while not self.eof() and self.peek() != end:
+ s += self.getc()
+ return s.strip()
+
+
+def print_instructions(instructions):
+ for opcode, argument in instructions:
+ print("%s %s" % (OpCode.str(opcode),
+ str(argument) if argument is not None else ""))
+
+
+def read_macro(macro):
+ instructions = []
+
+ for block in macro.split("*"):
+
+ is_primitive = False
+ is_equation = False
+
+ found_equation_left_side = False
+ found_primitive_code = False
+
+ equation_left_side = 0
+ primitive_code = 0
+
+ unary_minus_allowed = False
+ unary_minus = False
+
+ if Token.EQUALS in block:
+ is_equation = True
+ else:
+ is_primitive = True
+
+ scanner = Scanner(block)
+
+ # inlined here for compactness and convenience
+ op_stack = []
+
+ def pop():
+ return op_stack.pop()
+
+ def push(op):
+ op_stack.append(op)
+
+ def top():
+ return op_stack[-1]
+
+ def empty():
+ return len(op_stack) == 0
+
+ while not scanner.eof():
+
+ c = scanner.getc()
+
+ if c == ",":
+ found_primitive_code = True
+
+ # add all instructions on the stack to finish last modifier
+ while not empty():
+ instructions.append((token_to_opcode(pop()), None))
+
+ unary_minus_allowed = True
+
+ elif c in Token.OPERATORS:
+ if c == Token.SUB and unary_minus_allowed:
+ unary_minus = True
+ unary_minus_allowed = False
+ continue
+
+ while not empty() and is_op(top()) and precedence(top()) >= precedence(c):
+ instructions.append((token_to_opcode(pop()), None))
+
+ push(c)
+
+ elif c == Token.LEFT_PARENS:
+ push(c)
+
+ elif c == Token.RIGHT_PARENS:
+ while not empty() and top() != Token.LEFT_PARENS:
+ instructions.append((token_to_opcode(pop()), None))
+
+ if empty():
+ raise ValueError("unbalanced parentheses")
+
+ # discard "("
+ pop()
+
+ elif c.startswith("$"):
+ n = scanner.readint()
+
+ if is_equation and not found_equation_left_side:
+ equation_left_side = n
+ else:
+ instructions.append((OpCode.LOAD, n))
+
+ elif c == Token.EQUALS:
+ found_equation_left_side = True
+
+ elif c == "0":
+ if is_primitive and not found_primitive_code:
+ instructions.append((OpCode.PUSH, scanner.readstr("*")))
+ found_primitive_code = True
+ else:
+ # decimal or integer disambiguation
+ if scanner.peek() not in '.' or scanner.peek() == Token.EOF:
+ instructions.append((OpCode.PUSH, 0))
+
+ elif c in "123456789.":
+ scanner.ungetc()
+
+ if is_primitive and not found_primitive_code:
+ primitive_code = scanner.readint()
+ else:
+ n = scanner.readfloat()
+ if unary_minus:
+ unary_minus = False
+ n *= -1
+
+ instructions.append((OpCode.PUSH, n))
+ else:
+ # whitespace or unknown char
+ pass
+
+ # add all instructions on the stack to finish last modifier (if any)
+ while not empty():
+ instructions.append((token_to_opcode(pop()), None))
+
+ # at end, we either have a primitive or a equation
+ if is_primitive and found_primitive_code:
+ instructions.append((OpCode.PRIM, primitive_code))
+
+ if is_equation:
+ instructions.append((OpCode.STORE, equation_left_side))
+
+ return instructions
+
+if __name__ == '__main__':
+ import sys
+
+ instructions = read_macro(sys.argv[1])
+
+ print("insructions:")
+ print_instructions(instructions)
+
+ print("eval:")
+ for primitive in eval_macro(instructions):
+ print(primitive)
diff --git a/gerber/am_statements.py b/gerber/am_statements.py
new file mode 100644
index 0000000..31c0ae4
--- /dev/null
+++ b/gerber/am_statements.py
@@ -0,0 +1,1046 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be> and Paulo Henrique Silva
+# <ph.silva@gmail.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from math import asin
+import math
+
+from .primitives import *
+from .utils import validate_coordinates, inch, metric, rotate_point
+
+
+
+# TODO: Add support for aperture macro variables
+__all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive',
+ 'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive',
+ 'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive',
+ 'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive']
+
+
+class AMPrimitive(object):
+ """ Aperture Macro Primitive Base Class
+
+ Parameters
+ ----------
+ code : int
+ primitive shape code
+
+ exposure : str
+ on or off Primitives with exposure on create a slid part of
+ the macro aperture, and primitives with exposure off erase the
+ solid part created previously in the aperture macro definition.
+ .. note::
+ The erasing effect is limited to the aperture definition in
+ which it occurs.
+
+ Returns
+ -------
+ primitive : :class: `gerber.am_statements.AMPrimitive`
+
+ Raises
+ ------
+ TypeError, ValueError
+ """
+
+ def __init__(self, code, exposure=None):
+ VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22, 9999)
+ if not isinstance(code, int):
+ raise TypeError('Aperture Macro Primitive code must be an integer')
+ elif code not in VALID_CODES:
+ raise ValueError('Invalid Code. Valid codes are %s.' %
+ ', '.join(map(str, VALID_CODES)))
+ if exposure is not None and exposure.lower() not in ('on', 'off'):
+ raise ValueError('Exposure must be either on or off')
+ self.code = code
+ self.exposure = exposure.lower() if exposure is not None else None
+
+ def to_inch(self):
+ raise NotImplementedError('Subclass must implement `to-inch`')
+
+ def to_metric(self):
+ raise NotImplementedError('Subclass must implement `to-metric`')
+
+ @property
+ def _level_polarity(self):
+ if self.exposure == 'off':
+ return 'clear'
+ return 'dark'
+
+ def to_primitive(self, units):
+ """ Return a Primitive instance based on the specified macro params.
+ """
+ print('Rendering {}s is not supported yet.'.format(str(self.__class__)))
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+
+class AMCommentPrimitive(AMPrimitive):
+ """ Aperture Macro Comment primitive. Code 0
+
+ The comment primitive has no image meaning. It is used to include human-
+ readable comments into the AM command.
+
+ .. seealso::
+ `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.1:** Comment, primitive code 0
+
+ Parameters
+ ----------
+ code : int
+ Aperture Macro primitive code. 0 Indicates an AMCommentPrimitive
+
+ comment : str
+ The comment as a string.
+
+ Returns
+ -------
+ CommentPrimitive : :class:`gerber.am_statements.AMCommentPrimitive`
+ An Initialized AMCommentPrimitive
+
+ Raises
+ ------
+ ValueError
+ """
+ @classmethod
+ def from_gerber(cls, primitive):
+ primitive = primitive.strip()
+ code = int(primitive[0])
+ comment = primitive[1:]
+ return cls(code, comment)
+
+ def __init__(self, code, comment):
+ if code != 0:
+ raise ValueError('Not a valid Aperture Macro Comment statement')
+ super(AMCommentPrimitive, self).__init__(code)
+ self.comment = comment.strip(' *')
+
+ def to_inch(self):
+ pass
+
+ def to_metric(self):
+ pass
+
+ def to_gerber(self, settings=None):
+ return '0 %s *' % self.comment
+
+ def to_primitive(self, units):
+ """
+ Returns None - has not primitive representation
+ """
+ return None
+
+ def __str__(self):
+ return '<Aperture Macro Comment: %s>' % self.comment
+
+
+class AMCirclePrimitive(AMPrimitive):
+ """ Aperture macro Circle primitive. Code 1
+
+ A circle primitive is defined by its center point and diameter.
+
+ .. seealso::
+ `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.2:** Circle, primitive code 1
+
+ Parameters
+ ----------
+ code : int
+ Circle Primitive code. Must be 1
+
+ exposure : string
+ 'on' or 'off'
+
+ diameter : float
+ Circle diameter
+
+ position : tuple (<float>, <float>)
+ Position of the circle relative to the macro origin
+
+ Returns
+ -------
+ CirclePrimitive : :class:`gerber.am_statements.AMCirclePrimitive`
+ An initialized AMCirclePrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(',')
+ code = int(modifiers[0])
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
+ diameter = float(modifiers[2])
+ position = (float(modifiers[3]), float(modifiers[4]))
+ return cls(code, exposure, diameter, position)
+
+ @classmethod
+ def from_primitive(cls, primitive):
+ return cls(1, 'on', primitive.diameter, primitive.position)
+
+ def __init__(self, code, exposure, diameter, position):
+ validate_coordinates(position)
+ if code != 1:
+ raise ValueError('CirclePrimitive code is 1')
+ super(AMCirclePrimitive, self).__init__(code, exposure)
+ self.diameter = diameter
+ self.position = position
+
+ def to_inch(self):
+ self.diameter = inch(self.diameter)
+ self.position = tuple([inch(x) for x in self.position])
+
+ def to_metric(self):
+ self.diameter = metric(self.diameter)
+ self.position = tuple([metric(x) for x in self.position])
+
+ def to_gerber(self, settings=None):
+ data = dict(code=self.code,
+ exposure='1' if self.exposure == 'on' else 0,
+ diameter=self.diameter,
+ x=self.position[0],
+ y=self.position[1])
+ return '{code},{exposure},{diameter},{x},{y}*'.format(**data)
+
+ def to_primitive(self, units):
+ return Circle((self.position), self.diameter, units=units, level_polarity=self._level_polarity)
+
+
+class AMVectorLinePrimitive(AMPrimitive):
+ """ Aperture Macro Vector Line primitive. Code 2 or 20.
+
+ A vector line is a rectangle defined by its line width, start, and end
+ points. The line ends are rectangular.
+
+ .. seealso::
+ `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.3:** Vector Line, primitive code 2 or 20.
+
+ Parameters
+ ----------
+ code : int
+ Vector Line Primitive code. Must be either 2 or 20.
+
+ exposure : string
+ 'on' or 'off'
+
+ width : float
+ Line width
+
+ start : tuple (<float>, <float>)
+ coordinate of line start point
+
+ end : tuple (<float>, <float>)
+ coordinate of line end point
+
+ rotation : float
+ Line rotation about the origin.
+
+ Returns
+ -------
+ LinePrimitive : :class:`gerber.am_statements.AMVectorLinePrimitive`
+ An initialized AMVectorLinePrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+
+ @classmethod
+ def from_primitive(cls, primitive):
+ return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0)
+
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(',')
+ code = int(modifiers[0])
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
+ width = float(modifiers[2])
+ start = (float(modifiers[3]), float(modifiers[4]))
+ end = (float(modifiers[5]), float(modifiers[6]))
+ rotation = float(modifiers[7])
+ return cls(code, exposure, width, start, end, rotation)
+
+ def __init__(self, code, exposure, width, start, end, rotation):
+ validate_coordinates(start)
+ validate_coordinates(end)
+ if code not in (2, 20):
+ raise ValueError('VectorLinePrimitive codes are 2 or 20')
+ super(AMVectorLinePrimitive, self).__init__(code, exposure)
+ self.width = width
+ self.start = start
+ self.end = end
+ self.rotation = rotation
+
+ def to_inch(self):
+ self.width = inch(self.width)
+ self.start = tuple([inch(x) for x in self.start])
+ self.end = tuple([inch(x) for x in self.end])
+
+ def to_metric(self):
+ self.width = metric(self.width)
+ self.start = tuple([metric(x) for x in self.start])
+ self.end = tuple([metric(x) for x in self.end])
+
+ def to_gerber(self, settings=None):
+ fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*'
+ data = dict(code=self.code,
+ exp=1 if self.exposure == 'on' else 0,
+ width=self.width,
+ startx=self.start[0],
+ starty=self.start[1],
+ endx=self.end[0],
+ endy=self.end[1],
+ rotation=self.rotation)
+ return fmtstr.format(**data)
+
+ def to_primitive(self, units):
+ """
+ Convert this to a primitive. We use the Outline to represent this (instead of Line)
+ because the behaviour of the end caps is different for aperture macros compared to Lines
+ when rotated.
+ """
+
+ # Use a line to generate our vertices easily
+ line = Line(self.start, self.end, Rectangle(None, self.width, self.width))
+ vertices = line.vertices
+
+ aperture = Circle((0, 0), 0)
+
+ lines = []
+ prev_point = rotate_point(vertices[-1], self.rotation, (0, 0))
+ for point in vertices:
+ cur_point = rotate_point(point, self.rotation, (0, 0))
+
+ lines.append(Line(prev_point, cur_point, aperture))
+
+ return Outline(lines, units=units, level_polarity=self._level_polarity)
+
+
+class AMOutlinePrimitive(AMPrimitive):
+ """ Aperture Macro Outline primitive. Code 4.
+
+ An outline primitive is an area enclosed by an n-point polygon defined by
+ its start point and n subsequent points. The outline must be closed, i.e.
+ the last point must be equal to the start point. Self intersecting
+ outlines are not allowed.
+
+ .. seealso::
+ `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.6:** Outline, primitive code 4.
+
+ Parameters
+ ----------
+ code : int
+ OutlinePrimitive code. Must be 6.
+
+ exposure : string
+ 'on' or 'off'
+
+ start_point : tuple (<float>, <float>)
+ coordinate of outline start point
+
+ points : list of tuples (<float>, <float>)
+ coordinates of subsequent points
+
+ rotation : float
+ outline rotation about the origin.
+
+ Returns
+ -------
+ OutlinePrimitive : :class:`gerber.am_statements.AMOutlineinePrimitive`
+ An initialized AMOutlinePrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+
+ @classmethod
+ def from_primitive(cls, primitive):
+
+ start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6))
+ points = []
+ for prim in primitive.primitives:
+ points.append((round(prim.end[0], 6), round(prim.end[1], 6)))
+
+ rotation = 0.0
+
+ return cls(4, 'on', start_point, points, rotation)
+
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(",")
+
+ code = int(modifiers[0])
+ exposure = "on" if float(modifiers[1]) == 1 else "off"
+ n = int(float(modifiers[2]))
+ start_point = (float(modifiers[3]), float(modifiers[4]))
+ points = []
+ for i in range(n):
+ points.append((float(modifiers[5 + i * 2]),
+ float(modifiers[5 + i * 2 + 1])))
+ rotation = float(modifiers[-1])
+ return cls(code, exposure, start_point, points, rotation)
+
+ def __init__(self, code, exposure, start_point, points, rotation):
+ """ Initialize AMOutlinePrimitive
+ """
+ validate_coordinates(start_point)
+ for point in points:
+ validate_coordinates(point)
+ if code != 4:
+ raise ValueError('OutlinePrimitive code is 4')
+ super(AMOutlinePrimitive, self).__init__(code, exposure)
+ self.start_point = start_point
+ if points[-1] != start_point:
+ raise ValueError('OutlinePrimitive must be closed')
+ self.points = points
+ self.rotation = rotation
+
+ def to_inch(self):
+ self.start_point = tuple([inch(x) for x in self.start_point])
+ self.points = tuple([(inch(x), inch(y)) for x, y in self.points])
+
+ def to_metric(self):
+ self.start_point = tuple([metric(x) for x in self.start_point])
+ self.points = tuple([(metric(x), metric(y)) for x, y in self.points])
+
+ def to_gerber(self, settings=None):
+ data = dict(
+ code=self.code,
+ exposure="1" if self.exposure == "on" else "0",
+ n_points=len(self.points),
+ start_point="%.6g,%.6g" % self.start_point,
+ points=",\n".join(["%.6g,%.6g" % point for point in self.points]),
+ rotation=str(self.rotation)
+ )
+ 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 <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.8:** Polygon, primitive code 5.
+
+ Parameters
+ ----------
+ code : int
+ PolygonPrimitive code. Must be 5.
+
+ exposure : string
+ 'on' or 'off'
+
+ vertices : int, 3 <= vertices <= 12
+ Number of vertices
+
+ position : tuple (<float>, <float>)
+ X and Y coordinates of polygon center
+
+ diameter : float
+ diameter of circumscribed circle.
+
+ rotation : float
+ polygon rotation about the origin.
+
+ Returns
+ -------
+ PolygonPrimitive : :class:`gerber.am_statements.AMPolygonPrimitive`
+ An initialized AMPolygonPrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+
+ @classmethod
+ def from_primitive(cls, primitive):
+ return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation)
+
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(",")
+ code = int(modifiers[0])
+ exposure = "on" if float(modifiers[1]) == 1 else "off"
+ vertices = int(float(modifiers[2]))
+ position = (float(modifiers[3]), float(modifiers[4]))
+ try:
+ diameter = float(modifiers[5])
+ except:
+ diameter = 0
+
+ rotation = float(modifiers[6])
+ return cls(code, exposure, vertices, position, diameter, rotation)
+
+ def __init__(self, code, exposure, vertices, position, diameter, rotation):
+ """ Initialize AMPolygonPrimitive
+ """
+ if code != 5:
+ raise ValueError('PolygonPrimitive code is 5')
+ super(AMPolygonPrimitive, self).__init__(code, exposure)
+ if vertices < 3 or vertices > 12:
+ raise ValueError('Number of vertices must be between 3 and 12')
+ self.vertices = vertices
+ validate_coordinates(position)
+ self.position = position
+ self.diameter = diameter
+ self.rotation = rotation
+
+ def to_inch(self):
+ self.position = tuple([inch(x) for x in self.position])
+ self.diameter = inch(self.diameter)
+
+ def to_metric(self):
+ self.position = tuple([metric(x) for x in self.position])
+ self.diameter = metric(self.diameter)
+
+ def to_gerber(self, settings=None):
+ data = dict(
+ code=self.code,
+ exposure="1" if self.exposure == "on" else "0",
+ vertices=self.vertices,
+ position="%.4g,%.4g" % self.position,
+ diameter='%.4g' % self.diameter,
+ rotation=str(self.rotation)
+ )
+ fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*"
+ return fmt.format(**data)
+
+ def to_primitive(self, units):
+ return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity)
+
+
+class AMMoirePrimitive(AMPrimitive):
+ """ Aperture Macro Moire primitive. Code 6.
+
+ The moire primitive is a cross hair centered on concentric rings (annuli).
+ Exposure is always on.
+
+ .. seealso::
+ `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.9:** Moire, primitive code 6.
+
+ Parameters
+ ----------
+ code : int
+ Moire Primitive code. Must be 6.
+
+ position : tuple (<float>, <float>)
+ X and Y coordinates of moire center
+
+ diameter : float
+ outer diameter of outer ring.
+
+ ring_thickness : float
+ thickness of concentric rings.
+
+ gap : float
+ gap between concentric rings.
+
+ max_rings : float
+ maximum number of rings
+
+ crosshair_thickness : float
+ thickness of crosshairs
+
+ crosshair_length : float
+ length of crosshairs
+
+ rotation : float
+ moire rotation about the origin.
+
+ Returns
+ -------
+ MoirePrimitive : :class:`gerber.am_statements.AMMoirePrimitive`
+ An initialized AMMoirePrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(",")
+ code = int(modifiers[0])
+ position = (float(modifiers[1]), float(modifiers[2]))
+ diameter = float(modifiers[3])
+ ring_thickness = float(modifiers[4])
+ gap = float(modifiers[5])
+ max_rings = int(float(modifiers[6]))
+ crosshair_thickness = float(modifiers[7])
+ crosshair_length = float(modifiers[8])
+ rotation = float(modifiers[9])
+ return cls(code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation)
+
+ def __init__(self, code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation):
+ """ Initialize AMoirePrimitive
+ """
+ if code != 6:
+ raise ValueError('MoirePrimitive code is 6')
+ super(AMMoirePrimitive, self).__init__(code, 'on')
+ validate_coordinates(position)
+ self.position = position
+ self.diameter = diameter
+ self.ring_thickness = ring_thickness
+ self.gap = gap
+ self.max_rings = max_rings
+ self.crosshair_thickness = crosshair_thickness
+ self.crosshair_length = crosshair_length
+ self.rotation = rotation
+
+ def to_inch(self):
+ self.position = tuple([inch(x) for x in self.position])
+ self.diameter = inch(self.diameter)
+ self.ring_thickness = inch(self.ring_thickness)
+ self.gap = inch(self.gap)
+ self.crosshair_thickness = inch(self.crosshair_thickness)
+ self.crosshair_length = inch(self.crosshair_length)
+
+ def to_metric(self):
+ self.position = tuple([metric(x) for x in self.position])
+ self.diameter = metric(self.diameter)
+ self.ring_thickness = metric(self.ring_thickness)
+ self.gap = metric(self.gap)
+ self.crosshair_thickness = metric(self.crosshair_thickness)
+ self.crosshair_length = metric(self.crosshair_length)
+
+
+ def to_gerber(self, settings=None):
+ data = dict(
+ code=self.code,
+ position="%.4g,%.4g" % self.position,
+ diameter=self.diameter,
+ ring_thickness=self.ring_thickness,
+ gap=self.gap,
+ max_rings=self.max_rings,
+ crosshair_thickness=self.crosshair_thickness,
+ crosshair_length=self.crosshair_length,
+ rotation=self.rotation
+ )
+ fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*"
+ return fmt.format(**data)
+
+ def to_primitive(self, units):
+ #raise NotImplementedError()
+ 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 <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.10:** Thermal, primitive code 7.
+
+ Parameters
+ ----------
+ code : int
+ Thermal Primitive code. Must be 7.
+
+ position : tuple (<float>, <float>)
+ X and Y coordinates of thermal center
+
+ outer_diameter : float
+ outer diameter of thermal.
+
+ inner_diameter : float
+ inner diameter of thermal.
+
+ gap : float
+ gap thickness
+
+ rotation : float
+ thermal rotation about the origin.
+
+ Returns
+ -------
+ ThermalPrimitive : :class:`gerber.am_statements.AMThermalPrimitive`
+ An initialized AMThermalPrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(",")
+ code = int(modifiers[0])
+ position = (float(modifiers[1]), float(modifiers[2]))
+ outer_diameter = float(modifiers[3])
+ inner_diameter = float(modifiers[4])
+ gap = float(modifiers[5])
+ rotation = float(modifiers[6])
+ return cls(code, position, outer_diameter, inner_diameter, gap, rotation)
+
+ def __init__(self, code, position, outer_diameter, inner_diameter, gap, rotation):
+ if code != 7:
+ raise ValueError('ThermalPrimitive code is 7')
+ super(AMThermalPrimitive, self).__init__(code, 'on')
+ validate_coordinates(position)
+ self.position = position
+ self.outer_diameter = outer_diameter
+ self.inner_diameter = inner_diameter
+ self.gap = gap
+ self.rotation = rotation
+
+ def to_inch(self):
+ self.position = tuple([inch(x) for x in self.position])
+ self.outer_diameter = inch(self.outer_diameter)
+ self.inner_diameter = inch(self.inner_diameter)
+ self.gap = inch(self.gap)
+
+ def to_metric(self):
+ self.position = tuple([metric(x) for x in self.position])
+ self.outer_diameter = metric(self.outer_diameter)
+ self.inner_diameter = metric(self.inner_diameter)
+ self.gap = metric(self.gap)
+
+ def to_gerber(self, settings=None):
+ data = dict(
+ code=self.code,
+ position="%.4g,%.4g" % self.position,
+ outer_diameter=self.outer_diameter,
+ inner_diameter=self.inner_diameter,
+ gap=self.gap,
+ rotation=self.rotation
+ )
+ fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap},{rotation}*"
+ return fmt.format(**data)
+
+ def _approximate_arc_cw(self, start_angle, end_angle, radius, center):
+ """
+ Get an arc as a series of points
+
+ Parameters
+ ----------
+ start_angle : The start angle in radians
+ end_angle : The end angle in radians
+ radius`: Radius of the arc
+ center : The center point of the arc (x, y) tuple
+
+ Returns
+ -------
+ array of point tuples
+ """
+
+ # The total sweep
+ sweep_angle = end_angle - start_angle
+ num_steps = 10
+
+ angle_step = sweep_angle / num_steps
+
+ radius = radius
+ center = center
+
+ points = []
+
+ for i in range(num_steps + 1):
+ current_angle = start_angle + (angle_step * i)
+
+ nextx = (center[0] + math.cos(current_angle) * radius)
+ nexty = (center[1] + math.sin(current_angle) * radius)
+
+ points.append((nextx, nexty))
+
+ return points
+
+ def to_primitive(self, units):
+
+ # We start with calculating the top right section, then duplicate it
+
+ inner_radius = self.inner_diameter / 2.0
+ outer_radius = self.outer_diameter / 2.0
+
+ # Calculate the start angle relative to the horizontal axis
+ inner_offset_angle = asin(self.gap / 2.0 / inner_radius)
+ outer_offset_angle = asin(self.gap / 2.0 / outer_radius)
+
+ rotation_rad = math.radians(self.rotation)
+ inner_start_angle = inner_offset_angle + rotation_rad
+ inner_end_angle = math.pi / 2 - inner_offset_angle + rotation_rad
+
+ outer_start_angle = outer_offset_angle + rotation_rad
+ outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad
+
+ outlines = []
+ aperture = Circle((0, 0), 0)
+
+ points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position)
+ + list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position))))
+ # Add in the last point since outlines should be closed
+ points.append(points[0])
+
+ # There are four outlines at rotated sections
+ for rotation in [0, 90.0, 180.0, 270.0]:
+
+ lines = []
+ prev_point = rotate_point(points[0], rotation, self.position)
+ for point in points[1:]:
+ cur_point = rotate_point(point, rotation, self.position)
+
+ lines.append(Line(prev_point, cur_point, aperture))
+
+ prev_point = cur_point
+
+ outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity))
+
+ return outlines
+
+
+class AMCenterLinePrimitive(AMPrimitive):
+ """ Aperture Macro Center Line primitive. Code 21.
+
+ The center line primitive is a rectangle defined by its width, height, and center point.
+
+ .. seealso::
+ `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.4:** Center Line, primitive code 21.
+
+ Parameters
+ ----------
+ code : int
+ Center Line Primitive code. Must be 21.
+
+ exposure : str
+ 'on' or 'off'
+
+ width : float
+ Width of rectangle
+
+ height : float
+ Height of rectangle
+
+ center : tuple (<float>, <float>)
+ X and Y coordinates of line center
+
+ rotation : float
+ rectangle rotation about its center.
+
+ Returns
+ -------
+ CenterLinePrimitive : :class:`gerber.am_statements.AMCenterLinePrimitive`
+ An initialized AMCenterLinePrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+
+ @classmethod
+ def from_primitive(cls, primitive):
+ width = primitive.width
+ height = primitive.height
+ center = primitive.position
+ rotation = math.degrees(primitive.rotation)
+ return cls(21, 'on', width, height, center, rotation)
+
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(",")
+ code = int(modifiers[0])
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
+ width = float(modifiers[2])
+ height = float(modifiers[3])
+ center = (float(modifiers[4]), float(modifiers[5]))
+ rotation = float(modifiers[6])
+ return cls(code, exposure, width, height, center, rotation)
+
+ def __init__(self, code, exposure, width, height, center, rotation):
+ if code != 21:
+ raise ValueError('CenterLinePrimitive code is 21')
+ super(AMCenterLinePrimitive, self).__init__(code, exposure)
+ self.width = width
+ self.height = height
+ validate_coordinates(center)
+ self.center = center
+ self.rotation = rotation
+
+ def to_inch(self):
+ self.center = tuple([inch(x) for x in self.center])
+ self.width = inch(self.width)
+ self.height = inch(self.height)
+
+ def to_metric(self):
+ self.center = tuple([metric(x) for x in self.center])
+ self.width = metric(self.width)
+ self.height = metric(self.height)
+
+ def to_gerber(self, settings=None):
+ data = dict(
+ code=self.code,
+ exposure = '1' if self.exposure == 'on' else '0',
+ width = self.width,
+ height = self.height,
+ center="%.4g,%.4g" % self.center,
+ rotation=self.rotation
+ )
+ fmt = "{code},{exposure},{width},{height},{center},{rotation}*"
+ return fmt.format(**data)
+
+ def to_primitive(self, units):
+
+ x = self.center[0]
+ y = self.center[1]
+ half_width = self.width / 2.0
+ half_height = self.height / 2.0
+
+ points = []
+ points.append((x - half_width, y + half_height))
+ points.append((x - half_width, y - half_height))
+ points.append((x + half_width, y - half_height))
+ points.append((x + half_width, y + half_height))
+
+ aperture = Circle((0, 0), 0)
+
+ lines = []
+ prev_point = rotate_point(points[3], self.rotation, self.center)
+ for point in points:
+ cur_point = rotate_point(point, self.rotation, self.center)
+
+ lines.append(Line(prev_point, cur_point, aperture))
+
+ return Outline(lines, units=units, level_polarity=self._level_polarity)
+
+
+class AMLowerLeftLinePrimitive(AMPrimitive):
+ """ Aperture Macro Lower Left Line primitive. Code 22.
+
+ The lower left line primitive is a rectangle defined by its width, height, and the lower left point.
+
+ .. seealso::
+ `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
+ **Section 4.12.3.5:** Lower Left Line, primitive code 22.
+
+ Parameters
+ ----------
+ code : int
+ Center Line Primitive code. Must be 22.
+
+ exposure : str
+ 'on' or 'off'
+
+ width : float
+ Width of rectangle
+
+ height : float
+ Height of rectangle
+
+ lower_left : tuple (<float>, <float>)
+ X and Y coordinates of lower left corner
+
+ rotation : float
+ rectangle rotation about its origin.
+
+ Returns
+ -------
+ LowerLeftLinePrimitive : :class:`gerber.am_statements.AMLowerLeftLinePrimitive`
+ An initialized AMLowerLeftLinePrimitive
+
+ Raises
+ ------
+ ValueError, TypeError
+ """
+ @classmethod
+ def from_gerber(cls, primitive):
+ modifiers = primitive.strip(' *').split(",")
+ code = int(modifiers[0])
+ exposure = 'on' if float(modifiers[1]) == 1 else 'off'
+ width = float(modifiers[2])
+ height = float(modifiers[3])
+ lower_left = (float(modifiers[4]), float(modifiers[5]))
+ rotation = float(modifiers[6])
+ return cls(code, exposure, width, height, lower_left, rotation)
+
+ def __init__(self, code, exposure, width, height, lower_left, rotation):
+ if code != 22:
+ raise ValueError('LowerLeftLinePrimitive code is 22')
+ super (AMLowerLeftLinePrimitive, self).__init__(code, exposure)
+ self.width = width
+ self.height = height
+ validate_coordinates(lower_left)
+ self.lower_left = lower_left
+ self.rotation = rotation
+
+ def to_inch(self):
+ self.lower_left = tuple([inch(x) for x in self.lower_left])
+ self.width = inch(self.width)
+ self.height = inch(self.height)
+
+ def to_metric(self):
+ self.lower_left = tuple([metric(x) for x in self.lower_left])
+ self.width = metric(self.width)
+ self.height = metric(self.height)
+
+ def to_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/gerber/cam.py b/gerber/cam.py
new file mode 100644
index 0000000..4f20283
--- /dev/null
+++ b/gerber/cam.py
@@ -0,0 +1,286 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+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 ('<Settings: %s %s %s %s %s>' %
+ (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 (<int>, <int>)
+ 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 <optional>
+ 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/gerber/common.py b/gerber/common.py
new file mode 100644
index 0000000..f496809
--- /dev/null
+++ b/gerber/common.py
@@ -0,0 +1,71 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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/gerber/excellon.py b/gerber/excellon.py
new file mode 100755
index 0000000..5ab062a
--- /dev/null
+++ b/gerber/excellon.py
@@ -0,0 +1,904 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+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 (<Tool>, (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 (<int part>, <decimal part>)
+ - `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/gerber/excellon_report/excellon_drr.py b/gerber/excellon_report/excellon_drr.py
new file mode 100644
index 0000000..ab9e857
--- /dev/null
+++ b/gerber/excellon_report/excellon_drr.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Excellon DRR File module
+====================
+**Excellon file classes**
+
+Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
+"""
+
diff --git a/gerber/excellon_settings.py b/gerber/excellon_settings.py
new file mode 100644
index 0000000..4dbe0ca
--- /dev/null
+++ b/gerber/excellon_settings.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from argparse import PARSER
+
+# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Excellon Settings Definition File module
+====================
+**Excellon file classes**
+
+This module provides Excellon file classes and parsing utilities
+"""
+
+import re
+try:
+ from cStringIO import StringIO
+except(ImportError):
+ from io import StringIO
+
+from .cam import FileSettings
+
+def loads(data):
+ """ Read settings file information and return an FileSettings
+ Parameters
+ ----------
+ data : string
+ string containing Excellon settings file contents
+
+ Returns
+ -------
+ file settings: FileSettings
+
+ """
+
+ return ExcellonSettingsParser().parse_raw(data)
+
+def map_coordinates(value):
+ if value == 'ABSOLUTE':
+ return 'absolute'
+ return 'relative'
+
+def map_units(value):
+ if value == 'ENGLISH':
+ return 'inch'
+ return 'metric'
+
+def map_boolean(value):
+ return value == 'YES'
+
+SETTINGS_KEYS = {
+ 'INTEGER-PLACES': (int, 'format-int'),
+ 'DECIMAL-PLACES': (int, 'format-dec'),
+ 'COORDINATES': (map_coordinates, 'notation'),
+ 'OUTPUT-UNITS': (map_units, 'units'),
+ }
+
+class ExcellonSettingsParser(object):
+ """Excellon Settings PARSER
+
+ Parameters
+ ----------
+ None
+ """
+
+ def __init__(self):
+ self.values = {}
+ self.settings = None
+
+ def parse_raw(self, data):
+ for line in StringIO(data):
+ self._parse(line.strip())
+
+ # Create the FileSettings object
+ self.settings = FileSettings(
+ notation=self.values['notation'],
+ units=self.values['units'],
+ format=(self.values['format-int'], self.values['format-dec'])
+ )
+
+ return self.settings
+
+ def _parse(self, line):
+
+ line_items = line.split()
+ if len(line_items) == 2:
+
+ item_type_info = SETTINGS_KEYS.get(line_items[0])
+ if item_type_info:
+ # Convert the value to the expected type
+ item_value = item_type_info[0](line_items[1])
+
+ self.values[item_type_info[1]] = item_value \ No newline at end of file
diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py
new file mode 100644
index 0000000..2c50ef9
--- /dev/null
+++ b/gerber/excellon_statements.py
@@ -0,0 +1,979 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+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 = '<ExcellonTool %%02d: %%%d.%dg%%s dia.>' % self.settings.format
+ return fmtstr % (self.number, self.diameter, unit)
+
+
+class ToolSelectionStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **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 '<Coordinate Statement: %s>' % coord_str
+
+
+class RepeatHoleStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, settings, **kwargs):
+ match = re.compile(r'R(?P<rcount>[0-9]*)X?(?P<xdelta>[+\-]?\d*\.?\d*)?Y?'
+ '(?P<ydelta>[+\-]?\d*\.?\d*)?').match(line)
+ stmt = match.groupdict()
+ count = int(stmt['rcount'])
+ xdelta = (parse_gerber_value(stmt['xdelta'], settings.format,
+ settings.zero_suppression)
+ if stmt['xdelta'] is not '' else None)
+ ydelta = (parse_gerber_value(stmt['ydelta'], settings.format,
+ settings.zero_suppression)
+ if stmt['ydelta'] is not '' else None)
+ c = cls(count, xdelta, ydelta, **kwargs)
+ c.units = settings.units
+ return c
+
+ def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs):
+ super(RepeatHoleStmt, self).__init__(**kwargs)
+ self.count = count
+ self.xdelta = xdelta
+ self.ydelta = ydelta
+
+ def to_excellon(self, settings):
+ stmt = 'R%d' % self.count
+ if self.xdelta is not None and self.xdelta != 0.0:
+ stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format,
+ settings.zero_suppression)
+ if self.ydelta is not None and self.ydelta != 0.0:
+ stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format,
+ settings.zero_suppression)
+ return stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.xdelta is not None:
+ self.xdelta = inch(self.xdelta)
+ if self.ydelta is not None:
+ self.ydelta = inch(self.ydelta)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.xdelta is not None:
+ self.xdelta = metric(self.xdelta)
+ if self.ydelta is not None:
+ self.ydelta = metric(self.ydelta)
+
+ def __str__(self):
+ return '<Repeat Hole: %d times, offset X: %g Y: %g>' % (
+ self.count,
+ self.xdelta if self.xdelta is not None else 0,
+ self.ydelta if self.ydelta is not None else 0)
+
+
+class CommentStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **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<x>\d*\.?\d*)?Y?'
+ '(?P<y>\d*\.?\d*)?').match(line)
+ stmt = match.groupdict()
+ x = (parse_gerber_value(stmt['x'], settings.format,
+ settings.zero_suppression)
+ if stmt['x'] is not '' else None)
+ y = (parse_gerber_value(stmt['y'], settings.format,
+ settings.zero_suppression)
+ if stmt['y'] is not '' else None)
+ c = cls(x, y, **kwargs)
+ c.units = settings.units
+ return c
+
+ def __init__(self, x=None, y=None, **kwargs):
+ super(EndOfProgramStmt, self).__init__(**kwargs)
+ self.x = x
+ self.y = y
+
+ def to_excellon(self, 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 "<Unknown Statement: %s>" % self.stmt
+
+
+class SlotStmt(ExcellonStatement):
+ """
+ G85 statement. Defines a slot created by multiple drills between two specified points.
+
+ Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
+ """
+
+ @classmethod
+ def from_points(cls, start, end):
+
+ return cls(start[0], start[1], end[0], end[1])
+
+ @classmethod
+ def from_excellon(cls, line, settings, **kwargs):
+ # Split the line based on the G85 separator
+ sub_coords = line.split('G85')
+ (x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
+ (x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
+
+ # Some files seem to specify only one of the coordinates
+ if x_end_coord == None:
+ x_end_coord = x_start_coord
+ if y_end_coord == None:
+ y_end_coord = y_start_coord
+
+ c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
+ c.units = settings.units
+ return c
+
+ @staticmethod
+ def parse_sub_coords(line, settings):
+
+ x_coord = None
+ y_coord = None
+
+ if line[0] == 'X':
+ splitline = line.strip('X').split('Y')
+ x_coord = parse_gerber_value(splitline[0], settings.format,
+ settings.zero_suppression)
+ if len(splitline) == 2:
+ y_coord = parse_gerber_value(splitline[1], settings.format,
+ settings.zero_suppression)
+ else:
+ y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
+ settings.zero_suppression)
+
+ return (x_coord, y_coord)
+
+
+ def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs):
+ super(SlotStmt, self).__init__(**kwargs)
+ self.x_start = x_start
+ self.y_start = y_start
+ self.x_end = x_end
+ self.y_end = y_end
+ self.mode = None
+
+ def to_excellon(self, settings):
+ stmt = ''
+
+ if self.x_start is not None:
+ stmt += 'X%s' % write_gerber_value(self.x_start, settings.format,
+ settings.zero_suppression)
+ if self.y_start is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
+ settings.zero_suppression)
+
+ stmt += 'G85'
+
+ if self.x_end is not None:
+ stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
+ settings.zero_suppression)
+ if self.y_end is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
+ settings.zero_suppression)
+
+ return stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.x_start is not None:
+ self.x_start = inch(self.x_start)
+ if self.y_start is not None:
+ self.y_start = inch(self.y_start)
+ if self.x_end is not None:
+ self.x_end = inch(self.x_end)
+ if self.y_end is not None:
+ self.y_end = inch(self.y_end)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.x_start is not None:
+ self.x_start = metric(self.x_start)
+ if self.y_start is not None:
+ self.y_start = metric(self.y_start)
+ if self.x_end is not None:
+ self.x_end = metric(self.x_end)
+ if self.y_end is not None:
+ self.y_end = metric(self.y_end)
+
+ def offset(self, x_offset=0, y_offset=0):
+ if self.x_start is not None:
+ self.x_start += x_offset
+ if self.y_start is not None:
+ self.y_start += y_offset
+ if self.x_end is not None:
+ self.x_end += x_offset
+ if self.y_end is not None:
+ self.y_end += y_offset
+
+ def __str__(self):
+ start_str = ''
+ if self.x_start is not None:
+ start_str += 'X: %g ' % self.x_start
+ if self.y_start is not None:
+ start_str += 'Y: %g ' % self.y_start
+
+ end_str = ''
+ if self.x_end is not None:
+ end_str += 'X: %g ' % self.x_end
+ if self.y_end is not None:
+ end_str += 'Y: %g ' % self.y_end
+
+ return '<Slot Statement: %s to %s>' % (start_str, end_str)
+
+def pairwise(iterator):
+ """ Iterate over list taking two elements at a time.
+
+ 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/gerber/excellon_tool.py b/gerber/excellon_tool.py
new file mode 100644
index 0000000..a9ac450
--- /dev/null
+++ b/gerber/excellon_tool.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Excellon Tool Definition File module
+====================
+**Excellon file classes**
+
+This module provides Excellon file classes and parsing utilities
+"""
+
+import re
+try:
+ from cStringIO import StringIO
+except(ImportError):
+ from io import StringIO
+
+from .excellon_statements import ExcellonTool
+
+def loads(data, settings=None):
+ """ Read tool file information and return a map of tools
+ Parameters
+ ----------
+ data : string
+ string containing Excellon Tool Definition file contents
+
+ Returns
+ -------
+ dict tool name: ExcellonTool
+
+ """
+ return ExcellonToolDefinitionParser(settings).parse_raw(data)
+
+class ExcellonToolDefinitionParser(object):
+ """ Excellon File Parser
+
+ Parameters
+ ----------
+ None
+ """
+
+ allegro_tool = re.compile(r'(?P<size>[0-9/.]+)\s+(?P<plated>P|N)\s+T(?P<toolid>[0-9]{2})\s+(?P<xtol>[0-9/.]+)\s+(?P<ytol>[0-9/.]+)')
+ allegro_comment_mils = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
+ allegro2_comment_mils = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
+ allegro_comment_mm = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
+ allegro2_comment_mm = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
+
+ matchers = [
+ (allegro_tool, 'mils'),
+ (allegro_comment_mils, 'mils'),
+ (allegro2_comment_mils, 'mils'),
+ (allegro_comment_mm, 'mm'),
+ (allegro2_comment_mm, 'mm'),
+ ]
+
+ def __init__(self, settings=None):
+ self.tools = {}
+ self.settings = settings
+
+ def parse_raw(self, data):
+ for line in StringIO(data):
+ self._parse(line.strip())
+
+ return self.tools
+
+ def _parse(self, line):
+
+ for matcher in ExcellonToolDefinitionParser.matchers:
+ m = matcher[0].match(line)
+ if m:
+ unit = matcher[1]
+
+ size = float(m.group('size'))
+ platedstr = m.group('plated')
+ toolid = int(m.group('toolid'))
+ xtol = float(m.group('xtol'))
+ ytol = float(m.group('ytol'))
+
+ size = self._convert_length(size, unit)
+ xtol = self._convert_length(xtol, unit)
+ ytol = self._convert_length(ytol, unit)
+
+ if platedstr == 'PLATED':
+ plated = ExcellonTool.PLATED_YES
+ elif platedstr == 'NON_PLATED':
+ plated = ExcellonTool.PLATED_NO
+ elif platedstr == 'OPTIONAL':
+ plated = ExcellonTool.PLATED_OPTIONAL
+ else:
+ plated = ExcellonTool.PLATED_UNKNOWN
+
+ tool = ExcellonTool(None, number=toolid, diameter=size,
+ plated=plated)
+
+ self.tools[tool.number] = tool
+
+ break
+
+ def _convert_length(self, value, unit):
+
+ # Convert the value to mm
+ if unit == 'mils':
+ value /= 39.3700787402
+
+ # Now convert to the settings unit
+ if self.settings.units == 'inch':
+ return value / 25.4
+ else:
+ # Already in mm
+ return value
+
+def loads_rep(data, settings=None):
+ """ Read tool report information generated by PADS and return a map of tools
+ Parameters
+ ----------
+ data : string
+ string containing Excellon Report file contents
+
+ Returns
+ -------
+ dict tool name: ExcellonTool
+
+ """
+ return ExcellonReportParser(settings).parse_raw(data)
+
+class ExcellonReportParser(object):
+
+ # We sometimes get files with different encoding, so we can't actually
+ # match the text - the best we can do it detect the table header
+ header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===')
+
+ def __init__(self, settings=None):
+ self.tools = {}
+ self.settings = settings
+
+ self.found_header = False
+
+ def parse_raw(self, data):
+ for line in StringIO(data):
+ self._parse(line.strip())
+
+ return self.tools
+
+ def _parse(self, line):
+
+ # skip empty lines and "comments"
+ if not line.strip():
+ return
+
+ if not self.found_header:
+ # Try to find the heaader, since we need that to be sure we
+ # understand the contents correctly.
+ if ExcellonReportParser.header.match(line):
+ self.found_header = True
+
+ elif line[0] != '=':
+ # Already found the header, so we know to to map the contents
+ parts = line.split()
+ if len(parts) == 6:
+ toolid = int(parts[0])
+ size = float(parts[1])
+ if parts[2] == 'x':
+ plated = ExcellonTool.PLATED_YES
+ elif parts[2] == '-':
+ plated = ExcellonTool.PLATED_NO
+ else:
+ plated = ExcellonTool.PLATED_UNKNOWN
+ feedrate = int(parts[3])
+ speed = int(parts[4])
+ qty = int(parts[5])
+
+ tool = ExcellonTool(None, number=toolid, diameter=size,
+ plated=plated, feed_rate=feedrate,
+ rpm=speed)
+
+ self.tools[tool.number] = tool
diff --git a/gerber/exceptions.py b/gerber/exceptions.py
new file mode 100644
index 0000000..65ae905
--- /dev/null
+++ b/gerber/exceptions.py
@@ -0,0 +1,36 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class ParseError(Exception):
+ pass
+
+
+class GerberParseError(ParseError):
+ pass
+
+
+class ExcellonParseError(ParseError):
+ pass
+
+
+class ExcellonFileError(IOError):
+ pass
+
+
+class GerberFileError(IOError):
+ pass
diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py
new file mode 100644
index 0000000..28f5e81
--- /dev/null
+++ b/gerber/gerber_statements.py
@@ -0,0 +1,1189 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Gerber (RS-274X) Statements
+===========================
+**Gerber RS-274X file statement classes**
+
+"""
+from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
+ 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 ('<Format Spec: %d:%d %s zero suppression %s notation>' %
+ (self.format[0], self.format[1], self.zero_suppression, self.notation))
+
+
+class MOParamStmt(ParamStmt):
+ """ MO - Gerber Mode (measurement units) Statement.
+ """
+
+ @classmethod
+ def from_units(cls, units):
+ return cls(None, units)
+
+ @classmethod
+ def from_dict(cls, stmt_dict):
+ param = stmt_dict.get('param')
+ if stmt_dict.get('mo') 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: %s>' % 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 '<Level Polarity: %s>' % 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 '<Aperture Definition: %d: %s>' % (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 '<Aperture Macro %s: %s>' % (self.name, self.macro)
+
+
+class ASParamStmt(ParamStmt):
+ """ AS - Axis Select. (Deprecated)
+ """
+ @classmethod
+ def from_dict(cls, stmt_dict):
+ param = stmt_dict.get('param')
+ mode = stmt_dict.get('mode')
+ return cls(param, mode)
+
+ def __init__(self, param, mode):
+ """ Initialize ASParamStmt class
+
+ Parameters
+ ----------
+ param : string
+ Parameter string.
+
+ mode : string
+ Axis select. May be either 'AXBY' or 'AYBX'
+
+ Returns
+ -------
+ ParamStmt : ASParamStmt
+ Initialized ASParamStmt class.
+
+ """
+ ParamStmt.__init__(self, param)
+ self.mode = mode
+
+ def to_gerber(self, settings=None):
+ return '%AS{0}*%'.format(self.mode)
+
+ def __str__(self):
+ return ('<Axis Select: %s>' % self.mode)
+
+
+class INParamStmt(ParamStmt):
+ """ IN - Image Name Statement (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 '<Image Name: %s>' % self.name
+
+
+class IPParamStmt(ParamStmt):
+ """ IP - Gerber Image Polarity Statement. (Deprecated)
+ """
+ @classmethod
+ def from_dict(cls, stmt_dict):
+ param = stmt_dict.get('param')
+ ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative'
+ return cls(param, ip)
+
+ def __init__(self, param, ip):
+ """ Initialize IPParamStmt class
+
+ Parameters
+ ----------
+ param : string
+ Parameter string.
+
+ ip : string
+ Image polarity. May be either'positive' or 'negative'
+
+ Returns
+ -------
+ ParamStmt : IPParamStmt
+ Initialized IPParamStmt class.
+
+ """
+ ParamStmt.__init__(self, param)
+ self.ip = ip
+
+ def to_gerber(self, settings=None):
+ ip = 'POS' if self.ip == 'positive' else 'NEG'
+ return '%IP{0}*%'.format(ip)
+
+ def __str__(self):
+ return ('<Image Polarity: %s>' % self.ip)
+
+
+class IRParamStmt(ParamStmt):
+ """ IR - Image Rotation Param (Deprecated)
+ """
+ @classmethod
+ def from_dict(cls, stmt_dict):
+ angle = int(stmt_dict['angle'])
+ return cls(stmt_dict['param'], angle)
+
+ def __init__(self, param, angle):
+ """ Initialize IRParamStmt class
+
+ Parameters
+ ----------
+ param : string
+ Parameter code
+
+ angle : int
+ Image angle
+
+ Returns
+ -------
+ ParamStmt : IRParamStmt
+ Initialized IRParamStmt class.
+
+ """
+ ParamStmt.__init__(self, param)
+ self.angle = angle
+
+ def to_gerber(self, settings=None):
+ return '%IR{0}*%'.format(self.angle)
+
+ def __str__(self):
+ return '<Image Angle: %s>' % self.angle
+
+
+class MIParamStmt(ParamStmt):
+ """ MI - Image Mirror Param (Deprecated)
+ """
+ @classmethod
+ def from_dict(cls, stmt_dict):
+ param = stmt_dict.get('param')
+ a = int(stmt_dict.get('a', 0))
+ b = int(stmt_dict.get('b', 0))
+ return cls(param, a, b)
+
+ def __init__(self, param, a, b):
+ """ Initialize MIParamStmt class
+
+ Parameters
+ ----------
+ param : string
+ Parameter code
+
+ a : int
+ Mirror for A output devices axis (0=disabled, 1=mirrored)
+
+ b : int
+ Mirror for B output devices axis (0=disabled, 1=mirrored)
+
+ Returns
+ -------
+ ParamStmt : MIParamStmt
+ Initialized MIParamStmt class.
+
+ """
+ ParamStmt.__init__(self, param)
+ self.a = a
+ self.b = b
+
+ def to_gerber(self, settings=None):
+ ret = "%MI"
+ if self.a is not None:
+ ret += "A{0}".format(self.a)
+ if self.b is not None:
+ ret += "B{0}".format(self.b)
+ ret += "*%"
+ return ret
+
+ def __str__(self):
+ return '<Image Mirror: A=%d B=%d>' % (self.a, self.b)
+
+
+class OFParamStmt(ParamStmt):
+ """ OF - Gerber Offset statement (Deprecated)
+ """
+
+ @classmethod
+ def from_dict(cls, stmt_dict):
+ param = stmt_dict.get('param')
+ a = float(stmt_dict.get('a', 0))
+ b = float(stmt_dict.get('b', 0))
+ return cls(param, a, b)
+
+ def __init__(self, param, a, b):
+ """ Initialize OFParamStmt class
+
+ Parameters
+ ----------
+ param : string
+ Parameter
+
+ a : float
+ Offset along the output device A axis
+
+ b : float
+ Offset along the output device B axis
+
+ Returns
+ -------
+ ParamStmt : OFParamStmt
+ Initialized OFParamStmt class.
+
+ """
+ ParamStmt.__init__(self, param)
+ self.a = a
+ self.b = b
+
+ def to_gerber(self, settings=None):
+ ret = '%OF'
+ if self.a is not None:
+ ret += 'A' + decimal_string(self.a, precision=5)
+ if self.b is not None:
+ ret += 'B' + decimal_string(self.b, precision=5)
+ return ret + '*%'
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.a is not None:
+ self.a = inch(self.a)
+ if self.b is not None:
+ self.b = inch(self.b)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.a is not None:
+ self.a = metric(self.a)
+ if self.b is not None:
+ self.b = metric(self.b)
+
+ def offset(self, x_offset=0, y_offset=0):
+ if self.a is not None:
+ self.a += x_offset
+ if self.b is not None:
+ self.b += y_offset
+
+ def __str__(self):
+ offset_str = ''
+ if self.a is not None:
+ offset_str += ('X: %f ' % self.a)
+ if self.b is not None:
+ offset_str += ('Y: %f ' % self.b)
+ return ('<Offset: %s>' % offset_str)
+
+
+class SFParamStmt(ParamStmt):
+ """ SF - Scale Factor Param (Deprecated)
+ """
+
+ @classmethod
+ def from_dict(cls, stmt_dict):
+ param = stmt_dict.get('param')
+ a = float(stmt_dict.get('a', 1))
+ b = float(stmt_dict.get('b', 1))
+ return cls(param, a, b)
+
+ def __init__(self, param, a, b):
+ """ Initialize OFParamStmt class
+
+ Parameters
+ ----------
+ param : string
+ Parameter
+
+ a : float
+ Scale factor for the output device A axis
+
+ b : float
+ Scale factor for the output device B axis
+
+ Returns
+ -------
+ ParamStmt : SFParamStmt
+ Initialized SFParamStmt class.
+
+ """
+ ParamStmt.__init__(self, param)
+ self.a = a
+ self.b = b
+
+ def to_gerber(self, settings=None):
+ ret = '%SF'
+ if self.a is not None:
+ ret += 'A' + decimal_string(self.a, precision=5)
+ if self.b is not None:
+ ret += 'B' + decimal_string(self.b, precision=5)
+ return ret + '*%'
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.a is not None:
+ self.a = inch(self.a)
+ if self.b is not None:
+ self.b = inch(self.b)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.a is not None:
+ self.a = metric(self.a)
+ if self.b is not None:
+ self.b = metric(self.b)
+
+ def offset(self, x_offset=0, y_offset=0):
+ if self.a is not None:
+ self.a += x_offset
+ if self.b is not None:
+ self.b += y_offset
+
+ def __str__(self):
+ scale_factor = ''
+ if self.a is not None:
+ scale_factor += ('X: %g ' % self.a)
+ if self.b is not None:
+ scale_factor += ('Y: %g' % self.b)
+ return ('<Scale Factor: %s>' % scale_factor)
+
+
+class LNParamStmt(ParamStmt):
+ """ LN - Level Name Statement (Deprecated)
+ """
+ @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 '<Level Name: %s>' % self.name
+
+
+class DeprecatedStmt(Statement):
+ """ Unimportant deprecated statement, will be parsed but not emitted.
+ """
+ @classmethod
+ def from_gerber(cls, line):
+ return cls(line)
+
+ def __init__(self, line):
+ """ Initialize DeprecatedStmt class
+
+ Parameters
+ ----------
+ line : string
+ Deprecated statement text
+
+ Returns
+ -------
+ DeprecatedStmt
+ Initialized DeprecatedStmt class.
+
+ """
+ Statement.__init__(self, "DEPRECATED")
+ self.line = line
+
+ def to_gerber(self, settings=None):
+ return self.line
+
+ def __str__(self):
+ return '<Deprecated Statement: \'%s\'>' % self.line
+
+
+class CoordStmt(Statement):
+ """ Coordinate Data Block
+ """
+
+ OP_DRAW = 'D01'
+ OP_MOVE = 'D02'
+ OP_FLASH = 'D03'
+
+ FUNC_LINEAR = 'G01'
+ FUNC_ARC_CW = 'G02'
+ FUNC_ARC_CCW = 'G03'
+
+ @classmethod
+ def from_dict(cls, stmt_dict, settings):
+ 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 '<Coordinate Statement: %s>' % coord_str
+
+ @property
+ def only_function(self):
+ """
+ Returns if the statement only set the function.
+ """
+
+ # TODO I would like to refactor this so that the function is handled separately and then
+ # TODO this isn't required
+ return self.function != None and self.op == None and self.x == None and self.y == None and self.i == None and self.j == None
+
+
+class ApertureStmt(Statement):
+ """ Aperture Statement
+ """
+
+ def __init__(self, d, 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 '<Aperture: %d>' % 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 '<Comment: %s>' % 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 '<EOF Statement>'
+
+
+class QuadrantModeStmt(Statement):
+
+ @classmethod
+ def single(cls):
+ return cls('single-quadrant')
+
+ @classmethod
+ def multi(cls):
+ return cls('multi-quadrant')
+
+ @classmethod
+ def from_gerber(cls, line):
+ 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 '<Unknown Statement: \'%s\'>' % self.line
diff --git a/gerber/ipc356.py b/gerber/ipc356.py
new file mode 100644
index 0000000..9337a99
--- /dev/null
+++ b/gerber/ipc356.py
@@ -0,0 +1,485 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import math
+import re
+from .cam import CamFile, FileSettings
+from .primitives import TestRecord
+
+# Net Name Variables
+_NNAME = re.compile(r'^NNAME\d+$')
+
+# Board Edge Coordinates
+_COORD = re.compile(r'X?(?P<x>[\d\s]*)?Y?(?P<y>[\d\s]*)?')
+
+_SM_FIELD = {
+ '0': 'none',
+ '1': 'primary side',
+ '2': 'secondary side',
+ '3': 'both'}
+
+
+def read(filename):
+ """ Read data from filename and return an 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 '<IPC-D-356 Comment: %s>' % self.comment
+
+
+class IPC356_Parameter(object):
+
+ @classmethod
+ def from_line(cls, line):
+ if line[0] != 'P':
+ raise ValueError('Not a valid parameter statment')
+ splitline = line[2:].split()
+ parameter = splitline[0].strip()
+ value = ' '.join(splitline[1:]).strip()
+ return cls(parameter, value)
+
+ def __init__(self, parameter, value):
+ self.parameter = parameter
+ self.value = value
+
+ def __repr__(self):
+ return '<IPC-D-356 Parameter: %s=%s>' % (self.parameter, self.value)
+
+
+class IPC356_TestRecord(object):
+
+ @classmethod
+ def from_line(cls, line, settings):
+ offset = 0
+ units = settings.units
+ angle = settings.angle_units
+ feature_types = {'1': 'through-hole', '2': 'smt',
+ '3': 'tooling-feature', '4': 'tooling-hole',
+ '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 '<IPC-D-356 %s Test Record: %s>' % (self.net_name,
+ self.feature_type)
+
+
+class IPC356_Outline(object):
+
+ @classmethod
+ def from_line(cls, line, settings):
+ type = line[3:17].strip()
+ scale = 0.0001 if settings.units == 'inch' else 0.001
+ points = []
+ x = 0
+ y = 0
+ coord_strings = line.strip().split()[1:]
+ for coord in coord_strings:
+ coord_dict = _COORD.match(coord).groupdict()
+ x = int(coord_dict['x']) if coord_dict['x'] is not '' else x
+ y = int(coord_dict['y']) if coord_dict['y'] is not '' else y
+ points.append((x * scale, y * scale))
+ return cls(type, points)
+
+ def __init__(self, type, points):
+ self.type = type
+ self.points = points
+
+ def __repr__(self):
+ return '<IPC-D-356 %s Outline Definition>' % self.type
+
+
+class IPC356_Conductor(object):
+
+ @classmethod
+ def from_line(cls, line, settings):
+ if line[0:3] != '378':
+ raise ValueError('Not a valid IPC-D-356 Conductor statement')
+
+ scale = 0.0001 if settings.units == 'inch' else 0.001
+ net_name = line[3:17].strip()
+ layer = int(line[19:21])
+
+ # Parse out aperture definiting
+ raw_aperture = line[22:].split()[0]
+ aperture_dict = _COORD.match(raw_aperture).groupdict()
+ x = 0
+ y = 0
+ x = int(aperture_dict['x']) * \
+ scale if aperture_dict['x'] is not '' else None
+ y = int(aperture_dict['y']) * \
+ scale if aperture_dict['y'] is not '' else None
+ aperture = (x, y)
+
+ # Parse out conductor shapes
+ shapes = []
+ coord_list = ' '.join(line[22:].split()[1:])
+ raw_shapes = coord_list.split('*')
+ for rshape in raw_shapes:
+ x = 0
+ y = 0
+ shape = []
+ coords = rshape.split()
+ for coord in coords:
+ coord_dict = _COORD.match(coord).groupdict()
+ x = int(coord_dict['x']) if coord_dict['x'] is not '' else x
+ y = int(coord_dict['y']) if coord_dict['y'] is not '' else y
+ shape.append((x * scale, y * scale))
+ shapes.append(tuple(shape))
+ return cls(net_name, layer, aperture, tuple(shapes))
+
+ def __init__(self, net_name, layer, aperture, shapes):
+ self.net_name = net_name
+ self.layer = layer
+ self.aperture = aperture
+ self.shapes = shapes
+
+ def __repr__(self):
+ return '<IPC-D-356 %s Conductor Record>' % self.net_name
+
+
+class IPC356_Adjacency(object):
+
+ @classmethod
+ def from_line(cls, line):
+ if line[0:3] != '379':
+ raise ValueError('Not a valid IPC-D-356 Conductor statement')
+ nets = line[3:].strip().split()
+
+ return cls(nets[0], nets[1:])
+
+ def __init__(self, net, adjacent_nets):
+ self.net = net
+ self.adjacent_nets = adjacent_nets
+
+ def __repr__(self):
+ return '<IPC-D-356 %s Adjacency Record>' % self.net
+
+
+class IPC356_EndOfFile(object):
+
+ def __init__(self):
+ pass
+
+ def to_netlist(self):
+ return '999'
+
+ def __repr__(self):
+ return '<IPC-D-356 EOF>'
+
+
+class IPC356_Net(object):
+
+ def __init__(self, name, adjacent_nets):
+ self.name = name
+ self.adjacent_nets = set(
+ adjacent_nets) if adjacent_nets is not None else set()
+
+ def __repr__(self):
+ return '<IPC-D-356 Net %s>' % self.name
diff --git a/gerber/layers.py b/gerber/layers.py
new file mode 100644
index 0000000..69e1c0d
--- /dev/null
+++ b/gerber/layers.py
@@ -0,0 +1,295 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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 '<PCBLayer: {}>'.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/gerber/ncparam/allegro.py b/gerber/ncparam/allegro.py
new file mode 100644
index 0000000..a67bcf1
--- /dev/null
+++ b/gerber/ncparam/allegro.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Allegro File module
+====================
+**Excellon file classes**
+
+Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
+"""
+
diff --git a/gerber/operations.py b/gerber/operations.py
new file mode 100644
index 0000000..d06876e
--- /dev/null
+++ b/gerber/operations.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+CAM File Operations
+===================
+**Transformations and other operations performed on Gerber and Excellon files**
+
+"""
+import copy
+
+
+def to_inch(cam_file):
+ """ Convert Gerber or Excellon file units to imperial
+
+ Parameters
+ ----------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ Gerber or Excellon file to convert
+
+ Returns
+ -------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ A deep copy of the source file with units converted to imperial.
+ """
+ cam_file = copy.deepcopy(cam_file)
+ cam_file.to_inch()
+ return cam_file
+
+
+def to_metric(cam_file):
+ """ Convert Gerber or Excellon file units to metric
+
+ Parameters
+ ----------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ Gerber or Excellon file to convert
+
+ Returns
+ -------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ A deep copy of the source file with units converted to metric.
+ """
+ cam_file = copy.deepcopy(cam_file)
+ cam_file.to_metric()
+ return cam_file
+
+
+def offset(cam_file, x_offset, y_offset):
+ """ Offset a Cam file by a specified amount in the X and Y directions.
+
+ Parameters
+ ----------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ Gerber or Excellon file to offset
+
+ x_offset : float
+ Amount to offset the file in the X direction
+
+ y_offset : float
+ Amount to offset the file in the Y direction
+
+ Returns
+ -------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ An offset deep copy of the source file.
+ """
+ cam_file = copy.deepcopy(cam_file)
+ cam_file.offset(x_offset, y_offset)
+ return cam_file
+
+
+def scale(cam_file, x_scale, y_scale):
+ """ Scale a Cam file by a specified amount in the X and Y directions.
+
+ Parameters
+ ----------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ Gerber or Excellon file to scale
+
+ x_scale : float
+ X-axis scale factor
+
+ y_scale : float
+ Y-axis scale factor
+
+ Returns
+ -------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ An scaled deep copy of the source file.
+ """
+ # TODO
+ pass
+
+
+def rotate(cam_file, angle):
+ """ Rotate a Cam file a specified amount about the origin.
+
+ Parameters
+ ----------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ Gerber or Excellon file to rotate
+
+ angle : float
+ Angle to rotate the file in degrees.
+
+ Returns
+ -------
+ cam_file : :class:`gerber.cam.CamFile` subclass
+ An rotated deep copy of the source file.
+ """
+ # TODO
+ pass
diff --git a/gerber/pcb.py b/gerber/pcb.py
new file mode 100644
index 0000000..1d22e74
--- /dev/null
+++ b/gerber/pcb.py
@@ -0,0 +1,124 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+from .exceptions import ParseError
+from .layers import PCBLayer, 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/gerber/primitives.py b/gerber/primitives.py
new file mode 100644
index 0000000..757f117
--- /dev/null
+++ b/gerber/primitives.py
@@ -0,0 +1,1697 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# 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 "<Line {} to {}>".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 "<Rectangle W {} H {} R {}>".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 '<Drill %f %s (%f, %f)>' % (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/gerber/render/__init__.py b/gerber/render/__init__.py
new file mode 100644
index 0000000..c7dbdd5
--- /dev/null
+++ b/gerber/render/__init__.py
@@ -0,0 +1,31 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+gerber.render
+============
+**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/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py
new file mode 100644
index 0000000..e1d1408
--- /dev/null
+++ b/gerber/render/cairo_backend.py
@@ -0,0 +1,616 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+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/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py
new file mode 100644
index 0000000..765d68c
--- /dev/null
+++ b/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/gerber/render/render.py b/gerber/render/render.py
new file mode 100644
index 0000000..580a7ea
--- /dev/null
+++ b/gerber/render/render.py
@@ -0,0 +1,246 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# Modified from code by Paulo Henrique Silva <ph.silva@gmail.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+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 (<float>, <float>, <float>)
+ Color used for rendering as a tuple of normalized (red, green, blue)
+ values.
+
+ drill_color : tuple (<float>, <float>, <float>)
+ Color used for rendering drill hits. Format is the same as for `color`.
+
+ background_color : tuple (<float>, <float>, <float>)
+ 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/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py
new file mode 100644
index 0000000..c7af2ea
--- /dev/null
+++ b/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/gerber/render/theme.py b/gerber/render/theme.py
new file mode 100644
index 0000000..2f558a1
--- /dev/null
+++ b/gerber/render/theme.py
@@ -0,0 +1,112 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .render import RenderSettings
+
+COLORS = {
+ 'black': (0.0, 0.0, 0.0),
+ 'white': (1.0, 1.0, 1.0),
+ 'red': (1.0, 0.0, 0.0),
+ 'green': (0.0, 1.0, 0.0),
+ '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/gerber/rs274x.py b/gerber/rs274x.py
new file mode 100644
index 0000000..afdf45f
--- /dev/null
+++ b/gerber/rs274x.py
@@ -0,0 +1,800 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+""" 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, (<float>, <float>)
+ Size in [self.units] of the layer described by the gerber file.
+
+ bounds: tuple, ((<float>, <float>), (<float>, <float>))
+ 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"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*"
+ MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
+ LP = r"(?P<param>LP)(?P<lp>(D|C))"
+ AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)"
+ AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,%]*)"
+ AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,%]*)"
+ AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,%]*)"
+ AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)".format(name=NAME)
+ AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>[^%]*)".format(name=NAME)
+ # Include File
+ IF = r"(?P<param>IF)(?P<filename>.*)"
+
+
+ # begin deprecated
+ AS = r"(?P<param>AS)(?P<mode>(AXBY)|(AYBX))"
+ IN = r"(?P<param>IN)(?P<name>.*)"
+ IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
+ IR = r"(?P<param>IR)(?P<angle>{number})".format(number=NUMBER)
+ MI = r"(?P<param>MI)(A(?P<a>0|1))?(B(?P<b>0|1))?"
+ OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
+ SF = r"(?P<param>SF)(?P<discarded>.*)"
+ LN = r"(?P<param>LN)(?P<name>.*)"
+ DEPRECATED_UNIT = re.compile(r'(?P<mode>G7[01])\*')
+ DEPRECATED_FORMAT = re.compile(r'(?P<format>G9[01])\*')
+ # end deprecated
+
+ PARAMS = (FS, MO, 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>{function})?"
+ r"(X(?P<x>{number}))?(Y(?P<y>{number}))?"
+ r"(I(?P<i>{number}))?(J(?P<j>{number}))?"
+ r"(?P<op>{op})?\*".format(number=NUMBER, function=COORD_FUNCTION, op=COORD_OP)))
+
+ APERTURE_STMT = re.compile(r"(?P<deprecated>(G54)|(G55))?D(?P<d>\d+)\*")
+
+ COMMENT_STMT = re.compile(r"G0?4(?P<comment>[^*]*)(\*)?")
+
+ EOF_STMT = re.compile(r"(?P<eof>M[0]?[012])\*")
+
+ REGION_MODE_STMT = re.compile(r'(?P<mode>G3[67])\*')
+ QUAD_MODE_STMT = re.compile(r'(?P<mode>G7[45])\*')
+
+ # 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/gerber/tests/__init__.py b/gerber/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gerber/tests/__init__.py
diff --git a/gerber/tests/golden/example_am_exposure_modifier.png b/gerber/tests/golden/example_am_exposure_modifier.png
new file mode 100644
index 0000000..dac951f
--- /dev/null
+++ b/gerber/tests/golden/example_am_exposure_modifier.png
Binary files differ
diff --git a/gerber/tests/golden/example_coincident_hole.png b/gerber/tests/golden/example_coincident_hole.png
new file mode 100644
index 0000000..9855b11
--- /dev/null
+++ b/gerber/tests/golden/example_coincident_hole.png
Binary files differ
diff --git a/gerber/tests/golden/example_cutin_multiple.png b/gerber/tests/golden/example_cutin_multiple.png
new file mode 100644
index 0000000..ebc1191
--- /dev/null
+++ b/gerber/tests/golden/example_cutin_multiple.png
Binary files differ
diff --git a/gerber/tests/golden/example_flash_circle.png b/gerber/tests/golden/example_flash_circle.png
new file mode 100644
index 0000000..0c407f6
--- /dev/null
+++ b/gerber/tests/golden/example_flash_circle.png
Binary files differ
diff --git a/gerber/tests/golden/example_flash_obround.png b/gerber/tests/golden/example_flash_obround.png
new file mode 100644
index 0000000..2fd4dc3
--- /dev/null
+++ b/gerber/tests/golden/example_flash_obround.png
Binary files differ
diff --git a/gerber/tests/golden/example_flash_polygon.png b/gerber/tests/golden/example_flash_polygon.png
new file mode 100644
index 0000000..89a964b
--- /dev/null
+++ b/gerber/tests/golden/example_flash_polygon.png
Binary files differ
diff --git a/gerber/tests/golden/example_flash_rectangle.png b/gerber/tests/golden/example_flash_rectangle.png
new file mode 100644
index 0000000..797e0c3
--- /dev/null
+++ b/gerber/tests/golden/example_flash_rectangle.png
Binary files differ
diff --git a/gerber/tests/golden/example_fully_coincident.png b/gerber/tests/golden/example_fully_coincident.png
new file mode 100644
index 0000000..4e522ff
--- /dev/null
+++ b/gerber/tests/golden/example_fully_coincident.png
Binary files differ
diff --git a/gerber/tests/golden/example_holes_dont_clear.png b/gerber/tests/golden/example_holes_dont_clear.png
new file mode 100644
index 0000000..7efb67b
--- /dev/null
+++ b/gerber/tests/golden/example_holes_dont_clear.png
Binary files differ
diff --git a/gerber/tests/golden/example_not_overlapping_contour.png b/gerber/tests/golden/example_not_overlapping_contour.png
new file mode 100644
index 0000000..4e522ff
--- /dev/null
+++ b/gerber/tests/golden/example_not_overlapping_contour.png
Binary files differ
diff --git a/gerber/tests/golden/example_not_overlapping_touching.png b/gerber/tests/golden/example_not_overlapping_touching.png
new file mode 100644
index 0000000..d485495
--- /dev/null
+++ b/gerber/tests/golden/example_not_overlapping_touching.png
Binary files differ
diff --git a/gerber/tests/golden/example_overlapping_contour.png b/gerber/tests/golden/example_overlapping_contour.png
new file mode 100644
index 0000000..7504311
--- /dev/null
+++ b/gerber/tests/golden/example_overlapping_contour.png
Binary files differ
diff --git a/gerber/tests/golden/example_overlapping_touching.png b/gerber/tests/golden/example_overlapping_touching.png
new file mode 100644
index 0000000..7504311
--- /dev/null
+++ b/gerber/tests/golden/example_overlapping_touching.png
Binary files differ
diff --git a/gerber/tests/golden/example_simple_contour.png b/gerber/tests/golden/example_simple_contour.png
new file mode 100644
index 0000000..564ae14
--- /dev/null
+++ b/gerber/tests/golden/example_simple_contour.png
Binary files differ
diff --git a/gerber/tests/golden/example_single_contour.png b/gerber/tests/golden/example_single_contour.png
new file mode 100644
index 0000000..3341638
--- /dev/null
+++ b/gerber/tests/golden/example_single_contour.png
Binary files differ
diff --git a/gerber/tests/golden/example_single_contour_3.png b/gerber/tests/golden/example_single_contour_3.png
new file mode 100644
index 0000000..1eecfee
--- /dev/null
+++ b/gerber/tests/golden/example_single_contour_3.png
Binary files differ
diff --git a/gerber/tests/golden/example_single_quadrant.gbr b/gerber/tests/golden/example_single_quadrant.gbr
new file mode 100644
index 0000000..b0a3166
--- /dev/null
+++ b/gerber/tests/golden/example_single_quadrant.gbr
@@ -0,0 +1,16 @@
+%FSLAX23Y23*%
+%MOIN*%
+%ADD10C,0.01*%
+G74*
+D10*
+%LPD*%
+G01X1100Y600D02*
+G03X700Y1000I-400J0D01*
+G03X300Y600I0J-400D01*
+G03X700Y200I400J0D01*
+G03X1100Y600I0J400D01*
+G01X300D02*
+X1100D01*
+X700Y200D02*
+Y1000D01*
+M02*
diff --git a/gerber/tests/golden/example_single_quadrant.png b/gerber/tests/golden/example_single_quadrant.png
new file mode 100644
index 0000000..89b763f
--- /dev/null
+++ b/gerber/tests/golden/example_single_quadrant.png
Binary files differ
diff --git a/gerber/tests/golden/example_two_square_boxes.gbr b/gerber/tests/golden/example_two_square_boxes.gbr
new file mode 100644
index 0000000..b5c60d1
--- /dev/null
+++ b/gerber/tests/golden/example_two_square_boxes.gbr
@@ -0,0 +1,16 @@
+%FSLAX25Y25*%
+%MOMM*%
+%ADD10C,0.01*%
+D10*
+%LPD*%
+G01X0Y0D02*
+X500000D01*
+Y500000D01*
+X0D01*
+Y0D01*
+X600000D02*
+X1100000D01*
+Y500000D01*
+X600000D01*
+Y0D01*
+M02*
diff --git a/gerber/tests/golden/example_two_square_boxes.png b/gerber/tests/golden/example_two_square_boxes.png
new file mode 100644
index 0000000..98d0518
--- /dev/null
+++ b/gerber/tests/golden/example_two_square_boxes.png
Binary files differ
diff --git a/gerber/tests/resources/board_outline.GKO b/gerber/tests/resources/board_outline.GKO
new file mode 100644
index 0000000..40b8c7d
--- /dev/null
+++ b/gerber/tests/resources/board_outline.GKO
@@ -0,0 +1,503 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239X$1,22.5*
+%
+%ADD10C,0.0000*%
+%ADD11C,0.0004*%
+D10*
+X000300Y003064D02*
+X000300Y018064D01*
+X022800Y018064D01*
+X022800Y003064D01*
+X000300Y003064D01*
+X001720Y005114D02*
+X001722Y005164D01*
+X001728Y005214D01*
+X001738Y005263D01*
+X001752Y005311D01*
+X001769Y005358D01*
+X001790Y005403D01*
+X001815Y005447D01*
+X001843Y005488D01*
+X001875Y005527D01*
+X001909Y005564D01*
+X001946Y005598D01*
+X001986Y005628D01*
+X002028Y005655D01*
+X002072Y005679D01*
+X002118Y005700D01*
+X002165Y005716D01*
+X002213Y005729D01*
+X002263Y005738D01*
+X002312Y005743D01*
+X002363Y005744D01*
+X002413Y005741D01*
+X002462Y005734D01*
+X002511Y005723D01*
+X002559Y005708D01*
+X002605Y005690D01*
+X002650Y005668D01*
+X002693Y005642D01*
+X002734Y005613D01*
+X002773Y005581D01*
+X002809Y005546D01*
+X002841Y005508D01*
+X002871Y005468D01*
+X002898Y005425D01*
+X002921Y005381D01*
+X002940Y005335D01*
+X002956Y005287D01*
+X002968Y005238D01*
+X002976Y005189D01*
+X002980Y005139D01*
+X002980Y005089D01*
+X002976Y005039D01*
+X002968Y004990D01*
+X002956Y004941D01*
+X002940Y004893D01*
+X002921Y004847D01*
+X002898Y004803D01*
+X002871Y004760D01*
+X002841Y004720D01*
+X002809Y004682D01*
+X002773Y004647D01*
+X002734Y004615D01*
+X002693Y004586D01*
+X002650Y004560D01*
+X002605Y004538D01*
+X002559Y004520D01*
+X002511Y004505D01*
+X002462Y004494D01*
+X002413Y004487D01*
+X002363Y004484D01*
+X002312Y004485D01*
+X002263Y004490D01*
+X002213Y004499D01*
+X002165Y004512D01*
+X002118Y004528D01*
+X002072Y004549D01*
+X002028Y004573D01*
+X001986Y004600D01*
+X001946Y004630D01*
+X001909Y004664D01*
+X001875Y004701D01*
+X001843Y004740D01*
+X001815Y004781D01*
+X001790Y004825D01*
+X001769Y004870D01*
+X001752Y004917D01*
+X001738Y004965D01*
+X001728Y005014D01*
+X001722Y005064D01*
+X001720Y005114D01*
+X001670Y016064D02*
+X001672Y016114D01*
+X001678Y016164D01*
+X001688Y016213D01*
+X001702Y016261D01*
+X001719Y016308D01*
+X001740Y016353D01*
+X001765Y016397D01*
+X001793Y016438D01*
+X001825Y016477D01*
+X001859Y016514D01*
+X001896Y016548D01*
+X001936Y016578D01*
+X001978Y016605D01*
+X002022Y016629D01*
+X002068Y016650D01*
+X002115Y016666D01*
+X002163Y016679D01*
+X002213Y016688D01*
+X002262Y016693D01*
+X002313Y016694D01*
+X002363Y016691D01*
+X002412Y016684D01*
+X002461Y016673D01*
+X002509Y016658D01*
+X002555Y016640D01*
+X002600Y016618D01*
+X002643Y016592D01*
+X002684Y016563D01*
+X002723Y016531D01*
+X002759Y016496D01*
+X002791Y016458D01*
+X002821Y016418D01*
+X002848Y016375D01*
+X002871Y016331D01*
+X002890Y016285D01*
+X002906Y016237D01*
+X002918Y016188D01*
+X002926Y016139D01*
+X002930Y016089D01*
+X002930Y016039D01*
+X002926Y015989D01*
+X002918Y015940D01*
+X002906Y015891D01*
+X002890Y015843D01*
+X002871Y015797D01*
+X002848Y015753D01*
+X002821Y015710D01*
+X002791Y015670D01*
+X002759Y015632D01*
+X002723Y015597D01*
+X002684Y015565D01*
+X002643Y015536D01*
+X002600Y015510D01*
+X002555Y015488D01*
+X002509Y015470D01*
+X002461Y015455D01*
+X002412Y015444D01*
+X002363Y015437D01*
+X002313Y015434D01*
+X002262Y015435D01*
+X002213Y015440D01*
+X002163Y015449D01*
+X002115Y015462D01*
+X002068Y015478D01*
+X002022Y015499D01*
+X001978Y015523D01*
+X001936Y015550D01*
+X001896Y015580D01*
+X001859Y015614D01*
+X001825Y015651D01*
+X001793Y015690D01*
+X001765Y015731D01*
+X001740Y015775D01*
+X001719Y015820D01*
+X001702Y015867D01*
+X001688Y015915D01*
+X001678Y015964D01*
+X001672Y016014D01*
+X001670Y016064D01*
+X020060Y012714D02*
+X020062Y012764D01*
+X020068Y012814D01*
+X020078Y012863D01*
+X020091Y012912D01*
+X020109Y012959D01*
+X020130Y013005D01*
+X020154Y013048D01*
+X020182Y013090D01*
+X020213Y013130D01*
+X020247Y013167D01*
+X020284Y013201D01*
+X020324Y013232D01*
+X020366Y013260D01*
+X020409Y013284D01*
+X020455Y013305D01*
+X020502Y013323D01*
+X020551Y013336D01*
+X020600Y013346D01*
+X020650Y013352D01*
+X020700Y013354D01*
+X020750Y013352D01*
+X020800Y013346D01*
+X020849Y013336D01*
+X020898Y013323D01*
+X020945Y013305D01*
+X020991Y013284D01*
+X021034Y013260D01*
+X021076Y013232D01*
+X021116Y013201D01*
+X021153Y013167D01*
+X021187Y013130D01*
+X021218Y013090D01*
+X021246Y013048D01*
+X021270Y013005D01*
+X021291Y012959D01*
+X021309Y012912D01*
+X021322Y012863D01*
+X021332Y012814D01*
+X021338Y012764D01*
+X021340Y012714D01*
+X021338Y012664D01*
+X021332Y012614D01*
+X021322Y012565D01*
+X021309Y012516D01*
+X021291Y012469D01*
+X021270Y012423D01*
+X021246Y012380D01*
+X021218Y012338D01*
+X021187Y012298D01*
+X021153Y012261D01*
+X021116Y012227D01*
+X021076Y012196D01*
+X021034Y012168D01*
+X020991Y012144D01*
+X020945Y012123D01*
+X020898Y012105D01*
+X020849Y012092D01*
+X020800Y012082D01*
+X020750Y012076D01*
+X020700Y012074D01*
+X020650Y012076D01*
+X020600Y012082D01*
+X020551Y012092D01*
+X020502Y012105D01*
+X020455Y012123D01*
+X020409Y012144D01*
+X020366Y012168D01*
+X020324Y012196D01*
+X020284Y012227D01*
+X020247Y012261D01*
+X020213Y012298D01*
+X020182Y012338D01*
+X020154Y012380D01*
+X020130Y012423D01*
+X020109Y012469D01*
+X020091Y012516D01*
+X020078Y012565D01*
+X020068Y012614D01*
+X020062Y012664D01*
+X020060Y012714D01*
+X020170Y016064D02*
+X020172Y016114D01*
+X020178Y016164D01*
+X020188Y016213D01*
+X020202Y016261D01*
+X020219Y016308D01*
+X020240Y016353D01*
+X020265Y016397D01*
+X020293Y016438D01*
+X020325Y016477D01*
+X020359Y016514D01*
+X020396Y016548D01*
+X020436Y016578D01*
+X020478Y016605D01*
+X020522Y016629D01*
+X020568Y016650D01*
+X020615Y016666D01*
+X020663Y016679D01*
+X020713Y016688D01*
+X020762Y016693D01*
+X020813Y016694D01*
+X020863Y016691D01*
+X020912Y016684D01*
+X020961Y016673D01*
+X021009Y016658D01*
+X021055Y016640D01*
+X021100Y016618D01*
+X021143Y016592D01*
+X021184Y016563D01*
+X021223Y016531D01*
+X021259Y016496D01*
+X021291Y016458D01*
+X021321Y016418D01*
+X021348Y016375D01*
+X021371Y016331D01*
+X021390Y016285D01*
+X021406Y016237D01*
+X021418Y016188D01*
+X021426Y016139D01*
+X021430Y016089D01*
+X021430Y016039D01*
+X021426Y015989D01*
+X021418Y015940D01*
+X021406Y015891D01*
+X021390Y015843D01*
+X021371Y015797D01*
+X021348Y015753D01*
+X021321Y015710D01*
+X021291Y015670D01*
+X021259Y015632D01*
+X021223Y015597D01*
+X021184Y015565D01*
+X021143Y015536D01*
+X021100Y015510D01*
+X021055Y015488D01*
+X021009Y015470D01*
+X020961Y015455D01*
+X020912Y015444D01*
+X020863Y015437D01*
+X020813Y015434D01*
+X020762Y015435D01*
+X020713Y015440D01*
+X020663Y015449D01*
+X020615Y015462D01*
+X020568Y015478D01*
+X020522Y015499D01*
+X020478Y015523D01*
+X020436Y015550D01*
+X020396Y015580D01*
+X020359Y015614D01*
+X020325Y015651D01*
+X020293Y015690D01*
+X020265Y015731D01*
+X020240Y015775D01*
+X020219Y015820D01*
+X020202Y015867D01*
+X020188Y015915D01*
+X020178Y015964D01*
+X020172Y016014D01*
+X020170Y016064D01*
+X020060Y008714D02*
+X020062Y008764D01*
+X020068Y008814D01*
+X020078Y008863D01*
+X020091Y008912D01*
+X020109Y008959D01*
+X020130Y009005D01*
+X020154Y009048D01*
+X020182Y009090D01*
+X020213Y009130D01*
+X020247Y009167D01*
+X020284Y009201D01*
+X020324Y009232D01*
+X020366Y009260D01*
+X020409Y009284D01*
+X020455Y009305D01*
+X020502Y009323D01*
+X020551Y009336D01*
+X020600Y009346D01*
+X020650Y009352D01*
+X020700Y009354D01*
+X020750Y009352D01*
+X020800Y009346D01*
+X020849Y009336D01*
+X020898Y009323D01*
+X020945Y009305D01*
+X020991Y009284D01*
+X021034Y009260D01*
+X021076Y009232D01*
+X021116Y009201D01*
+X021153Y009167D01*
+X021187Y009130D01*
+X021218Y009090D01*
+X021246Y009048D01*
+X021270Y009005D01*
+X021291Y008959D01*
+X021309Y008912D01*
+X021322Y008863D01*
+X021332Y008814D01*
+X021338Y008764D01*
+X021340Y008714D01*
+X021338Y008664D01*
+X021332Y008614D01*
+X021322Y008565D01*
+X021309Y008516D01*
+X021291Y008469D01*
+X021270Y008423D01*
+X021246Y008380D01*
+X021218Y008338D01*
+X021187Y008298D01*
+X021153Y008261D01*
+X021116Y008227D01*
+X021076Y008196D01*
+X021034Y008168D01*
+X020991Y008144D01*
+X020945Y008123D01*
+X020898Y008105D01*
+X020849Y008092D01*
+X020800Y008082D01*
+X020750Y008076D01*
+X020700Y008074D01*
+X020650Y008076D01*
+X020600Y008082D01*
+X020551Y008092D01*
+X020502Y008105D01*
+X020455Y008123D01*
+X020409Y008144D01*
+X020366Y008168D01*
+X020324Y008196D01*
+X020284Y008227D01*
+X020247Y008261D01*
+X020213Y008298D01*
+X020182Y008338D01*
+X020154Y008380D01*
+X020130Y008423D01*
+X020109Y008469D01*
+X020091Y008516D01*
+X020078Y008565D01*
+X020068Y008614D01*
+X020062Y008664D01*
+X020060Y008714D01*
+X020170Y005064D02*
+X020172Y005114D01*
+X020178Y005164D01*
+X020188Y005213D01*
+X020202Y005261D01*
+X020219Y005308D01*
+X020240Y005353D01*
+X020265Y005397D01*
+X020293Y005438D01*
+X020325Y005477D01*
+X020359Y005514D01*
+X020396Y005548D01*
+X020436Y005578D01*
+X020478Y005605D01*
+X020522Y005629D01*
+X020568Y005650D01*
+X020615Y005666D01*
+X020663Y005679D01*
+X020713Y005688D01*
+X020762Y005693D01*
+X020813Y005694D01*
+X020863Y005691D01*
+X020912Y005684D01*
+X020961Y005673D01*
+X021009Y005658D01*
+X021055Y005640D01*
+X021100Y005618D01*
+X021143Y005592D01*
+X021184Y005563D01*
+X021223Y005531D01*
+X021259Y005496D01*
+X021291Y005458D01*
+X021321Y005418D01*
+X021348Y005375D01*
+X021371Y005331D01*
+X021390Y005285D01*
+X021406Y005237D01*
+X021418Y005188D01*
+X021426Y005139D01*
+X021430Y005089D01*
+X021430Y005039D01*
+X021426Y004989D01*
+X021418Y004940D01*
+X021406Y004891D01*
+X021390Y004843D01*
+X021371Y004797D01*
+X021348Y004753D01*
+X021321Y004710D01*
+X021291Y004670D01*
+X021259Y004632D01*
+X021223Y004597D01*
+X021184Y004565D01*
+X021143Y004536D01*
+X021100Y004510D01*
+X021055Y004488D01*
+X021009Y004470D01*
+X020961Y004455D01*
+X020912Y004444D01*
+X020863Y004437D01*
+X020813Y004434D01*
+X020762Y004435D01*
+X020713Y004440D01*
+X020663Y004449D01*
+X020615Y004462D01*
+X020568Y004478D01*
+X020522Y004499D01*
+X020478Y004523D01*
+X020436Y004550D01*
+X020396Y004580D01*
+X020359Y004614D01*
+X020325Y004651D01*
+X020293Y004690D01*
+X020265Y004731D01*
+X020240Y004775D01*
+X020219Y004820D01*
+X020202Y004867D01*
+X020188Y004915D01*
+X020178Y004964D01*
+X020172Y005014D01*
+X020170Y005064D01*
+D11*
+X022869Y007639D02*
+X022869Y013789D01*
+M02*
diff --git a/gerber/tests/resources/bottom_copper.GBL b/gerber/tests/resources/bottom_copper.GBL
new file mode 100644
index 0000000..0d98da3
--- /dev/null
+++ b/gerber/tests/resources/bottom_copper.GBL
@@ -0,0 +1,1811 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239X$1,22.5*
+%
+%ADD10C,0.0000*%
+%ADD11C,0.0110*%
+%ADD12C,0.0004*%
+%ADD13C,0.0554*%
+%ADD14C,0.0600*%
+%ADD15C,0.0160*%
+%ADD16C,0.0396*%
+%ADD17C,0.0240*%
+D10*
+X000300Y003064D02*
+X000300Y018064D01*
+X022800Y018064D01*
+X022800Y003064D01*
+X000300Y003064D01*
+X001720Y005114D02*
+X001722Y005164D01*
+X001728Y005214D01*
+X001738Y005263D01*
+X001752Y005311D01*
+X001769Y005358D01*
+X001790Y005403D01*
+X001815Y005447D01*
+X001843Y005488D01*
+X001875Y005527D01*
+X001909Y005564D01*
+X001946Y005598D01*
+X001986Y005628D01*
+X002028Y005655D01*
+X002072Y005679D01*
+X002118Y005700D01*
+X002165Y005716D01*
+X002213Y005729D01*
+X002263Y005738D01*
+X002312Y005743D01*
+X002363Y005744D01*
+X002413Y005741D01*
+X002462Y005734D01*
+X002511Y005723D01*
+X002559Y005708D01*
+X002605Y005690D01*
+X002650Y005668D01*
+X002693Y005642D01*
+X002734Y005613D01*
+X002773Y005581D01*
+X002809Y005546D01*
+X002841Y005508D01*
+X002871Y005468D01*
+X002898Y005425D01*
+X002921Y005381D01*
+X002940Y005335D01*
+X002956Y005287D01*
+X002968Y005238D01*
+X002976Y005189D01*
+X002980Y005139D01*
+X002980Y005089D01*
+X002976Y005039D01*
+X002968Y004990D01*
+X002956Y004941D01*
+X002940Y004893D01*
+X002921Y004847D01*
+X002898Y004803D01*
+X002871Y004760D01*
+X002841Y004720D01*
+X002809Y004682D01*
+X002773Y004647D01*
+X002734Y004615D01*
+X002693Y004586D01*
+X002650Y004560D01*
+X002605Y004538D01*
+X002559Y004520D01*
+X002511Y004505D01*
+X002462Y004494D01*
+X002413Y004487D01*
+X002363Y004484D01*
+X002312Y004485D01*
+X002263Y004490D01*
+X002213Y004499D01*
+X002165Y004512D01*
+X002118Y004528D01*
+X002072Y004549D01*
+X002028Y004573D01*
+X001986Y004600D01*
+X001946Y004630D01*
+X001909Y004664D01*
+X001875Y004701D01*
+X001843Y004740D01*
+X001815Y004781D01*
+X001790Y004825D01*
+X001769Y004870D01*
+X001752Y004917D01*
+X001738Y004965D01*
+X001728Y005014D01*
+X001722Y005064D01*
+X001720Y005114D01*
+X001670Y016064D02*
+X001672Y016114D01*
+X001678Y016164D01*
+X001688Y016213D01*
+X001702Y016261D01*
+X001719Y016308D01*
+X001740Y016353D01*
+X001765Y016397D01*
+X001793Y016438D01*
+X001825Y016477D01*
+X001859Y016514D01*
+X001896Y016548D01*
+X001936Y016578D01*
+X001978Y016605D01*
+X002022Y016629D01*
+X002068Y016650D01*
+X002115Y016666D01*
+X002163Y016679D01*
+X002213Y016688D01*
+X002262Y016693D01*
+X002313Y016694D01*
+X002363Y016691D01*
+X002412Y016684D01*
+X002461Y016673D01*
+X002509Y016658D01*
+X002555Y016640D01*
+X002600Y016618D01*
+X002643Y016592D01*
+X002684Y016563D01*
+X002723Y016531D01*
+X002759Y016496D01*
+X002791Y016458D01*
+X002821Y016418D01*
+X002848Y016375D01*
+X002871Y016331D01*
+X002890Y016285D01*
+X002906Y016237D01*
+X002918Y016188D01*
+X002926Y016139D01*
+X002930Y016089D01*
+X002930Y016039D01*
+X002926Y015989D01*
+X002918Y015940D01*
+X002906Y015891D01*
+X002890Y015843D01*
+X002871Y015797D01*
+X002848Y015753D01*
+X002821Y015710D01*
+X002791Y015670D01*
+X002759Y015632D01*
+X002723Y015597D01*
+X002684Y015565D01*
+X002643Y015536D01*
+X002600Y015510D01*
+X002555Y015488D01*
+X002509Y015470D01*
+X002461Y015455D01*
+X002412Y015444D01*
+X002363Y015437D01*
+X002313Y015434D01*
+X002262Y015435D01*
+X002213Y015440D01*
+X002163Y015449D01*
+X002115Y015462D01*
+X002068Y015478D01*
+X002022Y015499D01*
+X001978Y015523D01*
+X001936Y015550D01*
+X001896Y015580D01*
+X001859Y015614D01*
+X001825Y015651D01*
+X001793Y015690D01*
+X001765Y015731D01*
+X001740Y015775D01*
+X001719Y015820D01*
+X001702Y015867D01*
+X001688Y015915D01*
+X001678Y015964D01*
+X001672Y016014D01*
+X001670Y016064D01*
+X020060Y012714D02*
+X020062Y012764D01*
+X020068Y012814D01*
+X020078Y012863D01*
+X020091Y012912D01*
+X020109Y012959D01*
+X020130Y013005D01*
+X020154Y013048D01*
+X020182Y013090D01*
+X020213Y013130D01*
+X020247Y013167D01*
+X020284Y013201D01*
+X020324Y013232D01*
+X020366Y013260D01*
+X020409Y013284D01*
+X020455Y013305D01*
+X020502Y013323D01*
+X020551Y013336D01*
+X020600Y013346D01*
+X020650Y013352D01*
+X020700Y013354D01*
+X020750Y013352D01*
+X020800Y013346D01*
+X020849Y013336D01*
+X020898Y013323D01*
+X020945Y013305D01*
+X020991Y013284D01*
+X021034Y013260D01*
+X021076Y013232D01*
+X021116Y013201D01*
+X021153Y013167D01*
+X021187Y013130D01*
+X021218Y013090D01*
+X021246Y013048D01*
+X021270Y013005D01*
+X021291Y012959D01*
+X021309Y012912D01*
+X021322Y012863D01*
+X021332Y012814D01*
+X021338Y012764D01*
+X021340Y012714D01*
+X021338Y012664D01*
+X021332Y012614D01*
+X021322Y012565D01*
+X021309Y012516D01*
+X021291Y012469D01*
+X021270Y012423D01*
+X021246Y012380D01*
+X021218Y012338D01*
+X021187Y012298D01*
+X021153Y012261D01*
+X021116Y012227D01*
+X021076Y012196D01*
+X021034Y012168D01*
+X020991Y012144D01*
+X020945Y012123D01*
+X020898Y012105D01*
+X020849Y012092D01*
+X020800Y012082D01*
+X020750Y012076D01*
+X020700Y012074D01*
+X020650Y012076D01*
+X020600Y012082D01*
+X020551Y012092D01*
+X020502Y012105D01*
+X020455Y012123D01*
+X020409Y012144D01*
+X020366Y012168D01*
+X020324Y012196D01*
+X020284Y012227D01*
+X020247Y012261D01*
+X020213Y012298D01*
+X020182Y012338D01*
+X020154Y012380D01*
+X020130Y012423D01*
+X020109Y012469D01*
+X020091Y012516D01*
+X020078Y012565D01*
+X020068Y012614D01*
+X020062Y012664D01*
+X020060Y012714D01*
+X020170Y016064D02*
+X020172Y016114D01*
+X020178Y016164D01*
+X020188Y016213D01*
+X020202Y016261D01*
+X020219Y016308D01*
+X020240Y016353D01*
+X020265Y016397D01*
+X020293Y016438D01*
+X020325Y016477D01*
+X020359Y016514D01*
+X020396Y016548D01*
+X020436Y016578D01*
+X020478Y016605D01*
+X020522Y016629D01*
+X020568Y016650D01*
+X020615Y016666D01*
+X020663Y016679D01*
+X020713Y016688D01*
+X020762Y016693D01*
+X020813Y016694D01*
+X020863Y016691D01*
+X020912Y016684D01*
+X020961Y016673D01*
+X021009Y016658D01*
+X021055Y016640D01*
+X021100Y016618D01*
+X021143Y016592D01*
+X021184Y016563D01*
+X021223Y016531D01*
+X021259Y016496D01*
+X021291Y016458D01*
+X021321Y016418D01*
+X021348Y016375D01*
+X021371Y016331D01*
+X021390Y016285D01*
+X021406Y016237D01*
+X021418Y016188D01*
+X021426Y016139D01*
+X021430Y016089D01*
+X021430Y016039D01*
+X021426Y015989D01*
+X021418Y015940D01*
+X021406Y015891D01*
+X021390Y015843D01*
+X021371Y015797D01*
+X021348Y015753D01*
+X021321Y015710D01*
+X021291Y015670D01*
+X021259Y015632D01*
+X021223Y015597D01*
+X021184Y015565D01*
+X021143Y015536D01*
+X021100Y015510D01*
+X021055Y015488D01*
+X021009Y015470D01*
+X020961Y015455D01*
+X020912Y015444D01*
+X020863Y015437D01*
+X020813Y015434D01*
+X020762Y015435D01*
+X020713Y015440D01*
+X020663Y015449D01*
+X020615Y015462D01*
+X020568Y015478D01*
+X020522Y015499D01*
+X020478Y015523D01*
+X020436Y015550D01*
+X020396Y015580D01*
+X020359Y015614D01*
+X020325Y015651D01*
+X020293Y015690D01*
+X020265Y015731D01*
+X020240Y015775D01*
+X020219Y015820D01*
+X020202Y015867D01*
+X020188Y015915D01*
+X020178Y015964D01*
+X020172Y016014D01*
+X020170Y016064D01*
+X020060Y008714D02*
+X020062Y008764D01*
+X020068Y008814D01*
+X020078Y008863D01*
+X020091Y008912D01*
+X020109Y008959D01*
+X020130Y009005D01*
+X020154Y009048D01*
+X020182Y009090D01*
+X020213Y009130D01*
+X020247Y009167D01*
+X020284Y009201D01*
+X020324Y009232D01*
+X020366Y009260D01*
+X020409Y009284D01*
+X020455Y009305D01*
+X020502Y009323D01*
+X020551Y009336D01*
+X020600Y009346D01*
+X020650Y009352D01*
+X020700Y009354D01*
+X020750Y009352D01*
+X020800Y009346D01*
+X020849Y009336D01*
+X020898Y009323D01*
+X020945Y009305D01*
+X020991Y009284D01*
+X021034Y009260D01*
+X021076Y009232D01*
+X021116Y009201D01*
+X021153Y009167D01*
+X021187Y009130D01*
+X021218Y009090D01*
+X021246Y009048D01*
+X021270Y009005D01*
+X021291Y008959D01*
+X021309Y008912D01*
+X021322Y008863D01*
+X021332Y008814D01*
+X021338Y008764D01*
+X021340Y008714D01*
+X021338Y008664D01*
+X021332Y008614D01*
+X021322Y008565D01*
+X021309Y008516D01*
+X021291Y008469D01*
+X021270Y008423D01*
+X021246Y008380D01*
+X021218Y008338D01*
+X021187Y008298D01*
+X021153Y008261D01*
+X021116Y008227D01*
+X021076Y008196D01*
+X021034Y008168D01*
+X020991Y008144D01*
+X020945Y008123D01*
+X020898Y008105D01*
+X020849Y008092D01*
+X020800Y008082D01*
+X020750Y008076D01*
+X020700Y008074D01*
+X020650Y008076D01*
+X020600Y008082D01*
+X020551Y008092D01*
+X020502Y008105D01*
+X020455Y008123D01*
+X020409Y008144D01*
+X020366Y008168D01*
+X020324Y008196D01*
+X020284Y008227D01*
+X020247Y008261D01*
+X020213Y008298D01*
+X020182Y008338D01*
+X020154Y008380D01*
+X020130Y008423D01*
+X020109Y008469D01*
+X020091Y008516D01*
+X020078Y008565D01*
+X020068Y008614D01*
+X020062Y008664D01*
+X020060Y008714D01*
+X020170Y005064D02*
+X020172Y005114D01*
+X020178Y005164D01*
+X020188Y005213D01*
+X020202Y005261D01*
+X020219Y005308D01*
+X020240Y005353D01*
+X020265Y005397D01*
+X020293Y005438D01*
+X020325Y005477D01*
+X020359Y005514D01*
+X020396Y005548D01*
+X020436Y005578D01*
+X020478Y005605D01*
+X020522Y005629D01*
+X020568Y005650D01*
+X020615Y005666D01*
+X020663Y005679D01*
+X020713Y005688D01*
+X020762Y005693D01*
+X020813Y005694D01*
+X020863Y005691D01*
+X020912Y005684D01*
+X020961Y005673D01*
+X021009Y005658D01*
+X021055Y005640D01*
+X021100Y005618D01*
+X021143Y005592D01*
+X021184Y005563D01*
+X021223Y005531D01*
+X021259Y005496D01*
+X021291Y005458D01*
+X021321Y005418D01*
+X021348Y005375D01*
+X021371Y005331D01*
+X021390Y005285D01*
+X021406Y005237D01*
+X021418Y005188D01*
+X021426Y005139D01*
+X021430Y005089D01*
+X021430Y005039D01*
+X021426Y004989D01*
+X021418Y004940D01*
+X021406Y004891D01*
+X021390Y004843D01*
+X021371Y004797D01*
+X021348Y004753D01*
+X021321Y004710D01*
+X021291Y004670D01*
+X021259Y004632D01*
+X021223Y004597D01*
+X021184Y004565D01*
+X021143Y004536D01*
+X021100Y004510D01*
+X021055Y004488D01*
+X021009Y004470D01*
+X020961Y004455D01*
+X020912Y004444D01*
+X020863Y004437D01*
+X020813Y004434D01*
+X020762Y004435D01*
+X020713Y004440D01*
+X020663Y004449D01*
+X020615Y004462D01*
+X020568Y004478D01*
+X020522Y004499D01*
+X020478Y004523D01*
+X020436Y004550D01*
+X020396Y004580D01*
+X020359Y004614D01*
+X020325Y004651D01*
+X020293Y004690D01*
+X020265Y004731D01*
+X020240Y004775D01*
+X020219Y004820D01*
+X020202Y004867D01*
+X020188Y004915D01*
+X020178Y004964D01*
+X020172Y005014D01*
+X020170Y005064D01*
+D11*
+X019495Y004010D02*
+X019298Y003813D01*
+X019101Y004010D01*
+X019101Y003419D01*
+X018850Y003419D02*
+X018654Y003419D01*
+X018752Y003419D02*
+X018752Y004010D01*
+X018850Y004010D02*
+X018654Y004010D01*
+X018421Y004010D02*
+X018125Y004010D01*
+X018027Y003911D01*
+X018027Y003518D01*
+X018125Y003419D01*
+X018421Y003419D01*
+X018421Y004010D01*
+X017776Y004010D02*
+X017579Y004010D01*
+X017678Y004010D02*
+X017678Y003419D01*
+X017776Y003419D02*
+X017579Y003419D01*
+X016702Y003715D02*
+X016308Y003715D01*
+X015413Y004010D02*
+X015413Y003419D01*
+X015118Y003419D01*
+X015019Y003518D01*
+X015019Y003911D01*
+X015118Y004010D01*
+X015413Y004010D01*
+X014768Y004010D02*
+X014768Y003419D01*
+X014375Y003419D02*
+X014375Y004010D01*
+X014571Y003813D01*
+X014768Y004010D01*
+X014124Y004010D02*
+X013730Y003419D01*
+X014124Y003419D02*
+X013730Y004010D01*
+X012835Y004010D02*
+X012835Y003419D01*
+X012539Y003419D01*
+X012441Y003518D01*
+X012441Y003616D01*
+X012539Y003715D01*
+X012835Y003715D01*
+X012835Y004010D02*
+X012539Y004010D01*
+X012441Y003911D01*
+X012441Y003813D01*
+X012539Y003715D01*
+X012190Y003813D02*
+X012190Y003419D01*
+X012190Y003616D02*
+X011993Y003813D01*
+X011895Y003813D01*
+X011653Y003813D02*
+X011555Y003813D01*
+X011555Y003419D01*
+X011653Y003419D02*
+X011456Y003419D01*
+X011223Y003518D02*
+X011223Y003715D01*
+X011125Y003813D01*
+X010830Y003813D01*
+X010830Y004010D02*
+X010830Y003419D01*
+X011125Y003419D01*
+X011223Y003518D01*
+X011555Y004010D02*
+X011555Y004108D01*
+X010579Y003715D02*
+X010579Y003518D01*
+X010480Y003419D01*
+X010185Y003419D01*
+X010185Y003321D02*
+X010185Y003813D01*
+X010480Y003813D01*
+X010579Y003715D01*
+X010185Y003321D02*
+X010283Y003222D01*
+X010382Y003222D01*
+X009934Y003518D02*
+X009934Y003715D01*
+X009836Y003813D01*
+X009639Y003813D01*
+X009541Y003715D01*
+X009541Y003616D01*
+X009934Y003616D01*
+X009934Y003518D02*
+X009836Y003419D01*
+X009639Y003419D01*
+X019495Y003419D02*
+X019495Y004010D01*
+D12*
+X022869Y007639D02*
+X022869Y013789D01*
+D13*
+X018200Y011964D03*
+X017200Y011464D03*
+X017200Y010464D03*
+X018200Y009964D03*
+X018200Y010964D03*
+X017200Y009464D03*
+D14*
+X017350Y016514D02*
+X017350Y017114D01*
+X018350Y017114D02*
+X018350Y016514D01*
+X007350Y016664D02*
+X007350Y017264D01*
+X006350Y017264D02*
+X006350Y016664D01*
+X005350Y016664D02*
+X005350Y017264D01*
+X001800Y012564D02*
+X001200Y012564D01*
+X001200Y011564D02*
+X001800Y011564D01*
+X001800Y010564D02*
+X001200Y010564D01*
+X001200Y009564D02*
+X001800Y009564D01*
+X001800Y008564D02*
+X001200Y008564D01*
+D15*
+X001031Y008136D02*
+X000780Y008136D01*
+X000780Y007978D02*
+X019853Y007978D01*
+X019804Y008027D02*
+X020012Y007818D01*
+X020268Y007671D01*
+X020553Y007594D01*
+X020847Y007594D01*
+X021132Y007671D01*
+X021388Y007818D01*
+X021596Y008027D01*
+X021744Y008282D01*
+X021820Y008567D01*
+X021820Y008862D01*
+X021744Y009147D01*
+X021596Y009402D01*
+X021388Y009611D01*
+X021132Y009758D01*
+X020847Y009834D01*
+X020553Y009834D01*
+X020268Y009758D01*
+X020012Y009611D01*
+X019804Y009402D01*
+X019656Y009147D01*
+X019580Y008862D01*
+X019580Y008567D01*
+X019656Y008282D01*
+X019804Y008027D01*
+X019740Y008136D02*
+X001969Y008136D01*
+X001891Y008104D02*
+X002061Y008174D01*
+X002190Y008304D01*
+X002260Y008473D01*
+X002260Y008656D01*
+X002190Y008825D01*
+X002061Y008954D01*
+X001891Y009024D01*
+X001108Y009024D01*
+X000939Y008954D01*
+X000810Y008825D01*
+X000780Y008752D01*
+X000780Y009376D01*
+X000810Y009304D01*
+X000939Y009174D01*
+X001108Y009104D01*
+X001891Y009104D01*
+X002061Y009174D01*
+X002190Y009304D01*
+X002260Y009473D01*
+X002260Y009656D01*
+X002190Y009825D01*
+X002061Y009954D01*
+X001891Y010024D01*
+X001108Y010024D01*
+X000939Y009954D01*
+X000810Y009825D01*
+X000780Y009752D01*
+X000780Y010376D01*
+X000810Y010304D01*
+X000939Y010174D01*
+X001108Y010104D01*
+X001891Y010104D01*
+X002061Y010174D01*
+X002190Y010304D01*
+X002260Y010473D01*
+X002260Y010656D01*
+X002190Y010825D01*
+X002061Y010954D01*
+X001891Y011024D01*
+X001108Y011024D01*
+X000939Y010954D01*
+X000810Y010825D01*
+X000780Y010752D01*
+X000780Y011376D01*
+X000810Y011304D01*
+X000939Y011174D01*
+X001108Y011104D01*
+X001891Y011104D01*
+X002061Y011174D01*
+X002190Y011304D01*
+X002260Y011473D01*
+X002260Y011656D01*
+X002190Y011825D01*
+X002061Y011954D01*
+X001891Y012024D01*
+X001108Y012024D01*
+X000939Y011954D01*
+X000810Y011825D01*
+X000780Y011752D01*
+X000780Y012376D01*
+X000810Y012304D01*
+X000939Y012174D01*
+X001108Y012104D01*
+X001891Y012104D01*
+X002061Y012174D01*
+X002190Y012304D01*
+X002260Y012473D01*
+X002260Y012656D01*
+X002190Y012825D01*
+X002061Y012954D01*
+X001891Y013024D01*
+X001108Y013024D01*
+X000939Y012954D01*
+X000810Y012825D01*
+X000780Y012752D01*
+X000780Y015356D01*
+X000786Y015335D01*
+X001068Y014922D01*
+X001068Y014922D01*
+X001068Y014922D01*
+X001460Y014609D01*
+X001926Y014426D01*
+X002426Y014389D01*
+X002914Y014500D01*
+X003347Y014751D01*
+X003347Y014751D01*
+X003688Y015118D01*
+X003905Y015569D01*
+X003980Y016064D01*
+X003905Y016560D01*
+X003688Y017011D01*
+X003347Y017378D01*
+X002990Y017584D01*
+X005019Y017584D01*
+X004960Y017525D01*
+X004890Y017356D01*
+X004890Y016573D01*
+X004960Y016404D01*
+X005089Y016274D01*
+X005258Y016204D01*
+X005441Y016204D01*
+X005611Y016274D01*
+X005740Y016404D01*
+X005810Y016573D01*
+X005810Y017356D01*
+X005740Y017525D01*
+X005681Y017584D01*
+X006019Y017584D01*
+X005960Y017525D01*
+X005890Y017356D01*
+X005890Y016573D01*
+X005960Y016404D01*
+X006089Y016274D01*
+X006258Y016204D01*
+X006441Y016204D01*
+X006611Y016274D01*
+X006740Y016404D01*
+X006810Y016573D01*
+X006810Y017356D01*
+X006740Y017525D01*
+X006681Y017584D01*
+X006991Y017584D01*
+X006984Y017577D01*
+X006939Y017516D01*
+X006905Y017449D01*
+X006882Y017377D01*
+X006870Y017302D01*
+X006870Y016984D01*
+X007330Y016984D01*
+X007330Y016944D01*
+X007370Y016944D01*
+X007370Y016184D01*
+X007388Y016184D01*
+X007462Y016196D01*
+X007534Y016219D01*
+X007602Y016254D01*
+X007663Y016298D01*
+X007716Y016352D01*
+X007761Y016413D01*
+X007795Y016480D01*
+X007818Y016552D01*
+X007830Y016627D01*
+X007830Y016944D01*
+X007370Y016944D01*
+X007370Y016984D01*
+X007830Y016984D01*
+X007830Y017302D01*
+X007818Y017377D01*
+X007795Y017449D01*
+X007761Y017516D01*
+X007716Y017577D01*
+X007709Y017584D01*
+X018249Y017584D01*
+X018238Y017583D01*
+X018166Y017559D01*
+X018098Y017525D01*
+X018037Y017480D01*
+X017984Y017427D01*
+X017939Y017366D01*
+X017905Y017299D01*
+X017882Y017227D01*
+X017870Y017152D01*
+X017870Y016834D01*
+X018330Y016834D01*
+X018330Y016794D01*
+X018370Y016794D01*
+X018370Y016034D01*
+X018388Y016034D01*
+X018462Y016046D01*
+X018534Y016069D01*
+X018602Y016104D01*
+X018663Y016148D01*
+X018716Y016202D01*
+X018761Y016263D01*
+X018795Y016330D01*
+X018818Y016402D01*
+X018830Y016477D01*
+X018830Y016794D01*
+X018370Y016794D01*
+X018370Y016834D01*
+X018830Y016834D01*
+X018830Y017152D01*
+X018818Y017227D01*
+X018795Y017299D01*
+X018761Y017366D01*
+X018716Y017427D01*
+X018663Y017480D01*
+X018602Y017525D01*
+X018534Y017559D01*
+X018462Y017583D01*
+X018451Y017584D01*
+X020126Y017584D01*
+X019960Y017519D01*
+X019568Y017207D01*
+X019286Y016793D01*
+X019139Y016315D01*
+X019139Y015814D01*
+X019286Y015335D01*
+X019568Y014922D01*
+X019568Y014922D01*
+X019568Y014922D01*
+X019960Y014609D01*
+X020426Y014426D01*
+X020926Y014389D01*
+X021414Y014500D01*
+X021847Y014751D01*
+X021847Y014751D01*
+X022188Y015118D01*
+X022320Y015392D01*
+X022320Y005737D01*
+X022188Y006011D01*
+X021847Y006378D01*
+X021414Y006628D01*
+X021414Y006628D01*
+X020926Y006740D01*
+X020926Y006740D01*
+X020426Y006702D01*
+X019960Y006519D01*
+X019568Y006207D01*
+X019286Y005793D01*
+X019139Y005315D01*
+X019139Y004814D01*
+X019231Y004514D01*
+X009450Y004514D01*
+X009450Y003928D01*
+X009326Y003804D01*
+X009326Y003544D01*
+X002937Y003544D01*
+X002964Y003550D01*
+X003397Y003801D01*
+X003397Y003801D01*
+X003738Y004168D01*
+X003955Y004619D01*
+X004030Y005114D01*
+X003955Y005610D01*
+X003738Y006061D01*
+X003397Y006428D01*
+X002964Y006678D01*
+X002964Y006678D01*
+X002476Y006790D01*
+X002476Y006790D01*
+X001976Y006752D01*
+X001510Y006569D01*
+X001118Y006257D01*
+X000836Y005843D01*
+X000780Y005660D01*
+X000780Y008376D01*
+X000810Y008304D01*
+X000939Y008174D01*
+X001108Y008104D01*
+X001891Y008104D01*
+X002181Y008295D02*
+X019653Y008295D01*
+X019610Y008453D02*
+X013735Y008453D01*
+X013753Y008461D02*
+X013854Y008561D01*
+X013908Y008693D01*
+X013908Y008836D01*
+X013854Y008967D01*
+X013753Y009068D01*
+X013621Y009122D01*
+X013588Y009122D01*
+X011930Y010780D01*
+X011930Y012938D01*
+X011954Y012961D01*
+X012008Y013093D01*
+X012008Y013236D01*
+X011954Y013367D01*
+X019783Y013367D01*
+X019804Y013402D02*
+X019656Y013147D01*
+X019580Y012862D01*
+X019580Y012567D01*
+X019656Y012282D01*
+X019804Y012027D01*
+X020012Y011818D01*
+X020268Y011671D01*
+X020553Y011594D01*
+X020847Y011594D01*
+X021132Y011671D01*
+X021388Y011818D01*
+X021596Y012027D01*
+X021744Y012282D01*
+X021820Y012567D01*
+X021820Y012862D01*
+X021744Y013147D01*
+X021596Y013402D01*
+X021388Y013611D01*
+X021132Y013758D01*
+X020847Y013834D01*
+X020553Y013834D01*
+X020268Y013758D01*
+X020012Y013611D01*
+X019804Y013402D01*
+X019927Y013525D02*
+X000780Y013525D01*
+X000780Y013367D02*
+X011346Y013367D01*
+X011292Y013236D01*
+X011292Y013093D01*
+X011346Y012961D01*
+X011370Y012938D01*
+X011370Y010609D01*
+X011413Y010506D01*
+X013192Y008726D01*
+X013192Y008693D01*
+X013246Y008561D01*
+X013347Y008461D01*
+X013479Y008406D01*
+X013621Y008406D01*
+X013753Y008461D01*
+X013874Y008612D02*
+X019580Y008612D01*
+X019580Y008770D02*
+X013908Y008770D01*
+X013869Y008929D02*
+X019598Y008929D01*
+X019640Y009087D02*
+X017432Y009087D01*
+X017448Y009094D02*
+X017571Y009217D01*
+X017637Y009377D01*
+X017637Y009551D01*
+X017571Y009712D01*
+X017558Y009724D01*
+X017826Y009724D01*
+X017829Y009717D01*
+X017952Y009594D01*
+X018113Y009527D01*
+X018287Y009527D01*
+X018448Y009594D01*
+X018571Y009717D01*
+X018637Y009877D01*
+X018637Y010051D01*
+X018571Y010212D01*
+X018448Y010335D01*
+X018287Y010401D01*
+X018113Y010401D01*
+X017952Y010335D01*
+X017829Y010212D01*
+X017826Y010204D01*
+X017576Y010204D01*
+X017591Y010225D01*
+X017624Y010289D01*
+X017646Y010357D01*
+X017657Y010428D01*
+X017657Y010456D01*
+X017209Y010456D01*
+X017209Y010473D01*
+X017657Y010473D01*
+X017657Y010500D01*
+X017646Y010571D01*
+X017624Y010640D01*
+X017591Y010704D01*
+X017549Y010762D01*
+X017498Y010813D01*
+X017440Y010855D01*
+X017375Y010888D01*
+X017307Y010910D01*
+X017236Y010921D01*
+X017209Y010921D01*
+X017209Y010473D01*
+X017191Y010473D01*
+X017191Y010456D01*
+X016743Y010456D01*
+X016743Y010428D01*
+X016754Y010357D01*
+X016776Y010289D01*
+X016809Y010225D01*
+X016824Y010204D01*
+X016066Y010204D01*
+X016053Y010218D01*
+X015921Y010272D01*
+X015779Y010272D01*
+X015647Y010218D01*
+X015546Y010117D01*
+X015492Y009986D01*
+X015492Y009843D01*
+X015546Y009711D01*
+X015647Y009611D01*
+X015779Y009556D01*
+X015921Y009556D01*
+X016053Y009611D01*
+X016154Y009711D01*
+X016159Y009724D01*
+X016842Y009724D01*
+X016829Y009712D01*
+X016763Y009551D01*
+X016763Y009377D01*
+X016829Y009217D01*
+X016952Y009094D01*
+X017113Y009027D01*
+X017287Y009027D01*
+X017448Y009094D01*
+X017583Y009246D02*
+X019714Y009246D01*
+X019806Y009404D02*
+X017637Y009404D01*
+X017632Y009563D02*
+X018027Y009563D01*
+X017827Y009721D02*
+X017561Y009721D01*
+X017645Y010355D02*
+X018002Y010355D01*
+X018113Y010527D02*
+X018287Y010527D01*
+X018448Y010594D01*
+X018571Y010717D01*
+X018637Y010877D01*
+X018637Y011051D01*
+X018571Y011212D01*
+X018448Y011335D01*
+X018287Y011401D01*
+X018113Y011401D01*
+X017952Y011335D01*
+X017829Y011212D01*
+X017763Y011051D01*
+X017763Y010877D01*
+X017829Y010717D01*
+X017952Y010594D01*
+X018113Y010527D01*
+X017874Y010672D02*
+X017607Y010672D01*
+X017655Y010514D02*
+X022320Y010514D01*
+X022320Y010672D02*
+X018526Y010672D01*
+X018618Y010831D02*
+X022320Y010831D01*
+X022320Y010989D02*
+X018637Y010989D01*
+X018597Y011148D02*
+X022320Y011148D01*
+X022320Y011306D02*
+X018476Y011306D01*
+X018448Y011594D02*
+X018287Y011527D01*
+X018113Y011527D01*
+X017952Y011594D01*
+X017829Y011717D01*
+X017763Y011877D01*
+X017763Y012051D01*
+X017829Y012212D01*
+X017952Y012335D01*
+X018113Y012401D01*
+X018287Y012401D01*
+X018448Y012335D01*
+X018571Y012212D01*
+X018637Y012051D01*
+X018637Y011877D01*
+X018571Y011717D01*
+X018448Y011594D01*
+X018477Y011623D02*
+X020444Y011623D01*
+X020075Y011782D02*
+X018598Y011782D01*
+X018637Y011940D02*
+X019890Y011940D01*
+X019762Y012099D02*
+X018617Y012099D01*
+X018525Y012257D02*
+X019671Y012257D01*
+X019620Y012416D02*
+X011930Y012416D01*
+X011930Y012574D02*
+X019580Y012574D01*
+X019580Y012733D02*
+X011930Y012733D01*
+X011930Y012891D02*
+X019588Y012891D01*
+X019630Y013050D02*
+X011990Y013050D01*
+X012008Y013208D02*
+X019692Y013208D01*
+X020139Y013684D02*
+X000780Y013684D01*
+X000780Y013842D02*
+X022320Y013842D01*
+X022320Y013684D02*
+X021261Y013684D01*
+X021473Y013525D02*
+X022320Y013525D01*
+X022320Y013367D02*
+X021617Y013367D01*
+X021708Y013208D02*
+X022320Y013208D01*
+X022320Y013050D02*
+X021770Y013050D01*
+X021812Y012891D02*
+X022320Y012891D01*
+X022320Y012733D02*
+X021820Y012733D01*
+X021820Y012574D02*
+X022320Y012574D01*
+X022320Y012416D02*
+X021780Y012416D01*
+X021729Y012257D02*
+X022320Y012257D01*
+X022320Y012099D02*
+X021638Y012099D01*
+X021510Y011940D02*
+X022320Y011940D01*
+X022320Y011782D02*
+X021325Y011782D01*
+X020956Y011623D02*
+X022320Y011623D01*
+X022320Y011465D02*
+X017637Y011465D01*
+X017637Y011551D02*
+X017637Y011377D01*
+X017571Y011217D01*
+X017448Y011094D01*
+X017287Y011027D01*
+X017113Y011027D01*
+X016952Y011094D01*
+X016829Y011217D01*
+X016763Y011377D01*
+X016763Y011551D01*
+X016829Y011712D01*
+X016952Y011835D01*
+X017113Y011901D01*
+X017287Y011901D01*
+X017448Y011835D01*
+X017571Y011712D01*
+X017637Y011551D01*
+X017607Y011623D02*
+X017923Y011623D01*
+X017802Y011782D02*
+X017501Y011782D01*
+X017763Y011940D02*
+X011930Y011940D01*
+X011930Y011782D02*
+X016899Y011782D01*
+X016793Y011623D02*
+X011930Y011623D01*
+X011930Y011465D02*
+X016763Y011465D01*
+X016792Y011306D02*
+X011930Y011306D01*
+X011930Y011148D02*
+X016898Y011148D01*
+X017025Y010888D02*
+X016960Y010855D01*
+X016902Y010813D01*
+X016851Y010762D01*
+X016809Y010704D01*
+X016776Y010640D01*
+X016754Y010571D01*
+X016743Y010500D01*
+X016743Y010473D01*
+X017191Y010473D01*
+X017191Y010921D01*
+X017164Y010921D01*
+X017093Y010910D01*
+X017025Y010888D01*
+X016927Y010831D02*
+X011930Y010831D01*
+X011930Y010989D02*
+X017763Y010989D01*
+X017782Y010831D02*
+X017473Y010831D01*
+X017502Y011148D02*
+X017803Y011148D01*
+X017924Y011306D02*
+X017608Y011306D01*
+X017209Y010831D02*
+X017191Y010831D01*
+X017191Y010672D02*
+X017209Y010672D01*
+X017209Y010514D02*
+X017191Y010514D01*
+X016793Y010672D02*
+X012038Y010672D01*
+X012196Y010514D02*
+X016745Y010514D01*
+X016755Y010355D02*
+X012355Y010355D01*
+X012513Y010197D02*
+X015626Y010197D01*
+X015514Y010038D02*
+X012672Y010038D01*
+X012830Y009880D02*
+X015492Y009880D01*
+X015542Y009721D02*
+X012989Y009721D01*
+X013147Y009563D02*
+X015763Y009563D01*
+X015937Y009563D02*
+X016768Y009563D01*
+X016763Y009404D02*
+X013306Y009404D01*
+X013464Y009246D02*
+X016817Y009246D01*
+X016968Y009087D02*
+X013706Y009087D01*
+X013148Y008770D02*
+X002213Y008770D01*
+X002260Y008612D02*
+X013226Y008612D01*
+X013365Y008453D02*
+X002252Y008453D01*
+X002086Y008929D02*
+X012990Y008929D01*
+X012831Y009087D02*
+X000780Y009087D01*
+X000780Y008929D02*
+X000914Y008929D01*
+X000787Y008770D02*
+X000780Y008770D01*
+X000780Y008295D02*
+X000819Y008295D01*
+X000780Y007819D02*
+X020011Y007819D01*
+X020304Y007661D02*
+X000780Y007661D01*
+X000780Y007502D02*
+X022320Y007502D01*
+X022320Y007344D02*
+X000780Y007344D01*
+X000780Y007185D02*
+X022320Y007185D01*
+X022320Y007027D02*
+X000780Y007027D01*
+X000780Y006868D02*
+X022320Y006868D01*
+X022320Y006710D02*
+X021056Y006710D01*
+X021547Y006551D02*
+X022320Y006551D01*
+X022320Y006393D02*
+X021821Y006393D01*
+X021847Y006378D02*
+X021847Y006378D01*
+X021981Y006234D02*
+X022320Y006234D01*
+X022320Y006076D02*
+X022128Y006076D01*
+X022188Y006011D02*
+X022188Y006011D01*
+X022233Y005917D02*
+X022320Y005917D01*
+X022309Y005759D02*
+X022320Y005759D01*
+X020528Y006710D02*
+X002825Y006710D01*
+X003184Y006551D02*
+X020042Y006551D01*
+X019960Y006519D02*
+X019960Y006519D01*
+X019801Y006393D02*
+X003430Y006393D01*
+X003397Y006428D02*
+X003397Y006428D01*
+X003577Y006234D02*
+X019603Y006234D01*
+X019568Y006207D02*
+X019568Y006207D01*
+X019479Y006076D02*
+X003724Y006076D01*
+X003738Y006061D02*
+X003738Y006061D01*
+X003807Y005917D02*
+X019371Y005917D01*
+X019286Y005793D02*
+X019286Y005793D01*
+X019276Y005759D02*
+X003883Y005759D01*
+X003955Y005610D02*
+X003955Y005610D01*
+X003957Y005600D02*
+X019227Y005600D01*
+X019178Y005442D02*
+X003981Y005442D01*
+X004005Y005283D02*
+X019139Y005283D01*
+X019139Y005125D02*
+X004028Y005125D01*
+X004008Y004966D02*
+X019139Y004966D01*
+X019141Y004808D02*
+X003984Y004808D01*
+X003960Y004649D02*
+X019190Y004649D01*
+X020426Y006702D02*
+X020426Y006702D01*
+X021096Y007661D02*
+X022320Y007661D01*
+X022320Y007819D02*
+X021389Y007819D01*
+X021547Y007978D02*
+X022320Y007978D01*
+X022320Y008136D02*
+X021660Y008136D01*
+X021747Y008295D02*
+X022320Y008295D01*
+X022320Y008453D02*
+X021790Y008453D01*
+X021820Y008612D02*
+X022320Y008612D01*
+X022320Y008770D02*
+X021820Y008770D01*
+X021802Y008929D02*
+X022320Y008929D01*
+X022320Y009087D02*
+X021760Y009087D01*
+X021686Y009246D02*
+X022320Y009246D01*
+X022320Y009404D02*
+X021594Y009404D01*
+X021435Y009563D02*
+X022320Y009563D01*
+X022320Y009721D02*
+X021196Y009721D01*
+X020204Y009721D02*
+X018573Y009721D01*
+X018637Y009880D02*
+X022320Y009880D01*
+X022320Y010038D02*
+X018637Y010038D01*
+X018577Y010197D02*
+X022320Y010197D01*
+X022320Y010355D02*
+X018398Y010355D01*
+X018200Y009964D02*
+X015900Y009964D01*
+X015850Y009914D01*
+X016158Y009721D02*
+X016839Y009721D01*
+X018373Y009563D02*
+X019965Y009563D01*
+X017783Y012099D02*
+X011930Y012099D01*
+X011930Y012257D02*
+X017875Y012257D01*
+X020426Y014426D02*
+X020426Y014426D01*
+X020299Y014476D02*
+X002808Y014476D01*
+X002914Y014500D02*
+X002914Y014500D01*
+X003147Y014635D02*
+X019928Y014635D01*
+X019960Y014609D02*
+X019960Y014609D01*
+X019729Y014793D02*
+X003387Y014793D01*
+X003534Y014952D02*
+X019548Y014952D01*
+X019440Y015110D02*
+X003681Y015110D01*
+X003688Y015118D02*
+X003688Y015118D01*
+X003761Y015269D02*
+X019332Y015269D01*
+X019286Y015335D02*
+X019286Y015335D01*
+X019258Y015427D02*
+X003837Y015427D01*
+X003905Y015569D02*
+X003905Y015569D01*
+X003908Y015586D02*
+X019209Y015586D01*
+X019160Y015744D02*
+X003932Y015744D01*
+X003956Y015903D02*
+X019139Y015903D01*
+X019139Y016061D02*
+X018509Y016061D01*
+X018370Y016061D02*
+X018330Y016061D01*
+X018330Y016034D02*
+X018330Y016794D01*
+X017870Y016794D01*
+X017870Y016477D01*
+X017882Y016402D01*
+X017905Y016330D01*
+X017939Y016263D01*
+X017984Y016202D01*
+X018037Y016148D01*
+X018098Y016104D01*
+X018166Y016069D01*
+X018238Y016046D01*
+X018312Y016034D01*
+X018330Y016034D01*
+X018191Y016061D02*
+X017458Y016061D01*
+X017441Y016054D02*
+X017611Y016124D01*
+X017740Y016254D01*
+X017810Y016423D01*
+X017810Y017206D01*
+X017740Y017375D01*
+X017611Y017504D01*
+X017441Y017574D01*
+X017258Y017574D01*
+X017089Y017504D01*
+X016960Y017375D01*
+X016890Y017206D01*
+X016890Y016423D01*
+X016960Y016254D01*
+X017089Y016124D01*
+X017258Y016054D01*
+X017441Y016054D01*
+X017242Y016061D02*
+X003980Y016061D01*
+X003980Y016064D02*
+X003980Y016064D01*
+X003957Y016220D02*
+X005221Y016220D01*
+X005479Y016220D02*
+X006221Y016220D01*
+X006479Y016220D02*
+X007165Y016220D01*
+X007166Y016219D02*
+X007238Y016196D01*
+X007312Y016184D01*
+X007330Y016184D01*
+X007330Y016944D01*
+X006870Y016944D01*
+X006870Y016627D01*
+X006882Y016552D01*
+X006905Y016480D01*
+X006939Y016413D01*
+X006984Y016352D01*
+X007037Y016298D01*
+X007098Y016254D01*
+X007166Y016219D01*
+X007330Y016220D02*
+X007370Y016220D01*
+X007370Y016378D02*
+X007330Y016378D01*
+X007330Y016537D02*
+X007370Y016537D01*
+X007370Y016695D02*
+X007330Y016695D01*
+X007330Y016854D02*
+X007370Y016854D01*
+X007830Y016854D02*
+X016890Y016854D01*
+X016890Y017012D02*
+X007830Y017012D01*
+X007830Y017171D02*
+X016890Y017171D01*
+X016941Y017329D02*
+X007826Y017329D01*
+X007775Y017488D02*
+X017073Y017488D01*
+X017627Y017488D02*
+X018047Y017488D01*
+X017921Y017329D02*
+X017759Y017329D01*
+X017810Y017171D02*
+X017873Y017171D01*
+X017870Y017012D02*
+X017810Y017012D01*
+X017810Y016854D02*
+X017870Y016854D01*
+X017870Y016695D02*
+X017810Y016695D01*
+X017810Y016537D02*
+X017870Y016537D01*
+X017889Y016378D02*
+X017792Y016378D01*
+X017706Y016220D02*
+X017971Y016220D01*
+X018330Y016220D02*
+X018370Y016220D01*
+X018370Y016378D02*
+X018330Y016378D01*
+X018330Y016537D02*
+X018370Y016537D01*
+X018370Y016695D02*
+X018330Y016695D01*
+X018830Y016695D02*
+X019256Y016695D01*
+X019286Y016793D02*
+X019286Y016793D01*
+X019328Y016854D02*
+X018830Y016854D01*
+X018830Y017012D02*
+X019436Y017012D01*
+X019544Y017171D02*
+X018827Y017171D01*
+X018779Y017329D02*
+X019722Y017329D01*
+X019568Y017207D02*
+X019568Y017207D01*
+X019921Y017488D02*
+X018653Y017488D01*
+X018830Y016537D02*
+X019207Y016537D01*
+X019158Y016378D02*
+X018811Y016378D01*
+X018729Y016220D02*
+X019139Y016220D01*
+X019960Y017519D02*
+X019960Y017519D01*
+X022261Y015269D02*
+X022320Y015269D01*
+X022320Y015110D02*
+X022181Y015110D01*
+X022188Y015118D02*
+X022188Y015118D01*
+X022320Y014952D02*
+X022034Y014952D01*
+X021887Y014793D02*
+X022320Y014793D01*
+X022320Y014635D02*
+X021647Y014635D01*
+X021414Y014500D02*
+X021414Y014500D01*
+X021308Y014476D02*
+X022320Y014476D01*
+X022320Y014318D02*
+X000780Y014318D01*
+X000780Y014476D02*
+X001799Y014476D01*
+X001926Y014426D02*
+X001926Y014426D01*
+X001460Y014609D02*
+X001460Y014609D01*
+X001428Y014635D02*
+X000780Y014635D01*
+X000780Y014793D02*
+X001229Y014793D01*
+X001048Y014952D02*
+X000780Y014952D01*
+X000780Y015110D02*
+X000940Y015110D01*
+X000832Y015269D02*
+X000780Y015269D01*
+X000786Y015335D02*
+X000786Y015335D01*
+X000780Y014159D02*
+X022320Y014159D01*
+X022320Y014001D02*
+X000780Y014001D01*
+X000780Y013208D02*
+X011292Y013208D01*
+X011310Y013050D02*
+X000780Y013050D01*
+X000780Y012891D02*
+X000876Y012891D01*
+X000856Y012257D02*
+X000780Y012257D01*
+X000780Y012099D02*
+X011370Y012099D01*
+X011370Y012257D02*
+X002144Y012257D01*
+X002236Y012416D02*
+X011370Y012416D01*
+X011370Y012574D02*
+X002260Y012574D01*
+X002228Y012733D02*
+X011370Y012733D01*
+X011370Y012891D02*
+X002124Y012891D01*
+X002075Y011940D02*
+X011370Y011940D01*
+X011370Y011782D02*
+X002208Y011782D01*
+X002260Y011623D02*
+X011370Y011623D01*
+X011370Y011465D02*
+X002257Y011465D01*
+X002191Y011306D02*
+X011370Y011306D01*
+X011370Y011148D02*
+X001997Y011148D01*
+X001976Y010989D02*
+X011370Y010989D01*
+X011370Y010831D02*
+X002184Y010831D01*
+X002253Y010672D02*
+X011370Y010672D01*
+X011409Y010514D02*
+X002260Y010514D01*
+X002211Y010355D02*
+X011563Y010355D01*
+X011722Y010197D02*
+X002083Y010197D01*
+X002135Y009880D02*
+X012039Y009880D01*
+X012197Y009721D02*
+X002233Y009721D01*
+X002260Y009563D02*
+X012356Y009563D01*
+X012514Y009404D02*
+X002232Y009404D01*
+X002132Y009246D02*
+X012673Y009246D01*
+X011880Y010038D02*
+X000780Y010038D01*
+X000780Y009880D02*
+X000865Y009880D01*
+X000917Y010197D02*
+X000780Y010197D01*
+X000780Y010355D02*
+X000789Y010355D01*
+X000780Y010831D02*
+X000816Y010831D01*
+X000780Y010989D02*
+X001024Y010989D01*
+X001003Y011148D02*
+X000780Y011148D01*
+X000780Y011306D02*
+X000809Y011306D01*
+X000780Y011782D02*
+X000792Y011782D01*
+X000780Y011940D02*
+X000925Y011940D01*
+X002426Y014389D02*
+X002426Y014389D01*
+X003933Y016378D02*
+X004985Y016378D01*
+X004905Y016537D02*
+X003909Y016537D01*
+X003840Y016695D02*
+X004890Y016695D01*
+X004890Y016854D02*
+X003764Y016854D01*
+X003688Y017011D02*
+X003688Y017011D01*
+X003687Y017012D02*
+X004890Y017012D01*
+X004890Y017171D02*
+X003539Y017171D01*
+X003392Y017329D02*
+X004890Y017329D01*
+X004945Y017488D02*
+X003157Y017488D01*
+X003347Y017378D02*
+X003347Y017378D01*
+X005715Y016378D02*
+X005985Y016378D01*
+X005905Y016537D02*
+X005795Y016537D01*
+X005810Y016695D02*
+X005890Y016695D01*
+X005890Y016854D02*
+X005810Y016854D01*
+X005810Y017012D02*
+X005890Y017012D01*
+X005890Y017171D02*
+X005810Y017171D01*
+X005810Y017329D02*
+X005890Y017329D01*
+X005945Y017488D02*
+X005755Y017488D01*
+X006755Y017488D02*
+X006925Y017488D01*
+X006874Y017329D02*
+X006810Y017329D01*
+X006810Y017171D02*
+X006870Y017171D01*
+X006870Y017012D02*
+X006810Y017012D01*
+X006810Y016854D02*
+X006870Y016854D01*
+X006870Y016695D02*
+X006810Y016695D01*
+X006795Y016537D02*
+X006887Y016537D01*
+X006964Y016378D02*
+X006715Y016378D01*
+X007535Y016220D02*
+X016994Y016220D01*
+X016908Y016378D02*
+X007736Y016378D01*
+X007813Y016537D02*
+X016890Y016537D01*
+X016890Y016695D02*
+X007830Y016695D01*
+X011346Y013367D02*
+X011447Y013468D01*
+X011579Y013522D01*
+X011721Y013522D01*
+X011853Y013468D01*
+X011954Y013367D01*
+X020926Y014389D02*
+X020926Y014389D01*
+X009450Y004491D02*
+X003894Y004491D01*
+X003955Y004619D02*
+X003955Y004619D01*
+X003817Y004332D02*
+X009450Y004332D01*
+X009450Y004174D02*
+X003741Y004174D01*
+X003738Y004168D02*
+X003738Y004168D01*
+X003596Y004015D02*
+X009450Y004015D01*
+X009379Y003857D02*
+X003449Y003857D01*
+X003220Y003698D02*
+X009326Y003698D01*
+X002964Y003550D02*
+X002964Y003550D01*
+X000810Y005759D02*
+X000780Y005759D01*
+X000836Y005843D02*
+X000836Y005843D01*
+X000887Y005917D02*
+X000780Y005917D01*
+X000780Y006076D02*
+X000995Y006076D01*
+X001103Y006234D02*
+X000780Y006234D01*
+X000780Y006393D02*
+X001289Y006393D01*
+X001118Y006257D02*
+X001118Y006257D01*
+X000780Y006551D02*
+X001488Y006551D01*
+X001510Y006569D02*
+X001510Y006569D01*
+X001868Y006710D02*
+X000780Y006710D01*
+X001976Y006752D02*
+X001976Y006752D01*
+X000868Y009246D02*
+X000780Y009246D01*
+D16*
+X004150Y011564D03*
+X006500Y013714D03*
+X010000Y015114D03*
+X011650Y013164D03*
+X013300Y011464D03*
+X013350Y010114D03*
+X013550Y008764D03*
+X013500Y006864D03*
+X012100Y005314D03*
+X009250Y004064D03*
+X015200Y004514D03*
+X015650Y006264D03*
+X015850Y009914D03*
+X014250Y014964D03*
+D17*
+X011650Y013164D02*
+X011650Y010664D01*
+X013550Y008764D01*
+M02*
diff --git a/gerber/tests/resources/bottom_mask.GBS b/gerber/tests/resources/bottom_mask.GBS
new file mode 100644
index 0000000..b06654f
--- /dev/null
+++ b/gerber/tests/resources/bottom_mask.GBS
@@ -0,0 +1,66 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239X$1,22.5*
+%
+%ADD10C,0.0634*%
+%ADD11C,0.1360*%
+%ADD12C,0.0680*%
+%ADD13C,0.1340*%
+%ADD14C,0.0476*%
+D10*
+X017200Y009464D03*
+X018200Y009964D03*
+X018200Y010964D03*
+X017200Y010464D03*
+X017200Y011464D03*
+X018200Y011964D03*
+D11*
+X020700Y012714D03*
+X020700Y008714D03*
+D12*
+X018350Y016514D02*
+X018350Y017114D01*
+X017350Y017114D02*
+X017350Y016514D01*
+X007350Y016664D02*
+X007350Y017264D01*
+X006350Y017264D02*
+X006350Y016664D01*
+X005350Y016664D02*
+X005350Y017264D01*
+X001800Y012564D02*
+X001200Y012564D01*
+X001200Y011564D02*
+X001800Y011564D01*
+X001800Y010564D02*
+X001200Y010564D01*
+X001200Y009564D02*
+X001800Y009564D01*
+X001800Y008564D02*
+X001200Y008564D01*
+D13*
+X002350Y005114D03*
+X002300Y016064D03*
+X020800Y016064D03*
+X020800Y005064D03*
+D14*
+X015650Y006264D03*
+X013500Y006864D03*
+X012100Y005314D03*
+X009250Y004064D03*
+X015200Y004514D03*
+X013550Y008764D03*
+X013350Y010114D03*
+X013300Y011464D03*
+X011650Y013164D03*
+X010000Y015114D03*
+X006500Y013714D03*
+X004150Y011564D03*
+X014250Y014964D03*
+X015850Y009914D03*
+M02*
diff --git a/gerber/tests/resources/bottom_silk.GBO b/gerber/tests/resources/bottom_silk.GBO
new file mode 100644
index 0000000..0e19197
--- /dev/null
+++ b/gerber/tests/resources/bottom_silk.GBO
@@ -0,0 +1,6007 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239X$1,22.5*
+%
+%ADD10C,0.0000*%
+%ADD11R,0.0470X0.0010*%
+%ADD12R,0.0560X0.0010*%
+%ADD13R,0.0570X0.0010*%
+%ADD14R,0.0580X0.0010*%
+%ADD15R,0.0300X0.0010*%
+%ADD16R,0.0450X0.0010*%
+%ADD17R,0.0670X0.0010*%
+%ADD18R,0.0510X0.0010*%
+%ADD19R,0.0760X0.0010*%
+%ADD20R,0.0520X0.0010*%
+%ADD21R,0.1900X0.0010*%
+%ADD22R,0.0820X0.0010*%
+%ADD23R,0.0880X0.0010*%
+%ADD24R,0.0530X0.0010*%
+%ADD25R,0.0940X0.0010*%
+%ADD26R,0.1000X0.0010*%
+%ADD27R,0.0540X0.0010*%
+%ADD28R,0.1050X0.0010*%
+%ADD29R,0.0550X0.0010*%
+%ADD30R,0.1100X0.0010*%
+%ADD31R,0.1140X0.0010*%
+%ADD32R,0.1180X0.0010*%
+%ADD33R,0.1220X0.0010*%
+%ADD34R,0.1260X0.0010*%
+%ADD35R,0.1300X0.0010*%
+%ADD36R,0.1320X0.0010*%
+%ADD37R,0.0590X0.0010*%
+%ADD38R,0.1360X0.0010*%
+%ADD39R,0.0600X0.0010*%
+%ADD40R,0.1400X0.0010*%
+%ADD41R,0.1420X0.0010*%
+%ADD42R,0.0610X0.0010*%
+%ADD43R,0.1460X0.0010*%
+%ADD44R,0.1480X0.0010*%
+%ADD45R,0.0620X0.0010*%
+%ADD46R,0.1500X0.0010*%
+%ADD47R,0.0630X0.0010*%
+%ADD48R,0.1540X0.0010*%
+%ADD49R,0.1560X0.0010*%
+%ADD50R,0.0640X0.0010*%
+%ADD51R,0.1580X0.0010*%
+%ADD52R,0.0650X0.0010*%
+%ADD53R,0.1600X0.0010*%
+%ADD54R,0.1640X0.0010*%
+%ADD55R,0.0660X0.0010*%
+%ADD56R,0.1660X0.0010*%
+%ADD57R,0.1680X0.0010*%
+%ADD58R,0.1700X0.0010*%
+%ADD59R,0.0680X0.0010*%
+%ADD60R,0.1720X0.0010*%
+%ADD61R,0.1740X0.0010*%
+%ADD62R,0.0690X0.0010*%
+%ADD63R,0.1760X0.0010*%
+%ADD64R,0.1780X0.0010*%
+%ADD65R,0.0700X0.0010*%
+%ADD66R,0.1800X0.0010*%
+%ADD67R,0.0710X0.0010*%
+%ADD68R,0.1820X0.0010*%
+%ADD69R,0.0720X0.0010*%
+%ADD70R,0.1840X0.0010*%
+%ADD71R,0.0730X0.0010*%
+%ADD72R,0.1860X0.0010*%
+%ADD73R,0.1880X0.0010*%
+%ADD74R,0.0740X0.0010*%
+%ADD75R,0.1920X0.0010*%
+%ADD76R,0.0750X0.0010*%
+%ADD77R,0.1940X0.0010*%
+%ADD78R,0.0860X0.0010*%
+%ADD79R,0.0850X0.0010*%
+%ADD80R,0.0810X0.0010*%
+%ADD81R,0.0770X0.0010*%
+%ADD82R,0.0790X0.0010*%
+%ADD83R,0.0780X0.0010*%
+%ADD84R,0.0800X0.0010*%
+%ADD85R,0.0830X0.0010*%
+%ADD86R,0.0840X0.0010*%
+%ADD87R,0.0870X0.0010*%
+%ADD88R,0.0890X0.0010*%
+%ADD89R,0.0900X0.0010*%
+%ADD90R,0.0910X0.0010*%
+%ADD91R,0.0920X0.0010*%
+%ADD92R,0.0930X0.0010*%
+%ADD93R,0.0950X0.0010*%
+%ADD94R,0.0960X0.0010*%
+%ADD95R,0.0970X0.0010*%
+%ADD96R,0.0980X0.0010*%
+%ADD97R,0.0990X0.0010*%
+%ADD98R,0.1010X0.0010*%
+%ADD99R,0.1020X0.0010*%
+%ADD100R,0.1030X0.0010*%
+%ADD101R,0.1040X0.0010*%
+%ADD102R,0.0480X0.0010*%
+%ADD103R,0.1990X0.0010*%
+%ADD104R,0.1850X0.0010*%
+%ADD105R,0.1620X0.0010*%
+%ADD106R,0.1570X0.0010*%
+%ADD107R,0.1550X0.0010*%
+%ADD108R,0.1520X0.0010*%
+%ADD109R,0.1490X0.0010*%
+%ADD110R,0.1470X0.0010*%
+%ADD111R,0.1430X0.0010*%
+%ADD112R,0.1410X0.0010*%
+%ADD113R,0.1380X0.0010*%
+%ADD114R,0.1350X0.0010*%
+%ADD115R,0.1310X0.0010*%
+%ADD116R,0.1280X0.0010*%
+%ADD117R,0.1250X0.0010*%
+%ADD118R,0.1210X0.0010*%
+%ADD119R,0.1170X0.0010*%
+%ADD120R,0.1120X0.0010*%
+%ADD121R,0.1080X0.0010*%
+%ADD122R,0.0500X0.0010*%
+%ADD123R,0.0370X0.0010*%
+%ADD124R,0.0070X0.0010*%
+%ADD125R,0.2950X0.0010*%
+%ADD126R,0.0490X0.0010*%
+%ADD127R,0.1290X0.0010*%
+%ADD128R,0.1610X0.0010*%
+%ADD129R,0.1690X0.0010*%
+%ADD130R,0.1710X0.0010*%
+%ADD131R,0.1730X0.0010*%
+%ADD132R,0.1750X0.0010*%
+%ADD133R,0.1810X0.0010*%
+%ADD134R,0.1830X0.0010*%
+%ADD135R,0.1870X0.0010*%
+%ADD136R,0.1890X0.0010*%
+%ADD137R,0.1910X0.0010*%
+%ADD138R,0.1930X0.0010*%
+%ADD139R,0.1950X0.0010*%
+%ADD140R,0.1960X0.0010*%
+%ADD141R,0.1970X0.0010*%
+%ADD142R,0.1980X0.0010*%
+%ADD143R,0.2000X0.0010*%
+%ADD144R,0.2010X0.0010*%
+%ADD145R,0.2020X0.0010*%
+%ADD146R,0.2060X0.0010*%
+%ADD147R,0.2050X0.0010*%
+%ADD148R,0.2030X0.0010*%
+%ADD149R,0.1790X0.0010*%
+%ADD150R,0.1770X0.0010*%
+%ADD151R,0.1450X0.0010*%
+%ADD152R,0.1440X0.0010*%
+%ADD153R,0.1670X0.0010*%
+%ADD154R,0.1650X0.0010*%
+%ADD155R,0.1630X0.0010*%
+%ADD156R,0.1390X0.0010*%
+%ADD157R,0.1370X0.0010*%
+%ADD158R,0.3140X0.0010*%
+%ADD159R,0.1240X0.0010*%
+%ADD160C,0.0004*%
+D10*
+X000303Y003014D02*
+X000310Y003014D01*
+X000313Y003018D01*
+X000318Y003018D02*
+X000318Y003014D01*
+X000322Y003014D01*
+X000322Y003018D01*
+X000318Y003018D01*
+X000318Y003024D02*
+X000318Y003028D01*
+X000322Y003028D01*
+X000322Y003024D01*
+X000318Y003024D01*
+X000313Y003031D02*
+X000310Y003034D01*
+X000303Y003034D01*
+X000300Y003031D01*
+X000300Y003018D01*
+X000303Y003014D01*
+X000328Y003014D02*
+X000341Y003034D01*
+X000346Y003034D02*
+X000346Y003018D01*
+X000349Y003014D01*
+X000356Y003014D01*
+X000359Y003018D01*
+X000359Y003034D01*
+X000368Y003028D02*
+X000378Y003028D01*
+X000383Y003024D02*
+X000386Y003028D01*
+X000393Y003028D01*
+X000396Y003024D01*
+X000396Y003021D01*
+X000383Y003021D01*
+X000383Y003018D02*
+X000383Y003024D01*
+X000383Y003018D02*
+X000386Y003014D01*
+X000393Y003014D01*
+X000401Y003014D02*
+X000401Y003028D01*
+X000408Y003028D02*
+X000411Y003028D01*
+X000408Y003028D02*
+X000401Y003021D01*
+X000417Y003024D02*
+X000420Y003028D01*
+X000430Y003028D01*
+X000427Y003021D02*
+X000420Y003021D01*
+X000417Y003024D01*
+X000417Y003014D02*
+X000427Y003014D01*
+X000430Y003018D01*
+X000427Y003021D01*
+X000435Y003014D02*
+X000448Y003034D01*
+X000453Y003034D02*
+X000453Y003014D01*
+X000453Y003024D02*
+X000457Y003028D01*
+X000463Y003028D01*
+X000467Y003024D01*
+X000467Y003014D01*
+X000472Y003018D02*
+X000475Y003021D01*
+X000485Y003021D01*
+X000485Y003024D02*
+X000485Y003014D01*
+X000475Y003014D01*
+X000472Y003018D01*
+X000475Y003028D02*
+X000482Y003028D01*
+X000485Y003024D01*
+X000490Y003028D02*
+X000494Y003028D01*
+X000497Y003024D01*
+X000500Y003028D01*
+X000504Y003024D01*
+X000504Y003014D01*
+X000509Y003014D02*
+X000515Y003014D01*
+X000512Y003014D02*
+X000512Y003028D01*
+X000509Y003028D01*
+X000512Y003034D02*
+X000512Y003038D01*
+X000521Y003034D02*
+X000524Y003034D01*
+X000524Y003014D01*
+X000521Y003014D02*
+X000528Y003014D01*
+X000537Y003018D02*
+X000540Y003014D01*
+X000537Y003018D02*
+X000537Y003031D01*
+X000540Y003028D02*
+X000533Y003028D01*
+X000546Y003024D02*
+X000546Y003018D01*
+X000549Y003014D01*
+X000556Y003014D01*
+X000559Y003018D01*
+X000559Y003024D01*
+X000556Y003028D01*
+X000549Y003028D01*
+X000546Y003024D01*
+X000564Y003028D02*
+X000574Y003028D01*
+X000577Y003024D01*
+X000577Y003014D01*
+X000582Y003014D02*
+X000586Y003014D01*
+X000586Y003018D01*
+X000582Y003018D01*
+X000582Y003014D01*
+X000592Y003014D02*
+X000592Y003034D01*
+X000602Y003028D02*
+X000592Y003021D01*
+X000602Y003014D01*
+X000607Y003014D02*
+X000614Y003014D01*
+X000610Y003014D02*
+X000610Y003028D01*
+X000607Y003028D01*
+X000610Y003034D02*
+X000610Y003038D01*
+X000619Y003034D02*
+X000619Y003014D01*
+X000629Y003014D01*
+X000633Y003018D01*
+X000633Y003024D01*
+X000629Y003028D01*
+X000619Y003028D01*
+X000638Y003028D02*
+X000648Y003028D01*
+X000651Y003024D01*
+X000651Y003018D01*
+X000648Y003014D01*
+X000638Y003014D01*
+X000638Y003034D01*
+X000656Y003024D02*
+X000659Y003028D01*
+X000666Y003028D01*
+X000669Y003024D01*
+X000669Y003021D01*
+X000656Y003021D01*
+X000656Y003018D02*
+X000656Y003024D01*
+X000656Y003018D02*
+X000659Y003014D01*
+X000666Y003014D01*
+X000674Y003014D02*
+X000688Y003034D01*
+X000693Y003034D02*
+X000703Y003034D01*
+X000706Y003031D01*
+X000706Y003018D01*
+X000703Y003014D01*
+X000693Y003014D01*
+X000693Y003034D01*
+X000711Y003024D02*
+X000715Y003028D01*
+X000721Y003028D01*
+X000725Y003024D01*
+X000725Y003021D01*
+X000711Y003021D01*
+X000711Y003018D02*
+X000711Y003024D01*
+X000711Y003018D02*
+X000715Y003014D01*
+X000721Y003014D01*
+X000730Y003014D02*
+X000740Y003014D01*
+X000743Y003018D01*
+X000740Y003021D01*
+X000733Y003021D01*
+X000730Y003024D01*
+X000733Y003028D01*
+X000743Y003028D01*
+X000748Y003034D02*
+X000748Y003014D01*
+X000748Y003021D02*
+X000758Y003028D01*
+X000763Y003028D02*
+X000770Y003028D01*
+X000767Y003031D02*
+X000767Y003018D01*
+X000770Y003014D01*
+X000776Y003018D02*
+X000779Y003014D01*
+X000786Y003014D01*
+X000789Y003018D01*
+X000789Y003024D01*
+X000786Y003028D01*
+X000779Y003028D01*
+X000776Y003024D01*
+X000776Y003018D01*
+X000758Y003014D02*
+X000748Y003021D01*
+X000794Y003014D02*
+X000804Y003014D01*
+X000807Y003018D01*
+X000807Y003024D01*
+X000804Y003028D01*
+X000794Y003028D01*
+X000794Y003008D01*
+X000813Y003014D02*
+X000826Y003034D01*
+X000831Y003034D02*
+X000831Y003014D01*
+X000841Y003014D01*
+X000844Y003018D01*
+X000844Y003024D01*
+X000841Y003028D01*
+X000831Y003028D01*
+X000849Y003028D02*
+X000856Y003028D01*
+X000853Y003031D02*
+X000853Y003018D01*
+X000856Y003014D01*
+X000865Y003014D02*
+X000865Y003031D01*
+X000868Y003034D01*
+X000874Y003028D02*
+X000887Y003014D01*
+X000892Y003014D02*
+X000896Y003014D01*
+X000896Y003018D01*
+X000892Y003018D01*
+X000892Y003014D01*
+X000902Y003014D02*
+X000912Y003014D01*
+X000915Y003018D01*
+X000915Y003021D01*
+X000912Y003024D01*
+X000902Y003024D01*
+X000912Y003024D02*
+X000915Y003028D01*
+X000915Y003031D01*
+X000912Y003034D01*
+X000902Y003034D01*
+X000902Y003014D01*
+X000920Y003014D02*
+X000920Y003034D01*
+X000927Y003028D01*
+X000933Y003034D01*
+X000933Y003014D01*
+X000938Y003014D02*
+X000938Y003034D01*
+X000948Y003034D01*
+X000952Y003031D01*
+X000952Y003024D01*
+X000948Y003021D01*
+X000938Y003021D01*
+X000887Y003028D02*
+X000874Y003014D01*
+X000868Y003024D02*
+X000862Y003024D01*
+X000564Y003014D02*
+X000564Y003028D01*
+X000497Y003024D02*
+X000497Y003014D01*
+X000490Y003014D02*
+X000490Y003028D01*
+X000378Y003018D02*
+X000374Y003021D01*
+X000368Y003021D01*
+X000364Y003024D01*
+X000368Y003028D01*
+X000364Y003014D02*
+X000374Y003014D01*
+X000378Y003018D01*
+X000300Y003064D02*
+X000300Y018064D01*
+X022800Y018064D01*
+X022800Y003064D01*
+X000300Y003064D01*
+X001720Y005114D02*
+X001722Y005164D01*
+X001728Y005214D01*
+X001738Y005263D01*
+X001752Y005311D01*
+X001769Y005358D01*
+X001790Y005403D01*
+X001815Y005447D01*
+X001843Y005488D01*
+X001875Y005527D01*
+X001909Y005564D01*
+X001946Y005598D01*
+X001986Y005628D01*
+X002028Y005655D01*
+X002072Y005679D01*
+X002118Y005700D01*
+X002165Y005716D01*
+X002213Y005729D01*
+X002263Y005738D01*
+X002312Y005743D01*
+X002363Y005744D01*
+X002413Y005741D01*
+X002462Y005734D01*
+X002511Y005723D01*
+X002559Y005708D01*
+X002605Y005690D01*
+X002650Y005668D01*
+X002693Y005642D01*
+X002734Y005613D01*
+X002773Y005581D01*
+X002809Y005546D01*
+X002841Y005508D01*
+X002871Y005468D01*
+X002898Y005425D01*
+X002921Y005381D01*
+X002940Y005335D01*
+X002956Y005287D01*
+X002968Y005238D01*
+X002976Y005189D01*
+X002980Y005139D01*
+X002980Y005089D01*
+X002976Y005039D01*
+X002968Y004990D01*
+X002956Y004941D01*
+X002940Y004893D01*
+X002921Y004847D01*
+X002898Y004803D01*
+X002871Y004760D01*
+X002841Y004720D01*
+X002809Y004682D01*
+X002773Y004647D01*
+X002734Y004615D01*
+X002693Y004586D01*
+X002650Y004560D01*
+X002605Y004538D01*
+X002559Y004520D01*
+X002511Y004505D01*
+X002462Y004494D01*
+X002413Y004487D01*
+X002363Y004484D01*
+X002312Y004485D01*
+X002263Y004490D01*
+X002213Y004499D01*
+X002165Y004512D01*
+X002118Y004528D01*
+X002072Y004549D01*
+X002028Y004573D01*
+X001986Y004600D01*
+X001946Y004630D01*
+X001909Y004664D01*
+X001875Y004701D01*
+X001843Y004740D01*
+X001815Y004781D01*
+X001790Y004825D01*
+X001769Y004870D01*
+X001752Y004917D01*
+X001738Y004965D01*
+X001728Y005014D01*
+X001722Y005064D01*
+X001720Y005114D01*
+X001670Y016064D02*
+X001672Y016114D01*
+X001678Y016164D01*
+X001688Y016213D01*
+X001702Y016261D01*
+X001719Y016308D01*
+X001740Y016353D01*
+X001765Y016397D01*
+X001793Y016438D01*
+X001825Y016477D01*
+X001859Y016514D01*
+X001896Y016548D01*
+X001936Y016578D01*
+X001978Y016605D01*
+X002022Y016629D01*
+X002068Y016650D01*
+X002115Y016666D01*
+X002163Y016679D01*
+X002213Y016688D01*
+X002262Y016693D01*
+X002313Y016694D01*
+X002363Y016691D01*
+X002412Y016684D01*
+X002461Y016673D01*
+X002509Y016658D01*
+X002555Y016640D01*
+X002600Y016618D01*
+X002643Y016592D01*
+X002684Y016563D01*
+X002723Y016531D01*
+X002759Y016496D01*
+X002791Y016458D01*
+X002821Y016418D01*
+X002848Y016375D01*
+X002871Y016331D01*
+X002890Y016285D01*
+X002906Y016237D01*
+X002918Y016188D01*
+X002926Y016139D01*
+X002930Y016089D01*
+X002930Y016039D01*
+X002926Y015989D01*
+X002918Y015940D01*
+X002906Y015891D01*
+X002890Y015843D01*
+X002871Y015797D01*
+X002848Y015753D01*
+X002821Y015710D01*
+X002791Y015670D01*
+X002759Y015632D01*
+X002723Y015597D01*
+X002684Y015565D01*
+X002643Y015536D01*
+X002600Y015510D01*
+X002555Y015488D01*
+X002509Y015470D01*
+X002461Y015455D01*
+X002412Y015444D01*
+X002363Y015437D01*
+X002313Y015434D01*
+X002262Y015435D01*
+X002213Y015440D01*
+X002163Y015449D01*
+X002115Y015462D01*
+X002068Y015478D01*
+X002022Y015499D01*
+X001978Y015523D01*
+X001936Y015550D01*
+X001896Y015580D01*
+X001859Y015614D01*
+X001825Y015651D01*
+X001793Y015690D01*
+X001765Y015731D01*
+X001740Y015775D01*
+X001719Y015820D01*
+X001702Y015867D01*
+X001688Y015915D01*
+X001678Y015964D01*
+X001672Y016014D01*
+X001670Y016064D01*
+X020060Y012714D02*
+X020062Y012764D01*
+X020068Y012814D01*
+X020078Y012863D01*
+X020091Y012912D01*
+X020109Y012959D01*
+X020130Y013005D01*
+X020154Y013048D01*
+X020182Y013090D01*
+X020213Y013130D01*
+X020247Y013167D01*
+X020284Y013201D01*
+X020324Y013232D01*
+X020366Y013260D01*
+X020409Y013284D01*
+X020455Y013305D01*
+X020502Y013323D01*
+X020551Y013336D01*
+X020600Y013346D01*
+X020650Y013352D01*
+X020700Y013354D01*
+X020750Y013352D01*
+X020800Y013346D01*
+X020849Y013336D01*
+X020898Y013323D01*
+X020945Y013305D01*
+X020991Y013284D01*
+X021034Y013260D01*
+X021076Y013232D01*
+X021116Y013201D01*
+X021153Y013167D01*
+X021187Y013130D01*
+X021218Y013090D01*
+X021246Y013048D01*
+X021270Y013005D01*
+X021291Y012959D01*
+X021309Y012912D01*
+X021322Y012863D01*
+X021332Y012814D01*
+X021338Y012764D01*
+X021340Y012714D01*
+X021338Y012664D01*
+X021332Y012614D01*
+X021322Y012565D01*
+X021309Y012516D01*
+X021291Y012469D01*
+X021270Y012423D01*
+X021246Y012380D01*
+X021218Y012338D01*
+X021187Y012298D01*
+X021153Y012261D01*
+X021116Y012227D01*
+X021076Y012196D01*
+X021034Y012168D01*
+X020991Y012144D01*
+X020945Y012123D01*
+X020898Y012105D01*
+X020849Y012092D01*
+X020800Y012082D01*
+X020750Y012076D01*
+X020700Y012074D01*
+X020650Y012076D01*
+X020600Y012082D01*
+X020551Y012092D01*
+X020502Y012105D01*
+X020455Y012123D01*
+X020409Y012144D01*
+X020366Y012168D01*
+X020324Y012196D01*
+X020284Y012227D01*
+X020247Y012261D01*
+X020213Y012298D01*
+X020182Y012338D01*
+X020154Y012380D01*
+X020130Y012423D01*
+X020109Y012469D01*
+X020091Y012516D01*
+X020078Y012565D01*
+X020068Y012614D01*
+X020062Y012664D01*
+X020060Y012714D01*
+X020170Y016064D02*
+X020172Y016114D01*
+X020178Y016164D01*
+X020188Y016213D01*
+X020202Y016261D01*
+X020219Y016308D01*
+X020240Y016353D01*
+X020265Y016397D01*
+X020293Y016438D01*
+X020325Y016477D01*
+X020359Y016514D01*
+X020396Y016548D01*
+X020436Y016578D01*
+X020478Y016605D01*
+X020522Y016629D01*
+X020568Y016650D01*
+X020615Y016666D01*
+X020663Y016679D01*
+X020713Y016688D01*
+X020762Y016693D01*
+X020813Y016694D01*
+X020863Y016691D01*
+X020912Y016684D01*
+X020961Y016673D01*
+X021009Y016658D01*
+X021055Y016640D01*
+X021100Y016618D01*
+X021143Y016592D01*
+X021184Y016563D01*
+X021223Y016531D01*
+X021259Y016496D01*
+X021291Y016458D01*
+X021321Y016418D01*
+X021348Y016375D01*
+X021371Y016331D01*
+X021390Y016285D01*
+X021406Y016237D01*
+X021418Y016188D01*
+X021426Y016139D01*
+X021430Y016089D01*
+X021430Y016039D01*
+X021426Y015989D01*
+X021418Y015940D01*
+X021406Y015891D01*
+X021390Y015843D01*
+X021371Y015797D01*
+X021348Y015753D01*
+X021321Y015710D01*
+X021291Y015670D01*
+X021259Y015632D01*
+X021223Y015597D01*
+X021184Y015565D01*
+X021143Y015536D01*
+X021100Y015510D01*
+X021055Y015488D01*
+X021009Y015470D01*
+X020961Y015455D01*
+X020912Y015444D01*
+X020863Y015437D01*
+X020813Y015434D01*
+X020762Y015435D01*
+X020713Y015440D01*
+X020663Y015449D01*
+X020615Y015462D01*
+X020568Y015478D01*
+X020522Y015499D01*
+X020478Y015523D01*
+X020436Y015550D01*
+X020396Y015580D01*
+X020359Y015614D01*
+X020325Y015651D01*
+X020293Y015690D01*
+X020265Y015731D01*
+X020240Y015775D01*
+X020219Y015820D01*
+X020202Y015867D01*
+X020188Y015915D01*
+X020178Y015964D01*
+X020172Y016014D01*
+X020170Y016064D01*
+X020060Y008714D02*
+X020062Y008764D01*
+X020068Y008814D01*
+X020078Y008863D01*
+X020091Y008912D01*
+X020109Y008959D01*
+X020130Y009005D01*
+X020154Y009048D01*
+X020182Y009090D01*
+X020213Y009130D01*
+X020247Y009167D01*
+X020284Y009201D01*
+X020324Y009232D01*
+X020366Y009260D01*
+X020409Y009284D01*
+X020455Y009305D01*
+X020502Y009323D01*
+X020551Y009336D01*
+X020600Y009346D01*
+X020650Y009352D01*
+X020700Y009354D01*
+X020750Y009352D01*
+X020800Y009346D01*
+X020849Y009336D01*
+X020898Y009323D01*
+X020945Y009305D01*
+X020991Y009284D01*
+X021034Y009260D01*
+X021076Y009232D01*
+X021116Y009201D01*
+X021153Y009167D01*
+X021187Y009130D01*
+X021218Y009090D01*
+X021246Y009048D01*
+X021270Y009005D01*
+X021291Y008959D01*
+X021309Y008912D01*
+X021322Y008863D01*
+X021332Y008814D01*
+X021338Y008764D01*
+X021340Y008714D01*
+X021338Y008664D01*
+X021332Y008614D01*
+X021322Y008565D01*
+X021309Y008516D01*
+X021291Y008469D01*
+X021270Y008423D01*
+X021246Y008380D01*
+X021218Y008338D01*
+X021187Y008298D01*
+X021153Y008261D01*
+X021116Y008227D01*
+X021076Y008196D01*
+X021034Y008168D01*
+X020991Y008144D01*
+X020945Y008123D01*
+X020898Y008105D01*
+X020849Y008092D01*
+X020800Y008082D01*
+X020750Y008076D01*
+X020700Y008074D01*
+X020650Y008076D01*
+X020600Y008082D01*
+X020551Y008092D01*
+X020502Y008105D01*
+X020455Y008123D01*
+X020409Y008144D01*
+X020366Y008168D01*
+X020324Y008196D01*
+X020284Y008227D01*
+X020247Y008261D01*
+X020213Y008298D01*
+X020182Y008338D01*
+X020154Y008380D01*
+X020130Y008423D01*
+X020109Y008469D01*
+X020091Y008516D01*
+X020078Y008565D01*
+X020068Y008614D01*
+X020062Y008664D01*
+X020060Y008714D01*
+X020170Y005064D02*
+X020172Y005114D01*
+X020178Y005164D01*
+X020188Y005213D01*
+X020202Y005261D01*
+X020219Y005308D01*
+X020240Y005353D01*
+X020265Y005397D01*
+X020293Y005438D01*
+X020325Y005477D01*
+X020359Y005514D01*
+X020396Y005548D01*
+X020436Y005578D01*
+X020478Y005605D01*
+X020522Y005629D01*
+X020568Y005650D01*
+X020615Y005666D01*
+X020663Y005679D01*
+X020713Y005688D01*
+X020762Y005693D01*
+X020813Y005694D01*
+X020863Y005691D01*
+X020912Y005684D01*
+X020961Y005673D01*
+X021009Y005658D01*
+X021055Y005640D01*
+X021100Y005618D01*
+X021143Y005592D01*
+X021184Y005563D01*
+X021223Y005531D01*
+X021259Y005496D01*
+X021291Y005458D01*
+X021321Y005418D01*
+X021348Y005375D01*
+X021371Y005331D01*
+X021390Y005285D01*
+X021406Y005237D01*
+X021418Y005188D01*
+X021426Y005139D01*
+X021430Y005089D01*
+X021430Y005039D01*
+X021426Y004989D01*
+X021418Y004940D01*
+X021406Y004891D01*
+X021390Y004843D01*
+X021371Y004797D01*
+X021348Y004753D01*
+X021321Y004710D01*
+X021291Y004670D01*
+X021259Y004632D01*
+X021223Y004597D01*
+X021184Y004565D01*
+X021143Y004536D01*
+X021100Y004510D01*
+X021055Y004488D01*
+X021009Y004470D01*
+X020961Y004455D01*
+X020912Y004444D01*
+X020863Y004437D01*
+X020813Y004434D01*
+X020762Y004435D01*
+X020713Y004440D01*
+X020663Y004449D01*
+X020615Y004462D01*
+X020568Y004478D01*
+X020522Y004499D01*
+X020478Y004523D01*
+X020436Y004550D01*
+X020396Y004580D01*
+X020359Y004614D01*
+X020325Y004651D01*
+X020293Y004690D01*
+X020265Y004731D01*
+X020240Y004775D01*
+X020219Y004820D01*
+X020202Y004867D01*
+X020188Y004915D01*
+X020178Y004964D01*
+X020172Y005014D01*
+X020170Y005064D01*
+D11*
+X015810Y007044D03*
+X015810Y007054D03*
+X015810Y007064D03*
+X015810Y007074D03*
+X015810Y007084D03*
+X015810Y007094D03*
+X015810Y007104D03*
+X015810Y007114D03*
+X015810Y007124D03*
+X015810Y007134D03*
+X015810Y007144D03*
+X015810Y007154D03*
+X015810Y007164D03*
+X015810Y007174D03*
+X015810Y007184D03*
+X015810Y007194D03*
+X015810Y007204D03*
+X015810Y007214D03*
+X015810Y007224D03*
+X015810Y007234D03*
+X015810Y007244D03*
+X015810Y007254D03*
+X015810Y007264D03*
+X015810Y007274D03*
+X015810Y007284D03*
+X015810Y007294D03*
+X015810Y007304D03*
+X015810Y007314D03*
+X015810Y007324D03*
+X015810Y007334D03*
+X015810Y007344D03*
+X015810Y007354D03*
+X015810Y007364D03*
+X015810Y007374D03*
+X015810Y007384D03*
+X015810Y007394D03*
+X015810Y007404D03*
+X015810Y007414D03*
+X015810Y007424D03*
+X015810Y007434D03*
+X015810Y007444D03*
+X015810Y007454D03*
+X015810Y007464D03*
+X015810Y007474D03*
+X015810Y007484D03*
+X015810Y007494D03*
+X015810Y007504D03*
+X015810Y007514D03*
+X015810Y007524D03*
+X015810Y007534D03*
+X015810Y007544D03*
+X015810Y007554D03*
+X015810Y007564D03*
+X015810Y007574D03*
+X015810Y007584D03*
+X015810Y007594D03*
+X015810Y007604D03*
+X015810Y007614D03*
+X015810Y007624D03*
+X015810Y007634D03*
+X015810Y007644D03*
+X015810Y007654D03*
+X015810Y007664D03*
+X015810Y007674D03*
+X015810Y007684D03*
+X015810Y007694D03*
+X015810Y007704D03*
+X015810Y007714D03*
+X015810Y007724D03*
+X015810Y007734D03*
+X015810Y007744D03*
+X015810Y007754D03*
+X015810Y007764D03*
+X015810Y007774D03*
+X015810Y007784D03*
+X015810Y007794D03*
+X015810Y007804D03*
+X015810Y007814D03*
+X015810Y007824D03*
+X015810Y007834D03*
+X015810Y007844D03*
+X015810Y007854D03*
+X015810Y007864D03*
+X015810Y007874D03*
+X015810Y007884D03*
+X015810Y007894D03*
+X015810Y007904D03*
+X015810Y007914D03*
+X015810Y007924D03*
+X015810Y007934D03*
+X015810Y007944D03*
+X015810Y007954D03*
+X015810Y007964D03*
+X015810Y007974D03*
+X015810Y007984D03*
+X015810Y007994D03*
+X015810Y008004D03*
+X015810Y008014D03*
+X015810Y008024D03*
+X015810Y008034D03*
+X015810Y008044D03*
+X015810Y008054D03*
+X015810Y008064D03*
+X015810Y008074D03*
+X015810Y008084D03*
+X015810Y008094D03*
+X015810Y008104D03*
+X015810Y008114D03*
+X015810Y008124D03*
+X015810Y008134D03*
+X015810Y008144D03*
+X015810Y008154D03*
+X015810Y008164D03*
+X015810Y008174D03*
+X015810Y008184D03*
+X015810Y008194D03*
+X015810Y008204D03*
+X015810Y008214D03*
+X015810Y008224D03*
+X015810Y008234D03*
+X015810Y008244D03*
+X015810Y008254D03*
+X015810Y008264D03*
+X015810Y008274D03*
+X015810Y008284D03*
+X015810Y008294D03*
+X015810Y008304D03*
+X015810Y008314D03*
+X015810Y008324D03*
+X015810Y008334D03*
+X015810Y008344D03*
+X015810Y008354D03*
+X015810Y008364D03*
+X015810Y008374D03*
+X015810Y008384D03*
+X015810Y008394D03*
+X015810Y008404D03*
+X015810Y008414D03*
+X015810Y008424D03*
+X015810Y008434D03*
+X015810Y008444D03*
+X015810Y008454D03*
+X015810Y008464D03*
+X015810Y008474D03*
+X015810Y008484D03*
+X015810Y008494D03*
+X015810Y008504D03*
+X015810Y008514D03*
+X015810Y008524D03*
+X015810Y008534D03*
+X015810Y008544D03*
+X015810Y008554D03*
+X015810Y008564D03*
+X015810Y008574D03*
+X015810Y008584D03*
+X015810Y008594D03*
+X015810Y008604D03*
+X015810Y008614D03*
+X015810Y008624D03*
+X015810Y008634D03*
+X010930Y008634D03*
+X010930Y008624D03*
+X010930Y008614D03*
+X010930Y008604D03*
+X010930Y008594D03*
+X010930Y008584D03*
+X010930Y008574D03*
+X010930Y008564D03*
+X010930Y008554D03*
+X010930Y008544D03*
+X010930Y008534D03*
+X010930Y008524D03*
+X010930Y008514D03*
+X010930Y008504D03*
+X010930Y008494D03*
+X010930Y008484D03*
+X010930Y008474D03*
+X010930Y008464D03*
+X010930Y008454D03*
+X010930Y008444D03*
+X010930Y008434D03*
+X010930Y008424D03*
+X010930Y008414D03*
+X010930Y008404D03*
+X010930Y008394D03*
+X010930Y008384D03*
+X010930Y008374D03*
+X010930Y008364D03*
+X010930Y008354D03*
+X010930Y008344D03*
+X010930Y008334D03*
+X010930Y008324D03*
+X010930Y008314D03*
+X010930Y008304D03*
+X010930Y008294D03*
+X010930Y008284D03*
+X010930Y008274D03*
+X010930Y008264D03*
+X010930Y008254D03*
+X010930Y008244D03*
+X010930Y008234D03*
+X010930Y008224D03*
+X010930Y008214D03*
+X010930Y008204D03*
+X010930Y008194D03*
+X010930Y008184D03*
+X010930Y008174D03*
+X010930Y008164D03*
+X010930Y008154D03*
+X010930Y008144D03*
+X010930Y008134D03*
+X010930Y008124D03*
+X010930Y008114D03*
+X010930Y008104D03*
+X010930Y008094D03*
+X010930Y008084D03*
+X010930Y008074D03*
+X010930Y008064D03*
+X010930Y008054D03*
+X010930Y008044D03*
+X010930Y008034D03*
+X010930Y008024D03*
+X010930Y008014D03*
+X010930Y008004D03*
+X010930Y007994D03*
+X010930Y007984D03*
+X010930Y007974D03*
+X010930Y007964D03*
+X010930Y007954D03*
+X010930Y007944D03*
+X010930Y007934D03*
+X010930Y007924D03*
+X010930Y007914D03*
+X010930Y007904D03*
+X010930Y007894D03*
+X010930Y007884D03*
+X010930Y007874D03*
+X010930Y007864D03*
+X010930Y007854D03*
+X010930Y007844D03*
+X010930Y007834D03*
+X010930Y007824D03*
+X010930Y007814D03*
+X010930Y007804D03*
+X010930Y007794D03*
+X010930Y007784D03*
+X010930Y007774D03*
+X010930Y007764D03*
+X010930Y007754D03*
+X010930Y007744D03*
+X010930Y007734D03*
+X010930Y007724D03*
+X010930Y007714D03*
+X010930Y007704D03*
+X010930Y007694D03*
+X010930Y007684D03*
+X010930Y007674D03*
+X010930Y007664D03*
+X010930Y007654D03*
+X010930Y007644D03*
+X010930Y007634D03*
+X010930Y007624D03*
+X010930Y007614D03*
+X010930Y007604D03*
+X010930Y007594D03*
+X010930Y007584D03*
+X010930Y007574D03*
+X010930Y007564D03*
+X010930Y007554D03*
+X010930Y007544D03*
+X010930Y007534D03*
+X010930Y007524D03*
+X010930Y007514D03*
+X010930Y007504D03*
+X010930Y007494D03*
+X010930Y007484D03*
+X010930Y007474D03*
+X010930Y007464D03*
+X010930Y007454D03*
+X010930Y007444D03*
+X010930Y007434D03*
+X010930Y007424D03*
+X010930Y007414D03*
+X010930Y007404D03*
+X010930Y007394D03*
+X010930Y007384D03*
+X010930Y007374D03*
+X010930Y007364D03*
+X010930Y007354D03*
+X010930Y007344D03*
+X010930Y007334D03*
+X010930Y007324D03*
+X010930Y007314D03*
+X010930Y007304D03*
+X010930Y007294D03*
+X010930Y007284D03*
+X010930Y007274D03*
+X010930Y007264D03*
+X010930Y007254D03*
+X010930Y007244D03*
+X010930Y007234D03*
+X010930Y007224D03*
+X010930Y007214D03*
+X010930Y007204D03*
+X010930Y007194D03*
+X010930Y007184D03*
+X010930Y007174D03*
+X010930Y007164D03*
+X010930Y007154D03*
+X010930Y007144D03*
+X010930Y007134D03*
+X010930Y007124D03*
+X010930Y007114D03*
+X010930Y007104D03*
+X010930Y007094D03*
+X010930Y007084D03*
+X010930Y007074D03*
+X010930Y007064D03*
+X010930Y007054D03*
+X010930Y007044D03*
+X010930Y007034D03*
+X010930Y007024D03*
+X010930Y007014D03*
+X010930Y007004D03*
+X010930Y006994D03*
+X010930Y006984D03*
+X010930Y006974D03*
+X010930Y008644D03*
+X010930Y008654D03*
+X010930Y008664D03*
+X010930Y008674D03*
+X010930Y008684D03*
+X010930Y008694D03*
+X010930Y008704D03*
+X010930Y008714D03*
+X010930Y008724D03*
+X010930Y008734D03*
+X010930Y008744D03*
+X010930Y008754D03*
+X010930Y008764D03*
+X010930Y008774D03*
+X010930Y008784D03*
+X010930Y008794D03*
+X010930Y008804D03*
+X010930Y008814D03*
+X010930Y008824D03*
+X010930Y008834D03*
+X010930Y008844D03*
+X010930Y008854D03*
+X010930Y008864D03*
+X010930Y008874D03*
+X010930Y008884D03*
+X010930Y008894D03*
+X010930Y008904D03*
+X010930Y008914D03*
+X010930Y008924D03*
+X010930Y008934D03*
+X010930Y008944D03*
+X010930Y008954D03*
+X010930Y008964D03*
+X010930Y008974D03*
+X010930Y008984D03*
+X010930Y008994D03*
+X010930Y009004D03*
+X010930Y009014D03*
+X010930Y009024D03*
+X010930Y009034D03*
+X010930Y009044D03*
+X010930Y009054D03*
+X010930Y009064D03*
+X010930Y009074D03*
+X010930Y009084D03*
+X010930Y009094D03*
+X010930Y009104D03*
+X010930Y009114D03*
+X010930Y009124D03*
+X010930Y009134D03*
+X010930Y009144D03*
+X010930Y009154D03*
+X010930Y009164D03*
+X010930Y009174D03*
+X010930Y009184D03*
+X010930Y009194D03*
+X010930Y009204D03*
+X010930Y009214D03*
+X010930Y009224D03*
+X010930Y009234D03*
+X010930Y009244D03*
+X010930Y009254D03*
+X010930Y009264D03*
+X010930Y009274D03*
+X010930Y009284D03*
+X010930Y009294D03*
+X010930Y009304D03*
+X010930Y009314D03*
+X010930Y009324D03*
+X010930Y009334D03*
+X010930Y009344D03*
+X010930Y009354D03*
+X010930Y009364D03*
+X010930Y009374D03*
+X010930Y009384D03*
+X010930Y009394D03*
+X010930Y009404D03*
+X010930Y009414D03*
+X010930Y009424D03*
+X010930Y009434D03*
+X010930Y009444D03*
+X010930Y009454D03*
+X010930Y009464D03*
+X010930Y009474D03*
+X010930Y009484D03*
+X010930Y009494D03*
+X010930Y009504D03*
+X010930Y009514D03*
+X010930Y009524D03*
+X010930Y009534D03*
+X010930Y009544D03*
+X010930Y009554D03*
+X010930Y009564D03*
+X010930Y009574D03*
+X010930Y009584D03*
+X010930Y009594D03*
+X010930Y009604D03*
+X010930Y009614D03*
+X010930Y009624D03*
+X010930Y009634D03*
+X010930Y009644D03*
+X010930Y009654D03*
+X010930Y009664D03*
+X010930Y009674D03*
+X010930Y009684D03*
+X010930Y009694D03*
+X010930Y009704D03*
+X010930Y009714D03*
+X010930Y009724D03*
+X010930Y009734D03*
+X010930Y009744D03*
+X010930Y009754D03*
+X010930Y009764D03*
+X010930Y009774D03*
+X010930Y009784D03*
+X010930Y009794D03*
+X010930Y009804D03*
+X010930Y009814D03*
+X010930Y009824D03*
+X010930Y009834D03*
+X010930Y009844D03*
+X010930Y009854D03*
+X010930Y009864D03*
+X010930Y009874D03*
+X010930Y009884D03*
+X010930Y009894D03*
+X010930Y009904D03*
+X010930Y009914D03*
+X010930Y009924D03*
+X010930Y009934D03*
+X010930Y009944D03*
+X010930Y009954D03*
+X010930Y009964D03*
+X010930Y009974D03*
+X010930Y009984D03*
+X010930Y009994D03*
+X010930Y010004D03*
+X010930Y010014D03*
+X010930Y010024D03*
+X010930Y010034D03*
+X010930Y010044D03*
+X010930Y010054D03*
+X010930Y010064D03*
+X010930Y010074D03*
+X010930Y010084D03*
+X010930Y010094D03*
+X010930Y010534D03*
+X010930Y010544D03*
+X010930Y010554D03*
+X010930Y010564D03*
+X010930Y010574D03*
+X010930Y010584D03*
+X010930Y010594D03*
+X010930Y010604D03*
+X010930Y010614D03*
+X010930Y010624D03*
+X010930Y010634D03*
+X010930Y010644D03*
+X010930Y010654D03*
+X010930Y010664D03*
+X010930Y010674D03*
+X010930Y010684D03*
+X010930Y010694D03*
+X010930Y010704D03*
+X010930Y010714D03*
+X010930Y010724D03*
+X010930Y010734D03*
+X010930Y010744D03*
+X010930Y010754D03*
+X010930Y010764D03*
+X010930Y010774D03*
+X010930Y010784D03*
+X010930Y010794D03*
+X010930Y010804D03*
+X010930Y010814D03*
+X010930Y010824D03*
+X010930Y010834D03*
+X010930Y010844D03*
+X010930Y010854D03*
+X010930Y010864D03*
+X010930Y010874D03*
+X010930Y010884D03*
+X010930Y010894D03*
+X010930Y010904D03*
+X010930Y010914D03*
+X010930Y010924D03*
+X010930Y010934D03*
+X010930Y010944D03*
+X010930Y010954D03*
+X010930Y010964D03*
+X010930Y010974D03*
+X010930Y010984D03*
+X010930Y010994D03*
+X010930Y011004D03*
+X010930Y011014D03*
+X010930Y011024D03*
+X010930Y011034D03*
+X010930Y011044D03*
+X010930Y011054D03*
+X010930Y011064D03*
+X010930Y011074D03*
+X010930Y011084D03*
+X010930Y011094D03*
+X010930Y011104D03*
+X010930Y011114D03*
+X010930Y011124D03*
+X010930Y011134D03*
+X010930Y011144D03*
+X010930Y011154D03*
+X010930Y011164D03*
+X010930Y011174D03*
+X010930Y011184D03*
+X010930Y011194D03*
+X010930Y011204D03*
+X010930Y011214D03*
+X010930Y011224D03*
+X010930Y011234D03*
+X010930Y011244D03*
+X010930Y011254D03*
+X010930Y011264D03*
+X010930Y011274D03*
+X010930Y011284D03*
+X010930Y011294D03*
+X010930Y011304D03*
+X010930Y011314D03*
+X010930Y011324D03*
+X010930Y011334D03*
+X010930Y011344D03*
+X010930Y011354D03*
+X010930Y011364D03*
+X010930Y011374D03*
+X010930Y011384D03*
+X010930Y011394D03*
+X010930Y011404D03*
+X010930Y011414D03*
+X010930Y011424D03*
+X010930Y011434D03*
+X010930Y011444D03*
+X010930Y011454D03*
+X010930Y011464D03*
+X010930Y011474D03*
+X010930Y011484D03*
+X010930Y011494D03*
+X010930Y011504D03*
+X010930Y011514D03*
+X010930Y011524D03*
+X010930Y011534D03*
+X010930Y011544D03*
+X010930Y011554D03*
+X010930Y011564D03*
+X010930Y011574D03*
+X010930Y011584D03*
+X010930Y011594D03*
+X010930Y011604D03*
+X010930Y011614D03*
+X010930Y011624D03*
+X010930Y011634D03*
+X010930Y011644D03*
+X010930Y011654D03*
+X010930Y011664D03*
+X010930Y011674D03*
+X010930Y011684D03*
+X010930Y011694D03*
+X010930Y011704D03*
+X010930Y011714D03*
+X010930Y011724D03*
+X010930Y011734D03*
+X010930Y011744D03*
+X010930Y011754D03*
+X010930Y011764D03*
+X010930Y011774D03*
+X010930Y011784D03*
+X010930Y011794D03*
+X010930Y011804D03*
+X010930Y011814D03*
+X010930Y011824D03*
+X010930Y011834D03*
+X010930Y011844D03*
+X010930Y011854D03*
+X010930Y011864D03*
+X010930Y011874D03*
+X010930Y011884D03*
+X010930Y011894D03*
+X010930Y011904D03*
+X010930Y011914D03*
+X010930Y011924D03*
+X010930Y011934D03*
+X010930Y011944D03*
+X010930Y011954D03*
+X010930Y011964D03*
+X010930Y011974D03*
+X010930Y011984D03*
+X010930Y011994D03*
+X010930Y012004D03*
+X010930Y012014D03*
+X010930Y012024D03*
+X010930Y012034D03*
+X010930Y012044D03*
+X010930Y012054D03*
+X010930Y012064D03*
+X010930Y012074D03*
+X010930Y012084D03*
+X010930Y012094D03*
+X010930Y012104D03*
+X010930Y012114D03*
+X010930Y012124D03*
+X010930Y012134D03*
+X010930Y012144D03*
+X010930Y012154D03*
+X010930Y012164D03*
+X010930Y012174D03*
+X010930Y012184D03*
+X010930Y012194D03*
+X010930Y012204D03*
+X010930Y012214D03*
+X010930Y012224D03*
+X010930Y012234D03*
+X010930Y012244D03*
+X010930Y012254D03*
+X010930Y012264D03*
+X010930Y012274D03*
+X010930Y012284D03*
+X010930Y012294D03*
+X010930Y012304D03*
+X010930Y012314D03*
+X010930Y012324D03*
+X010930Y012334D03*
+X010930Y012344D03*
+X010930Y012354D03*
+X010930Y012364D03*
+X010930Y012374D03*
+X010930Y012384D03*
+X010930Y012394D03*
+X010930Y012404D03*
+X010930Y012414D03*
+X010930Y012424D03*
+X010930Y012434D03*
+X010930Y012444D03*
+X010930Y012454D03*
+X010930Y012464D03*
+X010930Y012474D03*
+X010930Y012484D03*
+X010930Y012494D03*
+X010930Y012504D03*
+X010930Y012514D03*
+X010930Y012524D03*
+X010930Y012534D03*
+X010930Y012544D03*
+X010930Y012554D03*
+X010930Y012564D03*
+X010930Y012574D03*
+X010930Y012584D03*
+X010930Y012594D03*
+X010930Y012604D03*
+X010930Y012614D03*
+X010930Y012624D03*
+X010930Y012634D03*
+X010930Y012644D03*
+X010930Y012654D03*
+X010930Y012664D03*
+X010930Y012674D03*
+X010930Y012684D03*
+X010930Y012694D03*
+X010930Y012704D03*
+X010930Y012714D03*
+X010930Y012724D03*
+X010930Y012734D03*
+X010930Y012744D03*
+X010930Y012754D03*
+X010930Y012764D03*
+X010930Y012774D03*
+X010930Y012784D03*
+X010930Y012794D03*
+X010930Y012804D03*
+X010930Y012814D03*
+X010930Y012824D03*
+X010930Y012834D03*
+X010930Y012844D03*
+X010930Y012854D03*
+X010930Y012864D03*
+X010930Y012874D03*
+X010930Y012884D03*
+X010930Y012894D03*
+X010930Y012904D03*
+X010930Y012914D03*
+X010930Y012924D03*
+X010930Y012934D03*
+X010930Y012944D03*
+X010930Y012954D03*
+X010930Y012964D03*
+X010930Y012974D03*
+X010930Y012984D03*
+X010930Y012994D03*
+X010930Y013004D03*
+X010930Y013014D03*
+X010930Y013024D03*
+X010930Y013034D03*
+X010930Y013044D03*
+X010930Y013054D03*
+X010930Y013064D03*
+X010930Y013074D03*
+X010930Y013084D03*
+X010930Y013094D03*
+X010930Y013104D03*
+X010930Y013114D03*
+X010930Y013124D03*
+X010930Y013134D03*
+X010930Y013144D03*
+X010930Y013154D03*
+X010930Y013164D03*
+X010930Y013174D03*
+X010930Y013184D03*
+X010930Y013194D03*
+X010930Y013204D03*
+X010930Y013214D03*
+X010930Y013224D03*
+X010930Y013234D03*
+X010930Y013244D03*
+X010930Y013254D03*
+D12*
+X013355Y012004D03*
+X014305Y011544D03*
+X014285Y011494D03*
+X014275Y011464D03*
+X014265Y011444D03*
+X014255Y011414D03*
+X014245Y011394D03*
+X014235Y011364D03*
+X014225Y011344D03*
+X014225Y011334D03*
+X014215Y011314D03*
+X014205Y011294D03*
+X014205Y011284D03*
+X014195Y011264D03*
+X014185Y011244D03*
+X014185Y011234D03*
+X014175Y011214D03*
+X015765Y009434D03*
+X015765Y009424D03*
+X015275Y008624D03*
+X015265Y008604D03*
+X015255Y008594D03*
+X015245Y008574D03*
+X015225Y008544D03*
+X015215Y008524D03*
+X015205Y008514D03*
+X015195Y008494D03*
+X015185Y008474D03*
+X015175Y008464D03*
+X015165Y008444D03*
+X015145Y008414D03*
+X015135Y008394D03*
+X015125Y008384D03*
+X015115Y008364D03*
+X015105Y008344D03*
+X015095Y008334D03*
+X015085Y008314D03*
+X015075Y008304D03*
+X015075Y008294D03*
+X015065Y008284D03*
+X015055Y008264D03*
+X015045Y008254D03*
+X015035Y008234D03*
+X015025Y008214D03*
+X015015Y008204D03*
+X015005Y008184D03*
+X014995Y008174D03*
+X014995Y008164D03*
+X014985Y008154D03*
+X014975Y008134D03*
+X014965Y008124D03*
+X014955Y008104D03*
+X014945Y008094D03*
+X014945Y008084D03*
+X014935Y008074D03*
+X014925Y008054D03*
+X014915Y008044D03*
+X014915Y008034D03*
+X014905Y008024D03*
+X014895Y008004D03*
+X014885Y007994D03*
+X014885Y007984D03*
+X014875Y007974D03*
+X014865Y007964D03*
+X014865Y007954D03*
+X014855Y007944D03*
+X014845Y007924D03*
+X014835Y007914D03*
+X014835Y007904D03*
+X014345Y007124D03*
+X014345Y007114D03*
+X016765Y007834D03*
+X016775Y007814D03*
+X016775Y007804D03*
+X016785Y007784D03*
+X016785Y007774D03*
+X016795Y007764D03*
+X016785Y008794D03*
+X016795Y008814D03*
+X016795Y008824D03*
+X016805Y008844D03*
+X018495Y008824D03*
+X018495Y008814D03*
+X018505Y008804D03*
+X018505Y008794D03*
+X018515Y008774D03*
+X018525Y007834D03*
+X018515Y007814D03*
+X018515Y007804D03*
+X018505Y007794D03*
+X018505Y007784D03*
+X019175Y011774D03*
+X019195Y012154D03*
+X019195Y012164D03*
+X019205Y012174D03*
+X019295Y013194D03*
+X015165Y013674D03*
+X006695Y007474D03*
+X006705Y007454D03*
+X006715Y007444D03*
+X006725Y007424D03*
+X006745Y007394D03*
+X006755Y007374D03*
+X006765Y007364D03*
+X006775Y007344D03*
+X006795Y007314D03*
+X006825Y007264D03*
+X006845Y007234D03*
+X006875Y007184D03*
+X006925Y007104D03*
+X007005Y006974D03*
+X006685Y007494D03*
+X006675Y007504D03*
+X006665Y007524D03*
+X006655Y007534D03*
+X006655Y007544D03*
+X006645Y007554D03*
+X006635Y007574D03*
+X006625Y007584D03*
+X006615Y007604D03*
+X006605Y007614D03*
+X006605Y007624D03*
+X006595Y007634D03*
+X006585Y007654D03*
+X006575Y007664D03*
+X006575Y007674D03*
+X006565Y007684D03*
+X006555Y007694D03*
+X006555Y007704D03*
+X006545Y007714D03*
+X006535Y007734D03*
+X006525Y007744D03*
+X006525Y007754D03*
+X006515Y007764D03*
+X006505Y007774D03*
+X006505Y007784D03*
+X006495Y007794D03*
+X006495Y007804D03*
+X006485Y007814D03*
+X006475Y007824D03*
+X006475Y007834D03*
+X006465Y007844D03*
+X006455Y007854D03*
+X006455Y007864D03*
+X006445Y007874D03*
+X006445Y007884D03*
+X006435Y007894D03*
+X006425Y007904D03*
+X006425Y007914D03*
+X006415Y007924D03*
+X006405Y007944D03*
+X006395Y007954D03*
+X006395Y007964D03*
+X006385Y007974D03*
+X006375Y007984D03*
+X006375Y007994D03*
+X006365Y008004D03*
+X006355Y008024D03*
+X006345Y008034D03*
+X006345Y008044D03*
+X006335Y008054D03*
+X006325Y008074D03*
+X006315Y008084D03*
+X006305Y008104D03*
+X006295Y008124D03*
+X006285Y008134D03*
+X006275Y008154D03*
+X006265Y008164D03*
+X006255Y008184D03*
+X006235Y008214D03*
+X006225Y008234D03*
+X006205Y008264D03*
+X006185Y008294D03*
+X006175Y008314D03*
+X006155Y008344D03*
+X006125Y008394D03*
+X006105Y008424D03*
+X006075Y008474D03*
+X006025Y008554D03*
+X004355Y009554D03*
+X004345Y009544D03*
+X004335Y009524D03*
+X004315Y009494D03*
+X004305Y009474D03*
+X004285Y009444D03*
+X004275Y009424D03*
+X004255Y009394D03*
+X004245Y009374D03*
+X004225Y009344D03*
+X004215Y009324D03*
+X004195Y009294D03*
+X004185Y009274D03*
+X004165Y009244D03*
+X004155Y009224D03*
+X004135Y009194D03*
+X004105Y009144D03*
+X004075Y009094D03*
+X004045Y009044D03*
+X004015Y008994D03*
+X003995Y008964D03*
+X003985Y008944D03*
+X003965Y008914D03*
+X003935Y008864D03*
+X003905Y008814D03*
+X003875Y008764D03*
+X003845Y008714D03*
+X003815Y008664D03*
+X003645Y008384D03*
+X004365Y009574D03*
+X004375Y009594D03*
+X004385Y009604D03*
+X004395Y009624D03*
+X004405Y009644D03*
+X004415Y009654D03*
+X004415Y009664D03*
+X004425Y009674D03*
+X004435Y009694D03*
+X004445Y009704D03*
+X004445Y009714D03*
+X004455Y009724D03*
+X004455Y009734D03*
+X004465Y009744D03*
+X004475Y009764D03*
+X004485Y009774D03*
+X004485Y009784D03*
+X004495Y009794D03*
+X004505Y009814D03*
+X005235Y010924D03*
+X005245Y010944D03*
+X005265Y010974D03*
+X005275Y010994D03*
+X005285Y011014D03*
+X005295Y011024D03*
+X005295Y011034D03*
+X005305Y011044D03*
+X005305Y011054D03*
+X005315Y011064D03*
+X005325Y011074D03*
+X005325Y011084D03*
+X005335Y011094D03*
+X005335Y011104D03*
+X005345Y011114D03*
+X005355Y011124D03*
+X005355Y011134D03*
+X005365Y011144D03*
+X005365Y011154D03*
+X005375Y011164D03*
+X005385Y011184D03*
+X005395Y011194D03*
+X005395Y011204D03*
+X005405Y011214D03*
+X005415Y011234D03*
+X005425Y011244D03*
+X005425Y011254D03*
+X005435Y011264D03*
+X005445Y011284D03*
+X005455Y011294D03*
+X005455Y011304D03*
+X005465Y011314D03*
+X005475Y011334D03*
+X005485Y011344D03*
+X005485Y011354D03*
+X005495Y011364D03*
+X005505Y011384D03*
+X005515Y011394D03*
+X005515Y011404D03*
+X005525Y011414D03*
+X005535Y011434D03*
+X005545Y011444D03*
+X005545Y011454D03*
+X005555Y011464D03*
+X005565Y011484D03*
+X005575Y011494D03*
+X005575Y011504D03*
+X005585Y011514D03*
+X005595Y011534D03*
+X005605Y011554D03*
+X005615Y011564D03*
+X005625Y011584D03*
+X005635Y011604D03*
+X005645Y011614D03*
+X005645Y011624D03*
+X005655Y011634D03*
+X005665Y011654D03*
+X005675Y011664D03*
+X005675Y011674D03*
+X005685Y011684D03*
+X005695Y011704D03*
+X005705Y011714D03*
+X005705Y011724D03*
+X005715Y011734D03*
+X005725Y011754D03*
+X005735Y011764D03*
+X005735Y011774D03*
+X005745Y011784D03*
+X005755Y011804D03*
+X005765Y011814D03*
+X005765Y011824D03*
+X005775Y011834D03*
+X005785Y011854D03*
+X005795Y011864D03*
+X005795Y011874D03*
+X005805Y011884D03*
+X005815Y011904D03*
+X005825Y011914D03*
+X005825Y011924D03*
+X005835Y011934D03*
+X005845Y011954D03*
+X005855Y011974D03*
+X005865Y011984D03*
+X005875Y012004D03*
+X005885Y012024D03*
+X005895Y012034D03*
+X005905Y012054D03*
+X005915Y012074D03*
+X005925Y012084D03*
+X005935Y012104D03*
+X005945Y012124D03*
+X005955Y012134D03*
+X005965Y012154D03*
+X005975Y012174D03*
+X005985Y012184D03*
+X005995Y012204D03*
+X006005Y012224D03*
+X006015Y012234D03*
+X006025Y012254D03*
+X006035Y012274D03*
+X006045Y012284D03*
+X006055Y012304D03*
+X006065Y012324D03*
+X006075Y012334D03*
+X006085Y012354D03*
+X006095Y012374D03*
+X006105Y012384D03*
+X006115Y012404D03*
+X006125Y012424D03*
+X006145Y012454D03*
+X006155Y012474D03*
+X006175Y012504D03*
+X006185Y012524D03*
+X006205Y012554D03*
+X006215Y012574D03*
+X006235Y012604D03*
+X006245Y012624D03*
+X006265Y012654D03*
+X006275Y012674D03*
+X006295Y012704D03*
+X006305Y012724D03*
+X006325Y012754D03*
+X006335Y012774D03*
+X006355Y012804D03*
+X006365Y012824D03*
+X006395Y012874D03*
+X006425Y012924D03*
+X006455Y012974D03*
+X006485Y013024D03*
+X006515Y013074D03*
+X006545Y013124D03*
+X006575Y013174D03*
+X006605Y013224D03*
+D13*
+X006600Y013214D03*
+X006590Y013204D03*
+X006590Y013194D03*
+X006580Y013184D03*
+X006570Y013164D03*
+X006560Y013154D03*
+X006560Y013144D03*
+X006550Y013134D03*
+X006540Y013114D03*
+X006530Y013104D03*
+X006530Y013094D03*
+X006520Y013084D03*
+X006510Y013064D03*
+X006500Y013054D03*
+X006500Y013044D03*
+X006490Y013034D03*
+X006480Y013014D03*
+X006470Y013004D03*
+X006470Y012994D03*
+X006460Y012984D03*
+X006450Y012964D03*
+X006440Y012954D03*
+X006440Y012944D03*
+X006430Y012934D03*
+X006420Y012914D03*
+X006410Y012904D03*
+X006410Y012894D03*
+X006400Y012884D03*
+X006390Y012864D03*
+X006380Y012854D03*
+X006380Y012844D03*
+X006370Y012834D03*
+X006360Y012814D03*
+X006350Y012794D03*
+X006340Y012784D03*
+X006330Y012764D03*
+X006320Y012744D03*
+X006310Y012734D03*
+X006300Y012714D03*
+X006290Y012694D03*
+X006280Y012684D03*
+X006270Y012664D03*
+X006260Y012644D03*
+X006250Y012634D03*
+X006240Y012614D03*
+X006230Y012594D03*
+X006220Y012584D03*
+X006210Y012564D03*
+X006200Y012544D03*
+X006190Y012534D03*
+X006180Y012514D03*
+X006170Y012494D03*
+X006160Y012484D03*
+X006150Y012464D03*
+X006140Y012444D03*
+X006130Y012434D03*
+X006120Y012414D03*
+X006110Y012394D03*
+X006090Y012364D03*
+X006080Y012344D03*
+X006060Y012314D03*
+X006050Y012294D03*
+X006030Y012264D03*
+X006020Y012244D03*
+X006000Y012214D03*
+X005990Y012194D03*
+X005970Y012164D03*
+X005960Y012144D03*
+X005940Y012114D03*
+X005930Y012094D03*
+X005910Y012064D03*
+X005900Y012044D03*
+X005880Y012014D03*
+X005870Y011994D03*
+X005850Y011964D03*
+X005840Y011944D03*
+X005810Y011894D03*
+X005780Y011844D03*
+X005750Y011794D03*
+X005720Y011744D03*
+X005690Y011694D03*
+X005660Y011644D03*
+X005630Y011594D03*
+X005620Y011574D03*
+X005600Y011544D03*
+X005590Y011524D03*
+X005560Y011474D03*
+X005530Y011424D03*
+X005500Y011374D03*
+X005470Y011324D03*
+X005440Y011274D03*
+X005410Y011224D03*
+X005380Y011174D03*
+X004430Y009684D03*
+X004400Y009634D03*
+X004390Y009614D03*
+X004370Y009584D03*
+X004360Y009564D03*
+X004340Y009534D03*
+X004330Y009514D03*
+X004320Y009504D03*
+X004310Y009484D03*
+X004300Y009464D03*
+X004290Y009454D03*
+X004280Y009434D03*
+X004270Y009414D03*
+X004260Y009404D03*
+X004250Y009384D03*
+X004240Y009364D03*
+X004230Y009354D03*
+X004220Y009334D03*
+X004210Y009314D03*
+X004200Y009304D03*
+X004190Y009284D03*
+X004180Y009264D03*
+X004170Y009254D03*
+X004160Y009234D03*
+X004150Y009214D03*
+X004140Y009204D03*
+X004130Y009184D03*
+X004120Y009174D03*
+X004120Y009164D03*
+X004110Y009154D03*
+X004100Y009134D03*
+X004090Y009124D03*
+X004090Y009114D03*
+X004080Y009104D03*
+X004070Y009084D03*
+X004060Y009074D03*
+X004060Y009064D03*
+X004050Y009054D03*
+X004040Y009034D03*
+X004030Y009024D03*
+X004030Y009014D03*
+X004020Y009004D03*
+X004010Y008984D03*
+X004000Y008974D03*
+X003990Y008954D03*
+X003980Y008934D03*
+X003970Y008924D03*
+X003960Y008904D03*
+X003950Y008894D03*
+X003950Y008884D03*
+X003940Y008874D03*
+X003930Y008854D03*
+X003920Y008844D03*
+X003920Y008834D03*
+X003910Y008824D03*
+X003900Y008804D03*
+X003890Y008794D03*
+X003890Y008784D03*
+X003880Y008774D03*
+X003870Y008754D03*
+X003860Y008744D03*
+X003860Y008734D03*
+X003850Y008724D03*
+X003840Y008704D03*
+X003830Y008694D03*
+X003830Y008684D03*
+X003820Y008674D03*
+X003810Y008654D03*
+X003800Y008644D03*
+X003800Y008634D03*
+X003790Y008624D03*
+X003780Y008614D03*
+X003780Y008604D03*
+X003770Y008594D03*
+X003770Y008584D03*
+X003760Y008574D03*
+X003750Y008564D03*
+X003750Y008554D03*
+X003740Y008544D03*
+X003740Y008534D03*
+X003730Y008524D03*
+X003720Y008514D03*
+X003720Y008504D03*
+X003710Y008494D03*
+X003710Y008484D03*
+X003700Y008474D03*
+X003690Y008464D03*
+X003690Y008454D03*
+X003680Y008444D03*
+X003680Y008434D03*
+X003670Y008424D03*
+X003660Y008414D03*
+X003660Y008404D03*
+X003650Y008394D03*
+X003640Y008374D03*
+X003630Y008364D03*
+X003630Y008354D03*
+X003620Y008344D03*
+X003610Y008334D03*
+X003610Y008324D03*
+X003600Y008314D03*
+X003600Y008304D03*
+X003590Y008294D03*
+X003580Y008284D03*
+X003580Y008274D03*
+X003570Y008264D03*
+X003570Y008254D03*
+X003560Y008244D03*
+X003550Y008234D03*
+X003550Y008224D03*
+X003540Y008214D03*
+X003540Y008204D03*
+X003530Y008194D03*
+X003520Y008184D03*
+X003520Y008174D03*
+X003510Y008164D03*
+X003510Y008154D03*
+X003500Y008144D03*
+X003490Y008134D03*
+X003490Y008124D03*
+X003480Y008114D03*
+X003480Y008104D03*
+X003470Y008094D03*
+X003460Y008084D03*
+X003460Y008074D03*
+X003450Y008064D03*
+X003450Y008054D03*
+X003440Y008044D03*
+X003430Y008034D03*
+X003430Y008024D03*
+X003420Y008014D03*
+X003410Y007994D03*
+X003400Y007984D03*
+X003400Y007974D03*
+X003390Y007964D03*
+X003380Y007944D03*
+X003370Y007934D03*
+X003370Y007924D03*
+X003360Y007914D03*
+X003350Y007894D03*
+X003340Y007884D03*
+X003340Y007874D03*
+X003330Y007864D03*
+X003320Y007844D03*
+X003310Y007834D03*
+X003310Y007824D03*
+X003300Y007814D03*
+X003290Y007804D03*
+X003290Y007794D03*
+X003280Y007784D03*
+X003280Y007774D03*
+X003270Y007764D03*
+X003260Y007754D03*
+X003260Y007744D03*
+X003250Y007734D03*
+X003240Y007714D03*
+X003230Y007704D03*
+X003230Y007694D03*
+X003220Y007684D03*
+X003210Y007664D03*
+X003200Y007654D03*
+X003200Y007644D03*
+X003190Y007634D03*
+X003180Y007614D03*
+X003170Y007604D03*
+X003170Y007594D03*
+X003160Y007584D03*
+X003150Y007564D03*
+X003140Y007554D03*
+X003140Y007544D03*
+X003130Y007534D03*
+X003120Y007514D03*
+X003110Y007504D03*
+X003110Y007494D03*
+X003100Y007484D03*
+X003090Y007464D03*
+X003080Y007454D03*
+X003070Y007434D03*
+X003060Y007414D03*
+X003050Y007404D03*
+X003040Y007384D03*
+X003030Y007364D03*
+X003020Y007354D03*
+X003010Y007334D03*
+X003000Y007314D03*
+X002990Y007304D03*
+X002980Y007284D03*
+X002970Y007264D03*
+X002960Y007254D03*
+X002950Y007234D03*
+X002940Y007224D03*
+X002940Y007214D03*
+X002930Y007204D03*
+X002920Y007184D03*
+X002910Y007174D03*
+X002900Y007154D03*
+X002890Y007134D03*
+X002880Y007124D03*
+X002870Y007104D03*
+X002860Y007084D03*
+X002850Y007074D03*
+X002840Y007054D03*
+X002830Y007034D03*
+X002820Y007024D03*
+X002810Y007004D03*
+X002800Y006984D03*
+X002790Y006974D03*
+X006540Y007724D03*
+X006590Y007644D03*
+X006620Y007594D03*
+X006640Y007564D03*
+X006670Y007514D03*
+X006690Y007484D03*
+X006700Y007464D03*
+X006720Y007434D03*
+X006730Y007414D03*
+X006740Y007404D03*
+X006750Y007384D03*
+X006770Y007354D03*
+X006780Y007334D03*
+X006790Y007324D03*
+X006800Y007304D03*
+X006810Y007294D03*
+X006810Y007284D03*
+X006820Y007274D03*
+X006830Y007254D03*
+X006840Y007244D03*
+X006850Y007224D03*
+X006860Y007214D03*
+X006860Y007204D03*
+X006870Y007194D03*
+X006880Y007174D03*
+X006890Y007164D03*
+X006890Y007154D03*
+X006900Y007144D03*
+X006910Y007134D03*
+X006910Y007124D03*
+X006920Y007114D03*
+X006930Y007094D03*
+X006940Y007084D03*
+X006940Y007074D03*
+X006950Y007064D03*
+X006960Y007054D03*
+X006960Y007044D03*
+X006970Y007034D03*
+X006970Y007024D03*
+X006980Y007014D03*
+X006990Y007004D03*
+X006990Y006994D03*
+X007000Y006984D03*
+X013350Y012014D03*
+X015160Y013654D03*
+X015160Y013664D03*
+X019300Y013204D03*
+X019370Y012754D03*
+X019220Y012194D03*
+X019210Y012184D03*
+X019180Y011764D03*
+X019190Y011744D03*
+X015760Y009414D03*
+X015760Y009404D03*
+X015280Y008634D03*
+X015270Y008614D03*
+X015250Y008584D03*
+X015240Y008564D03*
+X015230Y008554D03*
+X015220Y008534D03*
+X015200Y008504D03*
+X015190Y008484D03*
+X015170Y008454D03*
+X015160Y008434D03*
+X015150Y008424D03*
+X015140Y008404D03*
+X015120Y008374D03*
+X015110Y008354D03*
+X015090Y008324D03*
+X015060Y008274D03*
+X015040Y008244D03*
+X015030Y008224D03*
+X015010Y008194D03*
+X014980Y008144D03*
+X014960Y008114D03*
+X014930Y008064D03*
+X014900Y008014D03*
+X014850Y007934D03*
+X014350Y007134D03*
+X016800Y007744D03*
+X016800Y007754D03*
+X016810Y007724D03*
+X016800Y008834D03*
+X016810Y008854D03*
+X016820Y008864D03*
+X016820Y008874D03*
+X018480Y008854D03*
+X018480Y008844D03*
+X018490Y008834D03*
+X018500Y007774D03*
+X018500Y007764D03*
+X018490Y007754D03*
+X018490Y007744D03*
+X006860Y013644D03*
+X006860Y013654D03*
+X006870Y013664D03*
+X006870Y013674D03*
+X006880Y013684D03*
+X006850Y013634D03*
+X006840Y013624D03*
+X006840Y013614D03*
+X006830Y013604D03*
+X006830Y013594D03*
+X006820Y013584D03*
+X006810Y013574D03*
+X006810Y013564D03*
+X006800Y013554D03*
+X006800Y013544D03*
+X006790Y013534D03*
+X006780Y013524D03*
+X006780Y013514D03*
+X006770Y013504D03*
+X006770Y013494D03*
+X006760Y013484D03*
+X006750Y013474D03*
+X006750Y013464D03*
+X006740Y013454D03*
+X006740Y013444D03*
+X006730Y013434D03*
+X006720Y013424D03*
+X006720Y013414D03*
+X006710Y013404D03*
+X006710Y013394D03*
+X006700Y013384D03*
+X006690Y013374D03*
+X006690Y013364D03*
+X006680Y013354D03*
+X006680Y013344D03*
+X006670Y013334D03*
+X006660Y013324D03*
+X006660Y013314D03*
+X006650Y013304D03*
+X006650Y013294D03*
+X006640Y013284D03*
+X006630Y013274D03*
+X006630Y013264D03*
+X006620Y013254D03*
+X006620Y013244D03*
+X006610Y013234D03*
+D14*
+X013345Y012024D03*
+X015755Y009394D03*
+X016825Y008884D03*
+X016835Y008894D03*
+X016845Y008914D03*
+X016805Y007734D03*
+X016815Y007714D03*
+X016825Y007704D03*
+X016835Y007684D03*
+X017645Y007024D03*
+X018465Y007704D03*
+X018475Y007714D03*
+X018475Y007724D03*
+X018485Y007734D03*
+X018475Y008864D03*
+X018465Y008874D03*
+X018465Y008884D03*
+X018455Y008894D03*
+X019195Y011734D03*
+X019185Y011754D03*
+X019385Y012744D03*
+X019305Y013214D03*
+X014355Y007154D03*
+X014355Y007144D03*
+X003415Y008004D03*
+X003385Y007954D03*
+X003355Y007904D03*
+X003325Y007854D03*
+X003245Y007724D03*
+X003215Y007674D03*
+X003185Y007624D03*
+X003155Y007574D03*
+X003125Y007524D03*
+X003095Y007474D03*
+X003075Y007444D03*
+X003065Y007424D03*
+X003045Y007394D03*
+X003035Y007374D03*
+X003015Y007344D03*
+X003005Y007324D03*
+X002985Y007294D03*
+X002975Y007274D03*
+X002955Y007244D03*
+X002925Y007194D03*
+X002905Y007164D03*
+X002895Y007144D03*
+X002875Y007114D03*
+X002865Y007094D03*
+X002845Y007064D03*
+X002835Y007044D03*
+X002815Y007014D03*
+X002805Y006994D03*
+D15*
+X017645Y007004D03*
+D16*
+X017640Y007014D03*
+D17*
+X017640Y007034D03*
+X016950Y007534D03*
+X016940Y007544D03*
+X016950Y009044D03*
+X015710Y009244D03*
+X015710Y009254D03*
+X014400Y007294D03*
+X018330Y009044D03*
+X013300Y012114D03*
+X012690Y012714D03*
+X012260Y013184D03*
+X012250Y013194D03*
+X012240Y013204D03*
+X012230Y013214D03*
+X012220Y013224D03*
+X012210Y013234D03*
+X012200Y013244D03*
+X012190Y013254D03*
+X012180Y013264D03*
+X012170Y013274D03*
+X012160Y013284D03*
+X012150Y013294D03*
+X012090Y013364D03*
+X012080Y013374D03*
+X012070Y013384D03*
+X012060Y013394D03*
+X012050Y013404D03*
+X012040Y013414D03*
+X011720Y011224D03*
+X015160Y013524D03*
+X015160Y013534D03*
+X015160Y013544D03*
+X004870Y010414D03*
+D18*
+X005140Y009994D03*
+X005150Y009984D03*
+X005160Y009964D03*
+X005170Y009944D03*
+X004470Y010934D03*
+X004440Y010984D03*
+X004430Y011004D03*
+X004420Y011024D03*
+X004410Y011034D03*
+X004410Y011044D03*
+X004400Y011054D03*
+X004390Y011074D03*
+X004380Y011084D03*
+X004380Y011094D03*
+X004370Y011104D03*
+X004360Y011124D03*
+X004350Y011144D03*
+X004340Y011154D03*
+X004330Y011174D03*
+X004320Y011194D03*
+X004300Y011224D03*
+X004290Y011244D03*
+X004270Y011274D03*
+X004260Y011294D03*
+X004240Y011324D03*
+X004230Y011344D03*
+X004210Y011374D03*
+X004200Y011394D03*
+X004180Y011424D03*
+X013380Y011424D03*
+X013380Y011414D03*
+X013380Y011404D03*
+X013380Y011394D03*
+X013380Y011384D03*
+X013380Y011374D03*
+X013380Y011364D03*
+X013380Y011354D03*
+X013380Y011344D03*
+X013380Y011334D03*
+X013380Y011324D03*
+X013380Y011314D03*
+X013380Y011304D03*
+X013380Y011294D03*
+X013380Y011284D03*
+X013380Y011274D03*
+X013380Y011264D03*
+X013380Y011254D03*
+X013380Y011244D03*
+X013380Y011234D03*
+X013380Y011224D03*
+X013380Y011214D03*
+X013380Y011434D03*
+X013380Y011444D03*
+X013380Y011454D03*
+X013380Y011464D03*
+X013380Y011474D03*
+X013380Y011484D03*
+X013380Y011494D03*
+X013380Y011504D03*
+X013380Y011514D03*
+X013380Y011524D03*
+X013380Y011534D03*
+X013380Y011544D03*
+X013380Y011554D03*
+X013380Y011564D03*
+X013380Y011574D03*
+X013380Y011584D03*
+X013380Y011594D03*
+X013380Y011604D03*
+X013380Y011614D03*
+X013380Y011624D03*
+X013380Y011634D03*
+X013380Y011644D03*
+X013380Y011654D03*
+X013380Y011664D03*
+X013380Y011674D03*
+X013380Y011684D03*
+X013380Y011694D03*
+X013380Y011704D03*
+X013380Y011714D03*
+X013380Y011724D03*
+X013380Y011734D03*
+X013380Y011744D03*
+X013380Y011754D03*
+X013380Y011764D03*
+X013380Y011774D03*
+X013380Y011784D03*
+X013380Y011794D03*
+X013380Y011804D03*
+X013380Y011814D03*
+X013380Y011824D03*
+X013380Y011834D03*
+X013380Y011844D03*
+X013380Y011854D03*
+X013380Y011864D03*
+X013380Y011874D03*
+X013380Y011884D03*
+X013380Y011894D03*
+X013380Y011904D03*
+X013380Y011914D03*
+X013380Y011924D03*
+X013380Y011934D03*
+X013380Y011944D03*
+X013380Y011954D03*
+X013380Y012614D03*
+X013380Y012624D03*
+X013380Y012634D03*
+X013380Y012644D03*
+X013380Y012654D03*
+X013380Y012664D03*
+X013380Y012674D03*
+X013380Y012684D03*
+X013380Y012694D03*
+X013380Y012704D03*
+X013380Y012714D03*
+X013380Y012724D03*
+X013380Y012734D03*
+X013380Y012744D03*
+X013380Y012754D03*
+X013380Y012764D03*
+X013380Y012774D03*
+X013380Y012784D03*
+X013380Y012794D03*
+X013380Y012804D03*
+X013380Y012814D03*
+X013380Y012824D03*
+X013380Y012834D03*
+X013380Y012844D03*
+X013380Y012854D03*
+X013380Y012864D03*
+X013380Y012874D03*
+X013380Y012884D03*
+X013380Y012894D03*
+X013380Y012904D03*
+X013380Y012914D03*
+X013380Y012924D03*
+X013380Y012934D03*
+X013380Y012944D03*
+X013380Y012954D03*
+X013380Y012964D03*
+X013380Y012974D03*
+X013380Y012984D03*
+X013380Y012994D03*
+X013380Y013004D03*
+X013380Y013014D03*
+X013380Y013024D03*
+X013380Y013034D03*
+X013380Y013044D03*
+X013380Y013054D03*
+X013380Y013064D03*
+X013380Y013074D03*
+X013380Y013084D03*
+X013380Y013094D03*
+X013380Y013104D03*
+X013380Y013114D03*
+X013380Y013124D03*
+X013380Y013134D03*
+X013380Y013144D03*
+X013380Y013154D03*
+X013380Y013164D03*
+X013380Y013174D03*
+X013380Y013184D03*
+X013380Y013194D03*
+X013380Y013204D03*
+X013380Y013214D03*
+X013380Y013224D03*
+X013380Y013234D03*
+X013380Y013244D03*
+X013380Y013254D03*
+X013380Y013264D03*
+X013380Y013274D03*
+X013380Y013284D03*
+X013380Y013294D03*
+X013380Y013304D03*
+X013380Y013314D03*
+X013380Y013324D03*
+X013380Y013334D03*
+X013380Y013344D03*
+X013380Y013354D03*
+X013380Y013364D03*
+X013380Y013374D03*
+X013380Y013384D03*
+X013380Y013394D03*
+X013380Y013404D03*
+X013380Y013414D03*
+X013380Y013424D03*
+X013380Y013434D03*
+X013380Y013444D03*
+X013380Y013454D03*
+X013380Y013464D03*
+X013380Y013474D03*
+X013380Y013484D03*
+X013380Y013494D03*
+X013380Y013504D03*
+X013380Y013514D03*
+X013380Y013524D03*
+X013380Y013534D03*
+X013380Y013544D03*
+X013380Y013554D03*
+X013380Y013564D03*
+X013380Y013574D03*
+X013380Y013584D03*
+X013380Y013594D03*
+X013380Y013604D03*
+X013380Y013614D03*
+X013380Y013624D03*
+X013380Y013634D03*
+X013380Y013644D03*
+X013380Y013654D03*
+X013380Y013664D03*
+X013380Y013674D03*
+X013380Y013684D03*
+X013380Y013694D03*
+X014900Y013074D03*
+X014880Y013024D03*
+X015490Y012904D03*
+X015500Y012874D03*
+X015510Y012854D03*
+X015520Y012824D03*
+X015530Y012804D03*
+X015530Y012794D03*
+X015540Y012774D03*
+X015540Y012764D03*
+X015550Y012744D03*
+X015560Y012724D03*
+X015560Y012714D03*
+X015570Y012694D03*
+X015570Y012684D03*
+X015580Y012674D03*
+X015580Y012664D03*
+X015580Y012654D03*
+X015590Y012644D03*
+X015590Y012634D03*
+X015600Y012624D03*
+X015600Y012614D03*
+X015600Y012604D03*
+X015610Y012594D03*
+X015610Y012584D03*
+X015610Y012574D03*
+X015620Y012564D03*
+X015620Y012554D03*
+X015630Y012534D03*
+X015630Y012524D03*
+X015640Y012514D03*
+X015640Y012504D03*
+X015650Y012484D03*
+X015650Y012474D03*
+X015660Y012454D03*
+X015670Y012434D03*
+X015670Y012424D03*
+X015680Y012404D03*
+X015680Y012394D03*
+X015690Y012374D03*
+X015700Y012344D03*
+X015710Y012324D03*
+X015720Y012294D03*
+X015750Y012214D03*
+X018210Y012214D03*
+X018210Y012204D03*
+X018210Y012194D03*
+X018210Y012184D03*
+X018210Y012174D03*
+X018210Y012164D03*
+X018210Y012154D03*
+X018210Y012144D03*
+X018210Y012134D03*
+X018210Y012124D03*
+X018210Y012114D03*
+X018210Y012104D03*
+X018210Y012094D03*
+X018210Y012084D03*
+X018210Y012074D03*
+X018210Y012064D03*
+X018210Y012054D03*
+X018210Y012044D03*
+X018210Y012034D03*
+X018210Y012024D03*
+X018210Y012014D03*
+X018210Y012004D03*
+X018210Y011994D03*
+X018210Y011984D03*
+X018210Y011974D03*
+X018210Y011964D03*
+X018210Y011954D03*
+X018210Y011944D03*
+X018210Y011934D03*
+X018210Y011924D03*
+X018210Y011914D03*
+X018210Y011904D03*
+X018210Y011894D03*
+X018210Y011884D03*
+X018210Y011874D03*
+X018210Y011864D03*
+X018210Y011854D03*
+X018210Y011844D03*
+X018210Y011834D03*
+X018210Y011824D03*
+X018210Y011814D03*
+X018210Y011804D03*
+X018210Y011794D03*
+X018210Y011784D03*
+X018210Y011774D03*
+X018210Y011764D03*
+X018210Y011754D03*
+X018210Y011744D03*
+X018210Y011734D03*
+X018210Y011724D03*
+X018210Y011714D03*
+X018210Y011704D03*
+X018210Y011694D03*
+X018210Y011684D03*
+X018210Y011674D03*
+X018210Y011664D03*
+X018210Y011654D03*
+X018210Y011644D03*
+X018210Y011634D03*
+X018210Y012224D03*
+X018210Y012234D03*
+X018210Y012244D03*
+X018210Y012254D03*
+X018210Y012264D03*
+X018210Y012274D03*
+X018210Y012284D03*
+X018210Y012294D03*
+X018210Y012734D03*
+X018210Y012744D03*
+X018210Y012754D03*
+X018210Y012764D03*
+X018210Y012774D03*
+X018210Y012784D03*
+X018210Y012794D03*
+X018210Y012804D03*
+X018210Y012814D03*
+X018210Y012824D03*
+X018210Y012834D03*
+X018210Y012844D03*
+X018210Y012854D03*
+X018210Y012864D03*
+X018210Y012874D03*
+X018210Y012884D03*
+X018210Y012894D03*
+X018210Y012904D03*
+X018210Y012914D03*
+X018210Y012924D03*
+X018210Y012934D03*
+X018210Y012944D03*
+X018210Y012954D03*
+X018210Y012964D03*
+X018210Y012974D03*
+X018210Y012984D03*
+X018210Y012994D03*
+X018210Y013004D03*
+X018210Y013014D03*
+X018210Y013024D03*
+X018210Y013034D03*
+X018210Y013044D03*
+X018210Y013054D03*
+X018210Y013064D03*
+X018210Y013074D03*
+X018210Y013084D03*
+X018210Y013094D03*
+X018210Y013104D03*
+X018210Y013114D03*
+X018210Y013124D03*
+X018210Y013134D03*
+X018210Y013144D03*
+X018210Y013154D03*
+X018210Y013164D03*
+X018210Y013174D03*
+X018210Y013184D03*
+X018210Y013194D03*
+X018210Y013204D03*
+X018210Y013214D03*
+X018210Y013224D03*
+X018210Y013234D03*
+X018210Y013244D03*
+X018210Y013254D03*
+X018210Y013264D03*
+X019260Y013114D03*
+X019260Y013104D03*
+X019260Y013094D03*
+X019310Y012814D03*
+X019320Y012804D03*
+X020710Y012804D03*
+X020710Y012794D03*
+X020710Y012784D03*
+X020710Y012774D03*
+X020710Y012764D03*
+X020710Y012754D03*
+X020710Y012744D03*
+X020710Y012734D03*
+X020710Y012724D03*
+X020710Y012714D03*
+X020710Y012814D03*
+X020710Y012824D03*
+X020710Y012834D03*
+X020710Y012844D03*
+X020710Y012854D03*
+X020710Y012864D03*
+X020710Y012874D03*
+X020710Y012884D03*
+X020710Y012894D03*
+X020710Y012904D03*
+X020710Y012914D03*
+X020710Y012924D03*
+X020710Y012934D03*
+X020710Y012944D03*
+X020710Y012954D03*
+X020710Y012964D03*
+X020710Y012974D03*
+X020710Y012984D03*
+X020710Y012994D03*
+X020710Y013004D03*
+X020710Y013014D03*
+X020710Y013024D03*
+X020710Y013034D03*
+X020710Y013044D03*
+X020710Y013054D03*
+X020710Y013064D03*
+X020710Y013074D03*
+X020710Y013084D03*
+X020710Y013094D03*
+X020710Y013104D03*
+X020710Y013114D03*
+X020710Y013124D03*
+X020710Y013134D03*
+X020710Y013144D03*
+X020710Y013154D03*
+X020710Y013164D03*
+X020710Y013174D03*
+X020710Y013184D03*
+X020710Y013194D03*
+X020710Y013204D03*
+X020710Y013214D03*
+X020710Y013224D03*
+X020710Y013234D03*
+X020710Y013244D03*
+X020710Y013254D03*
+X020710Y013264D03*
+X020710Y013274D03*
+X020710Y012284D03*
+X020710Y012274D03*
+X020710Y012264D03*
+X020710Y012254D03*
+X020710Y012244D03*
+X020710Y012234D03*
+X020710Y012224D03*
+X020710Y012214D03*
+X020710Y012204D03*
+X020710Y012194D03*
+X020710Y012184D03*
+X020710Y012174D03*
+X020710Y012164D03*
+X020710Y012154D03*
+X020710Y012144D03*
+X020710Y012134D03*
+X020710Y012124D03*
+X020710Y012114D03*
+X020710Y012104D03*
+X020710Y012094D03*
+X020710Y012084D03*
+X020710Y012074D03*
+X020710Y012064D03*
+X020710Y012054D03*
+X020710Y012044D03*
+X020710Y012034D03*
+X020710Y012024D03*
+X020710Y012014D03*
+X020710Y012004D03*
+X020710Y011994D03*
+X020710Y011984D03*
+X020710Y011974D03*
+X020710Y011964D03*
+X020710Y011954D03*
+X020710Y011944D03*
+X020710Y011934D03*
+X020710Y011924D03*
+X020710Y011914D03*
+X020710Y011904D03*
+X020710Y011894D03*
+X020710Y011884D03*
+X020710Y011874D03*
+X020710Y011864D03*
+X020710Y011854D03*
+X020710Y011844D03*
+X020710Y011834D03*
+X020710Y011824D03*
+X020710Y011814D03*
+X020710Y011804D03*
+X020710Y011794D03*
+X020710Y011784D03*
+X020710Y011774D03*
+X020710Y011764D03*
+X020710Y011754D03*
+X020710Y011744D03*
+X020710Y011734D03*
+X020710Y011724D03*
+X020710Y011714D03*
+X020710Y011704D03*
+X020710Y011694D03*
+X020710Y011684D03*
+X020710Y011674D03*
+X020710Y011664D03*
+X020710Y011654D03*
+X020710Y011644D03*
+X020710Y011634D03*
+X020060Y009104D03*
+X020060Y009094D03*
+X020060Y009084D03*
+X020060Y009074D03*
+X020060Y009064D03*
+X020060Y009054D03*
+X020060Y009044D03*
+X020060Y009034D03*
+X020060Y009024D03*
+X020060Y009014D03*
+X020060Y009004D03*
+X020060Y008994D03*
+X020060Y008984D03*
+X020060Y008974D03*
+X020060Y008964D03*
+X020060Y008954D03*
+X020060Y008944D03*
+X020060Y008934D03*
+X020060Y008924D03*
+X020060Y008914D03*
+X020060Y008904D03*
+X020060Y008894D03*
+X020060Y008884D03*
+X020060Y008874D03*
+X020060Y008864D03*
+X020060Y008854D03*
+X020060Y008844D03*
+X020060Y008834D03*
+X020060Y008824D03*
+X020060Y008814D03*
+X020060Y008804D03*
+X020060Y008794D03*
+X020060Y008784D03*
+X020060Y008774D03*
+X020060Y008764D03*
+X020060Y008754D03*
+X020060Y008744D03*
+X020060Y008734D03*
+X020060Y008724D03*
+X020060Y008714D03*
+X020060Y008704D03*
+X020060Y008694D03*
+X020060Y008684D03*
+X020060Y008674D03*
+X020060Y008664D03*
+X020060Y008654D03*
+X020060Y008644D03*
+X020060Y008634D03*
+X020060Y008624D03*
+X020060Y008614D03*
+X020060Y008604D03*
+X020060Y008594D03*
+X020060Y008584D03*
+X020060Y008574D03*
+X020060Y008564D03*
+X020060Y008554D03*
+X020060Y008544D03*
+X020060Y008534D03*
+X020060Y008524D03*
+X020060Y008514D03*
+X020060Y008504D03*
+X020060Y008494D03*
+X020060Y008484D03*
+X020060Y008474D03*
+X020060Y008464D03*
+X020060Y008454D03*
+X020060Y008444D03*
+X020060Y008434D03*
+X020060Y008424D03*
+X020060Y008414D03*
+X020060Y008404D03*
+X020060Y008394D03*
+X020060Y008384D03*
+X020060Y008374D03*
+X020060Y008364D03*
+X020060Y008354D03*
+X020060Y008344D03*
+X020060Y008334D03*
+X020060Y008324D03*
+X020060Y008314D03*
+X020060Y008304D03*
+X020060Y008294D03*
+X020060Y008284D03*
+X020060Y008274D03*
+X020060Y008264D03*
+X020060Y008254D03*
+X020060Y008244D03*
+X020060Y008234D03*
+X020060Y008224D03*
+X020060Y008214D03*
+X020060Y008204D03*
+X020060Y008194D03*
+X020060Y008184D03*
+X020060Y008174D03*
+X020060Y008164D03*
+X020060Y008154D03*
+X020060Y008144D03*
+X020060Y008134D03*
+X020060Y008124D03*
+X020060Y008114D03*
+X020060Y008104D03*
+X020060Y008094D03*
+X020060Y008084D03*
+X020060Y008074D03*
+X020060Y008064D03*
+X020060Y008054D03*
+X020060Y008044D03*
+X020060Y008034D03*
+X020060Y008024D03*
+X020060Y008014D03*
+X020060Y008004D03*
+X020060Y007994D03*
+X020060Y007984D03*
+X020060Y007974D03*
+X020060Y007964D03*
+X020060Y007954D03*
+X020060Y007944D03*
+X020060Y007934D03*
+X020060Y007924D03*
+X020060Y007914D03*
+X020060Y007904D03*
+X020060Y007894D03*
+X020060Y007884D03*
+X020060Y007874D03*
+X020060Y007864D03*
+X020060Y007854D03*
+X020060Y007844D03*
+X020060Y007834D03*
+X020060Y007824D03*
+X020060Y007814D03*
+X020060Y007804D03*
+X020060Y007794D03*
+X020060Y007784D03*
+X020060Y007774D03*
+X020060Y007764D03*
+X020060Y007754D03*
+X020060Y007744D03*
+X020060Y007734D03*
+X020060Y007724D03*
+X020060Y007714D03*
+X020060Y007704D03*
+X020060Y007694D03*
+X020060Y007684D03*
+X020060Y007674D03*
+X020060Y007664D03*
+X020060Y007654D03*
+X020060Y007644D03*
+X020060Y007634D03*
+X020060Y007624D03*
+X020060Y007614D03*
+X020060Y007604D03*
+X020060Y007594D03*
+X020060Y007584D03*
+X020060Y007574D03*
+X020060Y007564D03*
+X020060Y007554D03*
+X020060Y007544D03*
+X020060Y007534D03*
+X020060Y007524D03*
+X020060Y007514D03*
+X020060Y007504D03*
+X020060Y007494D03*
+X020060Y007484D03*
+X020060Y007474D03*
+X020060Y007464D03*
+X020060Y007454D03*
+X020060Y007444D03*
+X020060Y007434D03*
+X020060Y007424D03*
+X020060Y007414D03*
+X020060Y007404D03*
+X020060Y007394D03*
+X020060Y007384D03*
+X020060Y007374D03*
+X020060Y007364D03*
+X020060Y007354D03*
+X020060Y007344D03*
+X020060Y007334D03*
+X020060Y007324D03*
+X020060Y007314D03*
+X020060Y007304D03*
+X020060Y007294D03*
+X020060Y007284D03*
+X020060Y007274D03*
+X020060Y007264D03*
+X020060Y007254D03*
+X020060Y007244D03*
+X020060Y007234D03*
+X020060Y007224D03*
+X020060Y007214D03*
+X020060Y007204D03*
+X020060Y007194D03*
+X020060Y007184D03*
+X020060Y007174D03*
+X020060Y007164D03*
+X020060Y007154D03*
+X020060Y007144D03*
+X020060Y007134D03*
+X020060Y007124D03*
+X020060Y007114D03*
+X020060Y007104D03*
+X020060Y007094D03*
+X020060Y007084D03*
+X020060Y007074D03*
+X020060Y007064D03*
+X020060Y007054D03*
+X020060Y007044D03*
+X015790Y009504D03*
+X015790Y009514D03*
+X013290Y009104D03*
+X013290Y009094D03*
+X013290Y009084D03*
+X013290Y009074D03*
+X013290Y009064D03*
+X013290Y009054D03*
+X013290Y009044D03*
+X013290Y009034D03*
+X013290Y009024D03*
+X013290Y009014D03*
+X013290Y009004D03*
+X013290Y008994D03*
+X013290Y008984D03*
+X013290Y008974D03*
+X013290Y008964D03*
+X013290Y008954D03*
+X013290Y008944D03*
+X013290Y008934D03*
+X013290Y008924D03*
+X013290Y008914D03*
+X013290Y008904D03*
+X013290Y008894D03*
+X013290Y008884D03*
+X013290Y008874D03*
+X013290Y008864D03*
+X013290Y008854D03*
+X013290Y008844D03*
+X013290Y008834D03*
+X013290Y008824D03*
+X013290Y008814D03*
+X013290Y008804D03*
+X013290Y008794D03*
+X013290Y008784D03*
+X013290Y008774D03*
+X013290Y008764D03*
+X013290Y008754D03*
+X013290Y008744D03*
+X013290Y008734D03*
+X013290Y008724D03*
+X013290Y008714D03*
+X013290Y008704D03*
+X013290Y008694D03*
+X013290Y008684D03*
+X013290Y008674D03*
+X013290Y008664D03*
+X013290Y008654D03*
+X013290Y008644D03*
+X013290Y008634D03*
+X013290Y008624D03*
+X013290Y008614D03*
+X013290Y008604D03*
+X013290Y008594D03*
+X013290Y008584D03*
+X013290Y008574D03*
+X013290Y008564D03*
+X013290Y008134D03*
+X013290Y008124D03*
+X013290Y008114D03*
+X013290Y008104D03*
+X013290Y008094D03*
+X013290Y008084D03*
+X013290Y008074D03*
+X013290Y008064D03*
+X013290Y008054D03*
+X013290Y008044D03*
+X013290Y008034D03*
+X013290Y008024D03*
+X013290Y008014D03*
+X013290Y008004D03*
+X013290Y007994D03*
+X013290Y007984D03*
+X013290Y007974D03*
+X013290Y007964D03*
+X013290Y007954D03*
+X013290Y007944D03*
+X013290Y007934D03*
+X013290Y007924D03*
+X013290Y007914D03*
+X013290Y007904D03*
+X013290Y007894D03*
+X013290Y007884D03*
+X013290Y007874D03*
+X013290Y007864D03*
+X013290Y007854D03*
+X013290Y007844D03*
+X013290Y007834D03*
+X013290Y007824D03*
+X013290Y007814D03*
+X013290Y007804D03*
+X013290Y007794D03*
+X013290Y007784D03*
+X013290Y007774D03*
+X013290Y007764D03*
+X013290Y007754D03*
+X013290Y007744D03*
+X013290Y007734D03*
+X013290Y007724D03*
+X013290Y007714D03*
+X013290Y007704D03*
+X013290Y007694D03*
+X013290Y007684D03*
+X013290Y007674D03*
+X013290Y007664D03*
+X013290Y007654D03*
+X013290Y007644D03*
+X013290Y007634D03*
+X013290Y007624D03*
+X013290Y007614D03*
+X013290Y007604D03*
+X013290Y007594D03*
+X013290Y007584D03*
+X013290Y007574D03*
+X013290Y007564D03*
+X013290Y007554D03*
+X013290Y007544D03*
+X013290Y007534D03*
+X013290Y007524D03*
+X013290Y007514D03*
+X013290Y007504D03*
+X013290Y007494D03*
+X013290Y007484D03*
+X013290Y007474D03*
+D19*
+X014445Y007444D03*
+X014445Y007434D03*
+X015665Y009094D03*
+X015665Y009104D03*
+X017035Y009104D03*
+X017035Y007474D03*
+X017645Y007044D03*
+X018255Y007474D03*
+X013255Y012214D03*
+X012695Y012654D03*
+X019495Y012714D03*
+X004865Y010244D03*
+X004865Y010234D03*
+D20*
+X005155Y009974D03*
+X005165Y009954D03*
+X005175Y009934D03*
+X005185Y009924D03*
+X005185Y009914D03*
+X005195Y009904D03*
+X005205Y009884D03*
+X004365Y011114D03*
+X004355Y011134D03*
+X004335Y011164D03*
+X004325Y011184D03*
+X004315Y011204D03*
+X004305Y011214D03*
+X004295Y011234D03*
+X004285Y011254D03*
+X004275Y011264D03*
+X004265Y011284D03*
+X004255Y011304D03*
+X004245Y011314D03*
+X004235Y011334D03*
+X004225Y011354D03*
+X004215Y011364D03*
+X004205Y011384D03*
+X004195Y011404D03*
+X004185Y011414D03*
+X004175Y011434D03*
+X004165Y011444D03*
+X004165Y011454D03*
+X004155Y011464D03*
+X004155Y011474D03*
+X004145Y011484D03*
+X004135Y011494D03*
+X004135Y011504D03*
+X004125Y011514D03*
+X004125Y011524D03*
+X004115Y011534D03*
+X004105Y011544D03*
+X004105Y011554D03*
+X004095Y011564D03*
+X004095Y011574D03*
+X004085Y011584D03*
+X004075Y011594D03*
+X004075Y011604D03*
+X004065Y011614D03*
+X004065Y011624D03*
+X004055Y011634D03*
+X004045Y011644D03*
+X004045Y011654D03*
+X004035Y011664D03*
+X004035Y011674D03*
+X004025Y011684D03*
+X004015Y011704D03*
+X004005Y011714D03*
+X003995Y011734D03*
+X003985Y011754D03*
+X003975Y011764D03*
+X003965Y011784D03*
+X003955Y011804D03*
+X003945Y011814D03*
+X003935Y011834D03*
+X003925Y011854D03*
+X003915Y011864D03*
+X003905Y011884D03*
+X003895Y011904D03*
+X003885Y011914D03*
+X003875Y011934D03*
+X003865Y011954D03*
+X003845Y011984D03*
+X003815Y012034D03*
+X003785Y012084D03*
+X003755Y012134D03*
+X013375Y011964D03*
+X014745Y012674D03*
+X014765Y012724D03*
+X014775Y012754D03*
+X014785Y012774D03*
+X014785Y012784D03*
+X014795Y012804D03*
+X014805Y012824D03*
+X014805Y012834D03*
+X014815Y012854D03*
+X014815Y012864D03*
+X014825Y012874D03*
+X014825Y012884D03*
+X014835Y012904D03*
+X014835Y012914D03*
+X014845Y012924D03*
+X014845Y012934D03*
+X014845Y012944D03*
+X014855Y012954D03*
+X014855Y012964D03*
+X014865Y012974D03*
+X014865Y012984D03*
+X014865Y012994D03*
+X014875Y013004D03*
+X014875Y013014D03*
+X014885Y013034D03*
+X014885Y013044D03*
+X014895Y013054D03*
+X014895Y013064D03*
+X014905Y013084D03*
+X015625Y012544D03*
+X015645Y012494D03*
+X015655Y012464D03*
+X015665Y012444D03*
+X015675Y012414D03*
+X015685Y012384D03*
+X015695Y012364D03*
+X015695Y012354D03*
+X015705Y012334D03*
+X015715Y012314D03*
+X015715Y012304D03*
+X015725Y012284D03*
+X015725Y012274D03*
+X015735Y012264D03*
+X015735Y012254D03*
+X015735Y012244D03*
+X015745Y012234D03*
+X015745Y012224D03*
+X015755Y012204D03*
+X015785Y009494D03*
+X016705Y008444D03*
+X018585Y008434D03*
+X018585Y008424D03*
+X018585Y008414D03*
+X014325Y007054D03*
+X014325Y007044D03*
+X019145Y011994D03*
+X019145Y012004D03*
+X019325Y012794D03*
+X019265Y013124D03*
+X019265Y013134D03*
+D21*
+X020015Y013334D03*
+X020015Y013344D03*
+X020015Y012384D03*
+X020015Y011434D03*
+X017515Y011434D03*
+X017515Y011424D03*
+X017515Y011414D03*
+X017515Y011404D03*
+X017515Y011394D03*
+X017515Y011384D03*
+X017515Y011374D03*
+X017515Y011364D03*
+X017515Y011354D03*
+X017515Y011344D03*
+X017515Y011334D03*
+X017515Y011324D03*
+X017515Y011314D03*
+X017515Y011304D03*
+X017515Y011294D03*
+X017515Y011284D03*
+X017515Y011274D03*
+X017515Y011264D03*
+X017515Y011254D03*
+X017515Y011244D03*
+X017515Y011234D03*
+X017515Y011224D03*
+X017515Y011214D03*
+X017515Y011444D03*
+X017515Y011454D03*
+X017515Y011464D03*
+X017515Y011474D03*
+X017515Y011484D03*
+X017515Y011494D03*
+X017515Y011504D03*
+X017515Y011514D03*
+X017515Y011524D03*
+X017515Y011534D03*
+X017515Y011544D03*
+X017515Y011554D03*
+X017515Y011564D03*
+X017515Y011574D03*
+X017515Y011584D03*
+X017515Y011594D03*
+X017515Y011604D03*
+X017515Y011614D03*
+X017515Y011624D03*
+X015155Y011974D03*
+X017645Y009154D03*
+X017645Y007414D03*
+X012595Y007414D03*
+X012595Y007404D03*
+X012595Y007394D03*
+X012595Y007384D03*
+X012595Y007374D03*
+X012595Y007364D03*
+X012595Y007354D03*
+X012595Y007344D03*
+X012595Y007334D03*
+X012595Y007324D03*
+X012595Y007314D03*
+X012595Y007304D03*
+X012595Y007294D03*
+X012595Y007284D03*
+X012595Y007274D03*
+X012595Y007264D03*
+X012595Y007254D03*
+X012595Y007244D03*
+X012595Y007234D03*
+X012595Y007224D03*
+X012595Y007214D03*
+X012595Y007204D03*
+X012595Y007194D03*
+X012595Y007184D03*
+X012595Y007174D03*
+X012595Y007164D03*
+X012595Y007154D03*
+X012595Y007144D03*
+X012595Y007134D03*
+X012595Y007124D03*
+X012595Y007114D03*
+X012595Y007104D03*
+X012595Y007094D03*
+X012595Y007084D03*
+X012595Y007074D03*
+X012595Y007064D03*
+X012595Y007054D03*
+X012595Y007044D03*
+X012595Y007424D03*
+X012595Y007434D03*
+X012595Y007444D03*
+X012595Y007454D03*
+X012595Y007464D03*
+D22*
+X014475Y007534D03*
+X014475Y007544D03*
+X015635Y009004D03*
+X017085Y009124D03*
+X018205Y009124D03*
+X018215Y007454D03*
+X017645Y007054D03*
+X013225Y012274D03*
+X012705Y012614D03*
+X015155Y013344D03*
+X004865Y010194D03*
+X004865Y010184D03*
+D23*
+X004865Y010144D03*
+X013195Y012334D03*
+X017115Y009134D03*
+X018165Y009134D03*
+X015605Y008904D03*
+X014505Y007634D03*
+X017645Y007064D03*
+D24*
+X016730Y007964D03*
+X016720Y008004D03*
+X016720Y008014D03*
+X016720Y008024D03*
+X016720Y008034D03*
+X016710Y008064D03*
+X016710Y008074D03*
+X016710Y008084D03*
+X016710Y008094D03*
+X016710Y008104D03*
+X016710Y008114D03*
+X016710Y008124D03*
+X016700Y008144D03*
+X016700Y008154D03*
+X016700Y008164D03*
+X016700Y008174D03*
+X016700Y008184D03*
+X016700Y008194D03*
+X016700Y008204D03*
+X016700Y008214D03*
+X016700Y008224D03*
+X016700Y008234D03*
+X016700Y008244D03*
+X016700Y008254D03*
+X016700Y008264D03*
+X016700Y008274D03*
+X016700Y008284D03*
+X016700Y008294D03*
+X016700Y008304D03*
+X016700Y008314D03*
+X016700Y008324D03*
+X016700Y008334D03*
+X016700Y008344D03*
+X016700Y008354D03*
+X016700Y008364D03*
+X016700Y008374D03*
+X016700Y008384D03*
+X016700Y008394D03*
+X016700Y008404D03*
+X016700Y008414D03*
+X016700Y008424D03*
+X016700Y008434D03*
+X016710Y008454D03*
+X016710Y008464D03*
+X016710Y008474D03*
+X016710Y008484D03*
+X016710Y008494D03*
+X016710Y008504D03*
+X016710Y008514D03*
+X016710Y008524D03*
+X016720Y008534D03*
+X016720Y008544D03*
+X016720Y008554D03*
+X016720Y008564D03*
+X016720Y008574D03*
+X016720Y008584D03*
+X016730Y008604D03*
+X016730Y008614D03*
+X016730Y008624D03*
+X016740Y008654D03*
+X016740Y008664D03*
+X015780Y009474D03*
+X015780Y009484D03*
+X017650Y009554D03*
+X018550Y008654D03*
+X018560Y008614D03*
+X018560Y008604D03*
+X018570Y008574D03*
+X018570Y008564D03*
+X018570Y008554D03*
+X018570Y008544D03*
+X018570Y008534D03*
+X018580Y008514D03*
+X018580Y008504D03*
+X018580Y008494D03*
+X018580Y008484D03*
+X018580Y008474D03*
+X018580Y008464D03*
+X018580Y008454D03*
+X018580Y008444D03*
+X018590Y008404D03*
+X018590Y008394D03*
+X018590Y008384D03*
+X018590Y008374D03*
+X018590Y008364D03*
+X018590Y008354D03*
+X018590Y008344D03*
+X018590Y008334D03*
+X018590Y008324D03*
+X018590Y008314D03*
+X018590Y008304D03*
+X018590Y008294D03*
+X018590Y008284D03*
+X018590Y008274D03*
+X018590Y008264D03*
+X018590Y008254D03*
+X018590Y008244D03*
+X018590Y008234D03*
+X018590Y008224D03*
+X018590Y008214D03*
+X018590Y008204D03*
+X018590Y008194D03*
+X018590Y008184D03*
+X018590Y008174D03*
+X018590Y008164D03*
+X018590Y008154D03*
+X018590Y008144D03*
+X018580Y008134D03*
+X018580Y008124D03*
+X018580Y008114D03*
+X018580Y008104D03*
+X018580Y008094D03*
+X018580Y008084D03*
+X018580Y008074D03*
+X018580Y008064D03*
+X018570Y008034D03*
+X018570Y008024D03*
+X018570Y008014D03*
+X018570Y008004D03*
+X018560Y007964D03*
+X014330Y007074D03*
+X014330Y007064D03*
+X016060Y011394D03*
+X016040Y011444D03*
+X016030Y011474D03*
+X016020Y011504D03*
+X016010Y011524D03*
+X016000Y011554D03*
+X015990Y011574D03*
+X015990Y011584D03*
+X015980Y011604D03*
+X015980Y011614D03*
+X015970Y011624D03*
+X015970Y011634D03*
+X015960Y011654D03*
+X015960Y011664D03*
+X015950Y011684D03*
+X015950Y011694D03*
+X015940Y011704D03*
+X015940Y011714D03*
+X015940Y011724D03*
+X015930Y011734D03*
+X015930Y011744D03*
+X015920Y011754D03*
+X015920Y011764D03*
+X014610Y012324D03*
+X014630Y012374D03*
+X014640Y012404D03*
+X014650Y012424D03*
+X014650Y012434D03*
+X014660Y012454D03*
+X014670Y012474D03*
+X014670Y012484D03*
+X014680Y012504D03*
+X014680Y012514D03*
+X014690Y012524D03*
+X014690Y012534D03*
+X014700Y012554D03*
+X014700Y012564D03*
+X014710Y012574D03*
+X014710Y012584D03*
+X014710Y012594D03*
+X014720Y012604D03*
+X014720Y012614D03*
+X014730Y012624D03*
+X014730Y012634D03*
+X014730Y012644D03*
+X014740Y012654D03*
+X014740Y012664D03*
+X014750Y012684D03*
+X014750Y012694D03*
+X014760Y012704D03*
+X014760Y012714D03*
+X014770Y012734D03*
+X014770Y012744D03*
+X014780Y012764D03*
+X014790Y012794D03*
+X014800Y012814D03*
+X014810Y012844D03*
+X014830Y012894D03*
+X013370Y011974D03*
+X019140Y011974D03*
+X019140Y011964D03*
+X019140Y011954D03*
+X019140Y011944D03*
+X019140Y011934D03*
+X019140Y011924D03*
+X019140Y011914D03*
+X019140Y011904D03*
+X019140Y011894D03*
+X019150Y011884D03*
+X019150Y011874D03*
+X019150Y011864D03*
+X019150Y011854D03*
+X019140Y011984D03*
+X019150Y012014D03*
+X019150Y012024D03*
+X019150Y012034D03*
+X019150Y012044D03*
+X019150Y012054D03*
+X019160Y012074D03*
+X019160Y012084D03*
+X019340Y012784D03*
+X019270Y013144D03*
+X019270Y013154D03*
+X005430Y009514D03*
+X005300Y009724D03*
+X005280Y009754D03*
+X005270Y009774D03*
+X005260Y009794D03*
+X005250Y009804D03*
+X005240Y009824D03*
+X005230Y009834D03*
+X005230Y009844D03*
+X005220Y009854D03*
+X005220Y009864D03*
+X005210Y009874D03*
+X005200Y009894D03*
+X004600Y009984D03*
+X004600Y009994D03*
+X004590Y009974D03*
+X004580Y009954D03*
+X004570Y009934D03*
+X005130Y010734D03*
+X005140Y010754D03*
+X004020Y011694D03*
+X004000Y011724D03*
+X003990Y011744D03*
+X003970Y011774D03*
+X003960Y011794D03*
+X003940Y011824D03*
+X003930Y011844D03*
+X003910Y011874D03*
+X003900Y011894D03*
+X003880Y011924D03*
+X003870Y011944D03*
+X003860Y011964D03*
+X003850Y011974D03*
+X003840Y011994D03*
+X003830Y012004D03*
+X003830Y012014D03*
+X003820Y012024D03*
+X003810Y012044D03*
+X003800Y012054D03*
+X003800Y012064D03*
+X003790Y012074D03*
+X003780Y012094D03*
+X003770Y012104D03*
+X003770Y012114D03*
+X003760Y012124D03*
+X003750Y012144D03*
+X003740Y012154D03*
+X003740Y012164D03*
+X003730Y012174D03*
+X003730Y012184D03*
+X003720Y012194D03*
+X003710Y012204D03*
+X003710Y012214D03*
+X003700Y012224D03*
+X003700Y012234D03*
+X003690Y012244D03*
+X003680Y012254D03*
+X003680Y012264D03*
+X003670Y012274D03*
+X003670Y012284D03*
+X003660Y012294D03*
+X003650Y012304D03*
+X003650Y012314D03*
+X003640Y012324D03*
+X003630Y012344D03*
+X003620Y012354D03*
+X003620Y012364D03*
+X003610Y012374D03*
+X003600Y012394D03*
+X003590Y012404D03*
+X003590Y012414D03*
+X003580Y012424D03*
+X003570Y012444D03*
+X003560Y012464D03*
+X003550Y012474D03*
+X003540Y012494D03*
+X003530Y012514D03*
+X003520Y012524D03*
+X003510Y012544D03*
+X003500Y012564D03*
+X003490Y012574D03*
+X003480Y012594D03*
+X003460Y012624D03*
+X003450Y012644D03*
+X003420Y012694D03*
+X003390Y012744D03*
+X003360Y012794D03*
+X003330Y012844D03*
+X003300Y012894D03*
+D25*
+X004865Y010094D03*
+X014535Y007734D03*
+X014535Y007724D03*
+X015575Y008804D03*
+X015575Y008814D03*
+X017645Y007074D03*
+X015155Y013194D03*
+D26*
+X015155Y013114D03*
+X015545Y008714D03*
+X015545Y008704D03*
+X014565Y007834D03*
+X014565Y007824D03*
+X017645Y007084D03*
+X004865Y010044D03*
+D27*
+X004585Y009964D03*
+X004575Y009944D03*
+X004565Y009924D03*
+X004555Y009914D03*
+X004555Y009904D03*
+X004545Y009884D03*
+X005245Y009814D03*
+X005265Y009784D03*
+X005275Y009764D03*
+X005285Y009744D03*
+X005295Y009734D03*
+X005305Y009714D03*
+X005315Y009704D03*
+X005315Y009694D03*
+X005325Y009684D03*
+X005325Y009674D03*
+X005335Y009664D03*
+X005345Y009654D03*
+X005345Y009644D03*
+X005355Y009634D03*
+X005365Y009624D03*
+X005365Y009614D03*
+X005375Y009604D03*
+X005375Y009594D03*
+X005385Y009584D03*
+X005395Y009574D03*
+X005395Y009564D03*
+X005405Y009554D03*
+X005415Y009544D03*
+X005415Y009534D03*
+X005425Y009524D03*
+X005435Y009504D03*
+X005445Y009494D03*
+X005445Y009484D03*
+X005455Y009474D03*
+X005465Y009464D03*
+X005465Y009454D03*
+X005475Y009444D03*
+X005475Y009434D03*
+X005485Y009424D03*
+X005495Y009414D03*
+X005495Y009404D03*
+X005505Y009394D03*
+X005515Y009384D03*
+X005515Y009374D03*
+X005525Y009364D03*
+X005525Y009354D03*
+X005535Y009344D03*
+X005545Y009334D03*
+X005545Y009324D03*
+X005555Y009314D03*
+X005565Y009294D03*
+X005575Y009284D03*
+X005575Y009274D03*
+X005585Y009264D03*
+X005595Y009244D03*
+X005605Y009234D03*
+X005615Y009214D03*
+X005625Y009204D03*
+X005625Y009194D03*
+X005635Y009184D03*
+X005645Y009164D03*
+X005655Y009154D03*
+X005665Y009134D03*
+X005675Y009114D03*
+X005685Y009104D03*
+X005695Y009084D03*
+X005705Y009074D03*
+X005715Y009054D03*
+X005725Y009034D03*
+X005735Y009024D03*
+X005745Y009004D03*
+X005765Y008974D03*
+X005785Y008944D03*
+X005795Y008924D03*
+X005815Y008894D03*
+X005845Y008844D03*
+X005865Y008814D03*
+X005895Y008764D03*
+X005125Y010724D03*
+X005135Y010744D03*
+X005145Y010764D03*
+X005155Y010774D03*
+X005155Y010784D03*
+X005165Y010794D03*
+X005165Y010804D03*
+X005175Y010814D03*
+X005185Y010834D03*
+X005195Y010854D03*
+X003635Y012334D03*
+X003605Y012384D03*
+X003575Y012434D03*
+X003565Y012454D03*
+X003545Y012484D03*
+X003535Y012504D03*
+X003515Y012534D03*
+X003505Y012554D03*
+X003485Y012584D03*
+X003475Y012604D03*
+X003465Y012614D03*
+X003455Y012634D03*
+X003445Y012654D03*
+X003435Y012664D03*
+X003435Y012674D03*
+X003425Y012684D03*
+X003415Y012704D03*
+X003405Y012714D03*
+X003405Y012724D03*
+X003395Y012734D03*
+X003385Y012754D03*
+X003375Y012764D03*
+X003375Y012774D03*
+X003365Y012784D03*
+X003355Y012804D03*
+X003345Y012814D03*
+X003345Y012824D03*
+X003335Y012834D03*
+X003325Y012854D03*
+X003315Y012864D03*
+X003315Y012874D03*
+X003305Y012884D03*
+X003295Y012904D03*
+X003285Y012914D03*
+X003285Y012924D03*
+X003275Y012934D03*
+X003265Y012954D03*
+X003255Y012964D03*
+X003255Y012974D03*
+X003245Y012984D03*
+X003235Y013004D03*
+X003225Y013014D03*
+X003225Y013024D03*
+X003215Y013034D03*
+X003205Y013054D03*
+X003195Y013064D03*
+X003195Y013074D03*
+X003185Y013084D03*
+X003175Y013104D03*
+X003165Y013114D03*
+X003165Y013124D03*
+X003155Y013134D03*
+X003145Y013154D03*
+X003135Y013174D03*
+X003125Y013184D03*
+X003115Y013204D03*
+X003095Y013234D03*
+X003085Y013254D03*
+X003065Y013284D03*
+X003055Y013304D03*
+X003035Y013334D03*
+X003025Y013354D03*
+X003005Y013384D03*
+X002995Y013404D03*
+X002965Y013454D03*
+X002935Y013504D03*
+X013365Y011984D03*
+X014375Y011724D03*
+X014565Y012204D03*
+X014565Y012214D03*
+X014575Y012224D03*
+X014575Y012234D03*
+X014575Y012244D03*
+X014585Y012254D03*
+X014585Y012264D03*
+X014595Y012274D03*
+X014595Y012284D03*
+X014595Y012294D03*
+X014605Y012304D03*
+X014605Y012314D03*
+X014615Y012334D03*
+X014615Y012344D03*
+X014625Y012354D03*
+X014625Y012364D03*
+X014635Y012384D03*
+X014635Y012394D03*
+X014645Y012414D03*
+X014655Y012444D03*
+X014665Y012464D03*
+X014675Y012494D03*
+X014695Y012544D03*
+X015955Y011674D03*
+X015965Y011644D03*
+X015985Y011594D03*
+X015995Y011564D03*
+X016005Y011544D03*
+X016005Y011534D03*
+X016015Y011514D03*
+X016025Y011494D03*
+X016025Y011484D03*
+X016035Y011464D03*
+X016035Y011454D03*
+X016045Y011434D03*
+X016045Y011424D03*
+X016055Y011414D03*
+X016055Y011404D03*
+X016065Y011384D03*
+X016065Y011374D03*
+X016075Y011364D03*
+X016075Y011354D03*
+X016075Y011344D03*
+X016085Y011334D03*
+X016085Y011324D03*
+X016095Y011304D03*
+X016095Y011294D03*
+X016105Y011284D03*
+X016105Y011274D03*
+X016105Y011264D03*
+X016115Y011254D03*
+X016115Y011244D03*
+X016125Y011224D03*
+X016125Y011214D03*
+X015775Y009464D03*
+X015775Y009454D03*
+X016765Y008744D03*
+X016755Y008724D03*
+X016755Y008714D03*
+X016755Y008704D03*
+X016745Y008694D03*
+X016745Y008684D03*
+X016745Y008674D03*
+X016735Y008644D03*
+X016735Y008634D03*
+X016725Y008594D03*
+X016705Y008134D03*
+X016715Y008054D03*
+X016715Y008044D03*
+X016725Y007994D03*
+X016725Y007984D03*
+X016725Y007974D03*
+X016735Y007954D03*
+X016735Y007944D03*
+X016735Y007934D03*
+X016735Y007924D03*
+X016745Y007914D03*
+X016745Y007904D03*
+X016745Y007894D03*
+X016755Y007864D03*
+X018545Y007904D03*
+X018545Y007914D03*
+X018555Y007924D03*
+X018555Y007934D03*
+X018555Y007944D03*
+X018555Y007954D03*
+X018565Y007974D03*
+X018565Y007984D03*
+X018565Y007994D03*
+X018575Y008044D03*
+X018575Y008054D03*
+X018575Y008524D03*
+X018565Y008584D03*
+X018565Y008594D03*
+X018555Y008624D03*
+X018555Y008634D03*
+X018555Y008644D03*
+X018545Y008664D03*
+X018545Y008674D03*
+X018545Y008684D03*
+X018535Y008704D03*
+X018535Y008714D03*
+X018525Y008744D03*
+X019155Y011824D03*
+X019155Y011834D03*
+X019155Y011844D03*
+X019155Y012064D03*
+X019165Y012094D03*
+X019165Y012104D03*
+X019175Y012114D03*
+X019175Y012124D03*
+X019345Y012774D03*
+X019275Y013164D03*
+X014335Y007084D03*
+D28*
+X017640Y007094D03*
+X004870Y010004D03*
+D29*
+X004550Y009894D03*
+X004540Y009874D03*
+X004530Y009864D03*
+X004530Y009854D03*
+X004520Y009844D03*
+X004520Y009834D03*
+X004510Y009824D03*
+X004500Y009804D03*
+X004470Y009754D03*
+X005560Y009304D03*
+X005590Y009254D03*
+X005610Y009224D03*
+X005640Y009174D03*
+X005660Y009144D03*
+X005670Y009124D03*
+X005690Y009094D03*
+X005710Y009064D03*
+X005720Y009044D03*
+X005740Y009014D03*
+X005750Y008994D03*
+X005760Y008984D03*
+X005770Y008964D03*
+X005780Y008954D03*
+X005790Y008934D03*
+X005800Y008914D03*
+X005810Y008904D03*
+X005820Y008884D03*
+X005830Y008874D03*
+X005830Y008864D03*
+X005840Y008854D03*
+X005850Y008834D03*
+X005860Y008824D03*
+X005870Y008804D03*
+X005880Y008794D03*
+X005880Y008784D03*
+X005890Y008774D03*
+X005900Y008754D03*
+X005910Y008744D03*
+X005910Y008734D03*
+X005920Y008724D03*
+X005930Y008714D03*
+X005930Y008704D03*
+X005940Y008694D03*
+X005940Y008684D03*
+X005950Y008674D03*
+X005960Y008664D03*
+X005960Y008654D03*
+X005970Y008644D03*
+X005980Y008634D03*
+X005980Y008624D03*
+X005990Y008614D03*
+X005990Y008604D03*
+X006000Y008594D03*
+X006010Y008584D03*
+X006010Y008574D03*
+X006020Y008564D03*
+X006030Y008544D03*
+X006040Y008534D03*
+X006040Y008524D03*
+X006050Y008514D03*
+X006060Y008504D03*
+X006060Y008494D03*
+X006070Y008484D03*
+X006080Y008464D03*
+X006090Y008454D03*
+X006090Y008444D03*
+X006100Y008434D03*
+X006110Y008414D03*
+X006120Y008404D03*
+X006130Y008384D03*
+X006140Y008374D03*
+X006140Y008364D03*
+X006150Y008354D03*
+X006160Y008334D03*
+X006170Y008324D03*
+X006180Y008304D03*
+X006190Y008284D03*
+X006200Y008274D03*
+X006210Y008254D03*
+X006220Y008244D03*
+X006230Y008224D03*
+X006240Y008204D03*
+X006250Y008194D03*
+X006260Y008174D03*
+X006280Y008144D03*
+X006300Y008114D03*
+X006310Y008094D03*
+X006330Y008064D03*
+X006360Y008014D03*
+X006410Y007934D03*
+X005180Y010824D03*
+X005190Y010844D03*
+X005200Y010864D03*
+X005210Y010874D03*
+X005210Y010884D03*
+X005220Y010894D03*
+X005220Y010904D03*
+X005230Y010914D03*
+X005240Y010934D03*
+X005250Y010954D03*
+X005260Y010964D03*
+X005270Y010984D03*
+X005280Y011004D03*
+X003270Y012944D03*
+X003240Y012994D03*
+X003210Y013044D03*
+X003180Y013094D03*
+X003150Y013144D03*
+X003140Y013164D03*
+X003120Y013194D03*
+X003110Y013214D03*
+X003100Y013224D03*
+X003090Y013244D03*
+X003080Y013264D03*
+X003070Y013274D03*
+X003060Y013294D03*
+X003050Y013314D03*
+X003040Y013324D03*
+X003030Y013344D03*
+X003020Y013364D03*
+X003010Y013374D03*
+X003000Y013394D03*
+X002990Y013414D03*
+X002980Y013424D03*
+X002980Y013434D03*
+X002970Y013444D03*
+X002960Y013464D03*
+X002950Y013474D03*
+X002950Y013484D03*
+X002940Y013494D03*
+X002930Y013514D03*
+X002920Y013524D03*
+X002920Y013534D03*
+X002910Y013544D03*
+X002900Y013554D03*
+X002900Y013564D03*
+X002890Y013574D03*
+X002890Y013584D03*
+X002880Y013594D03*
+X002870Y013604D03*
+X002870Y013614D03*
+X002860Y013624D03*
+X002860Y013634D03*
+X002850Y013644D03*
+X002840Y013654D03*
+X002840Y013664D03*
+X002830Y013674D03*
+X002830Y013684D03*
+X013360Y011994D03*
+X014320Y011594D03*
+X014320Y011584D03*
+X014320Y011574D03*
+X014310Y011564D03*
+X014310Y011554D03*
+X014300Y011534D03*
+X014300Y011524D03*
+X014290Y011514D03*
+X014290Y011504D03*
+X014280Y011484D03*
+X014280Y011474D03*
+X014270Y011454D03*
+X014260Y011434D03*
+X014260Y011424D03*
+X014250Y011404D03*
+X014240Y011384D03*
+X014240Y011374D03*
+X014230Y011354D03*
+X014220Y011324D03*
+X014210Y011304D03*
+X014200Y011274D03*
+X014190Y011254D03*
+X014180Y011224D03*
+X014330Y011604D03*
+X014330Y011614D03*
+X014340Y011624D03*
+X014340Y011634D03*
+X014340Y011644D03*
+X014350Y011654D03*
+X014350Y011664D03*
+X014360Y011674D03*
+X014360Y011684D03*
+X014360Y011694D03*
+X014370Y011704D03*
+X014370Y011714D03*
+X014380Y011734D03*
+X014380Y011744D03*
+X014390Y011754D03*
+X014390Y011764D03*
+X016090Y011314D03*
+X016120Y011234D03*
+X015770Y009444D03*
+X016790Y008804D03*
+X016780Y008784D03*
+X016780Y008774D03*
+X016770Y008764D03*
+X016770Y008754D03*
+X016760Y008734D03*
+X016750Y007884D03*
+X016750Y007874D03*
+X016760Y007854D03*
+X016760Y007844D03*
+X016770Y007824D03*
+X016780Y007794D03*
+X018520Y007824D03*
+X018530Y007844D03*
+X018530Y007854D03*
+X018530Y007864D03*
+X018540Y007874D03*
+X018540Y007884D03*
+X018540Y007894D03*
+X018540Y008694D03*
+X018530Y008724D03*
+X018530Y008734D03*
+X018520Y008754D03*
+X018520Y008764D03*
+X018510Y008784D03*
+X019170Y011784D03*
+X019170Y011794D03*
+X019160Y011804D03*
+X019160Y011814D03*
+X019180Y012134D03*
+X019190Y012144D03*
+X019360Y012764D03*
+X019280Y013174D03*
+X019290Y013184D03*
+X015160Y013684D03*
+X015160Y013694D03*
+X014340Y007104D03*
+X014340Y007094D03*
+D30*
+X017645Y007104D03*
+D31*
+X017645Y007114D03*
+D32*
+X017645Y007124D03*
+D33*
+X017645Y007134D03*
+D34*
+X017645Y007144D03*
+D35*
+X017645Y007154D03*
+D36*
+X017645Y007164D03*
+D37*
+X016840Y007674D03*
+X016830Y007694D03*
+X016840Y008904D03*
+X016850Y008924D03*
+X016860Y008934D03*
+X015750Y009374D03*
+X015750Y009384D03*
+X014360Y007164D03*
+X018450Y007674D03*
+X018460Y007694D03*
+X018450Y008904D03*
+X018440Y008914D03*
+X018440Y008924D03*
+X018430Y008934D03*
+X019200Y011724D03*
+X019230Y012204D03*
+X019240Y012214D03*
+X015160Y013634D03*
+X015160Y013644D03*
+X013340Y012034D03*
+X012420Y012354D03*
+X012420Y012364D03*
+X012410Y012344D03*
+X012410Y012334D03*
+X012400Y012324D03*
+X012390Y012314D03*
+X012390Y012304D03*
+X012380Y012294D03*
+X012370Y012274D03*
+X012360Y012264D03*
+X012350Y012244D03*
+D38*
+X012955Y012584D03*
+X012955Y012594D03*
+X017645Y007174D03*
+D39*
+X016855Y007654D03*
+X016845Y007664D03*
+X016865Y008944D03*
+X015745Y009364D03*
+X014365Y007184D03*
+X014365Y007174D03*
+X018435Y007654D03*
+X018445Y007664D03*
+X018455Y007684D03*
+X018425Y008944D03*
+X018415Y008954D03*
+X019215Y011714D03*
+X019405Y012734D03*
+X019315Y013224D03*
+X019325Y013234D03*
+X015165Y013624D03*
+X013335Y012044D03*
+X012375Y012284D03*
+X012355Y012254D03*
+X012345Y012234D03*
+X012335Y012224D03*
+X012335Y012214D03*
+X012325Y012204D03*
+X012315Y012194D03*
+X012315Y012184D03*
+X012305Y012174D03*
+X012295Y012154D03*
+X012285Y012144D03*
+X012285Y012134D03*
+X012275Y012124D03*
+X012265Y012104D03*
+X012245Y012074D03*
+D40*
+X012935Y012524D03*
+X017645Y007184D03*
+D41*
+X017645Y007194D03*
+X012925Y012494D03*
+D42*
+X013330Y012054D03*
+X012300Y012164D03*
+X012270Y012114D03*
+X012260Y012094D03*
+X012250Y012084D03*
+X012240Y012064D03*
+X012230Y012054D03*
+X012230Y012044D03*
+X012220Y012034D03*
+X012210Y012024D03*
+X012210Y012014D03*
+X012200Y012004D03*
+X012190Y011984D03*
+X012180Y011974D03*
+X012170Y011954D03*
+X012160Y011934D03*
+X012140Y011904D03*
+X015160Y013604D03*
+X015160Y013614D03*
+X019250Y012224D03*
+X019260Y012234D03*
+X019220Y011704D03*
+X018400Y008974D03*
+X018410Y008964D03*
+X018430Y007644D03*
+X018420Y007634D03*
+X016870Y007634D03*
+X016860Y007644D03*
+X016870Y008954D03*
+X016880Y008964D03*
+X016890Y008974D03*
+X015740Y009344D03*
+X015740Y009354D03*
+X014370Y007204D03*
+X014370Y007194D03*
+X004870Y010364D03*
+D43*
+X012905Y012434D03*
+X017645Y007204D03*
+X020235Y011224D03*
+D44*
+X017645Y007214D03*
+X012895Y012404D03*
+D45*
+X013325Y012064D03*
+X012195Y011994D03*
+X012175Y011964D03*
+X012165Y011944D03*
+X012155Y011924D03*
+X012145Y011914D03*
+X012135Y011894D03*
+X012125Y011884D03*
+X012125Y011874D03*
+X012115Y011864D03*
+X012105Y011854D03*
+X012105Y011844D03*
+X012095Y011834D03*
+X012085Y011814D03*
+X012065Y011784D03*
+X012035Y011734D03*
+X015735Y009334D03*
+X015735Y009324D03*
+X016895Y008984D03*
+X016905Y008994D03*
+X016875Y007624D03*
+X016885Y007614D03*
+X018415Y007624D03*
+X014375Y007214D03*
+X019225Y011694D03*
+X019335Y013244D03*
+X004865Y010354D03*
+D46*
+X012885Y012374D03*
+X017645Y007224D03*
+D47*
+X016900Y007594D03*
+X016890Y007604D03*
+X018390Y007594D03*
+X018400Y007604D03*
+X018410Y007614D03*
+X018390Y008984D03*
+X018380Y008994D03*
+X017650Y009544D03*
+X015730Y009314D03*
+X014380Y007234D03*
+X014380Y007224D03*
+X011960Y011614D03*
+X011980Y011644D03*
+X011990Y011664D03*
+X012000Y011674D03*
+X012010Y011694D03*
+X012020Y011704D03*
+X012020Y011714D03*
+X012030Y011724D03*
+X012040Y011744D03*
+X012050Y011754D03*
+X012050Y011764D03*
+X012060Y011774D03*
+X012070Y011794D03*
+X012080Y011804D03*
+X012090Y011824D03*
+X013320Y012074D03*
+X015160Y013584D03*
+X015160Y013594D03*
+X019430Y012724D03*
+X004870Y010384D03*
+X004870Y010374D03*
+X004860Y010344D03*
+D48*
+X017645Y007234D03*
+D49*
+X017645Y007244D03*
+D50*
+X016915Y007574D03*
+X016905Y007584D03*
+X018375Y007574D03*
+X018385Y007584D03*
+X018375Y009004D03*
+X018365Y009014D03*
+X016925Y009014D03*
+X016915Y009004D03*
+X015725Y009294D03*
+X015725Y009304D03*
+X014385Y007244D03*
+X011855Y011444D03*
+X011875Y011474D03*
+X011885Y011494D03*
+X011895Y011504D03*
+X011905Y011524D03*
+X011915Y011534D03*
+X011915Y011544D03*
+X011925Y011554D03*
+X011925Y011564D03*
+X011935Y011574D03*
+X011945Y011584D03*
+X011945Y011594D03*
+X011955Y011604D03*
+X011965Y011624D03*
+X011975Y011634D03*
+X011985Y011654D03*
+X012005Y011684D03*
+X013315Y012084D03*
+X012655Y012764D03*
+X012645Y012774D03*
+X012635Y012784D03*
+X012625Y012794D03*
+X012615Y012804D03*
+X015165Y013574D03*
+X019275Y012244D03*
+X019235Y011684D03*
+X004865Y010334D03*
+D51*
+X017645Y007254D03*
+X020175Y013634D03*
+D52*
+X019350Y013254D03*
+X019290Y012254D03*
+X019250Y011674D03*
+X016930Y009024D03*
+X015720Y009274D03*
+X015720Y009284D03*
+X016920Y007564D03*
+X016930Y007554D03*
+X014390Y007264D03*
+X014390Y007254D03*
+X011770Y011304D03*
+X011780Y011324D03*
+X011790Y011334D03*
+X011800Y011354D03*
+X011810Y011374D03*
+X011820Y011384D03*
+X011830Y011404D03*
+X011840Y011414D03*
+X011840Y011424D03*
+X011850Y011434D03*
+X011860Y011454D03*
+X011870Y011464D03*
+X011880Y011484D03*
+X011900Y011514D03*
+X013310Y012094D03*
+X012690Y012724D03*
+X012680Y012734D03*
+X012670Y012744D03*
+X012660Y012754D03*
+X012610Y012814D03*
+X012600Y012824D03*
+X012590Y012834D03*
+X012580Y012844D03*
+X012570Y012854D03*
+X012560Y012864D03*
+X012550Y012874D03*
+X012540Y012884D03*
+X012530Y012894D03*
+X012520Y012904D03*
+X012510Y012914D03*
+X012500Y012924D03*
+X015160Y013554D03*
+X015160Y013564D03*
+X004870Y010404D03*
+X004870Y010394D03*
+D53*
+X017645Y009314D03*
+X017645Y007264D03*
+X020165Y013624D03*
+D54*
+X020145Y013604D03*
+X020145Y011264D03*
+X017645Y009294D03*
+X017645Y007274D03*
+D55*
+X018355Y007554D03*
+X018365Y007564D03*
+X018355Y009024D03*
+X018345Y009034D03*
+X016945Y009034D03*
+X015715Y009264D03*
+X014395Y007284D03*
+X014395Y007274D03*
+X011715Y011214D03*
+X011725Y011234D03*
+X011735Y011244D03*
+X011735Y011254D03*
+X011745Y011264D03*
+X011755Y011274D03*
+X011755Y011284D03*
+X011765Y011294D03*
+X011775Y011314D03*
+X011795Y011344D03*
+X011805Y011364D03*
+X011825Y011394D03*
+X013305Y012104D03*
+X012495Y012934D03*
+X012485Y012944D03*
+X012475Y012954D03*
+X012465Y012964D03*
+X012455Y012974D03*
+X012445Y012984D03*
+X012435Y012994D03*
+X012425Y013004D03*
+X012415Y013014D03*
+X012405Y013024D03*
+X012395Y013034D03*
+X012385Y013044D03*
+X012375Y013054D03*
+X012375Y013064D03*
+X012365Y013074D03*
+X012355Y013084D03*
+X012345Y013094D03*
+X012335Y013104D03*
+X012325Y013114D03*
+X012315Y013124D03*
+X012305Y013134D03*
+X012295Y013144D03*
+X012285Y013154D03*
+X012275Y013164D03*
+X012265Y013174D03*
+X004865Y010324D03*
+X004865Y010314D03*
+D56*
+X017645Y009284D03*
+X017645Y007284D03*
+X020135Y011274D03*
+X020135Y013594D03*
+D57*
+X020125Y013584D03*
+X020125Y012564D03*
+X017645Y009274D03*
+X017645Y007294D03*
+D58*
+X017645Y007304D03*
+X017645Y009264D03*
+X020115Y012504D03*
+X020115Y012574D03*
+D59*
+X019375Y013264D03*
+X019265Y011664D03*
+X018325Y009054D03*
+X016965Y009054D03*
+X015705Y009224D03*
+X015705Y009234D03*
+X014405Y007314D03*
+X014405Y007304D03*
+X018335Y007534D03*
+X018345Y007544D03*
+X013295Y012124D03*
+X012695Y012704D03*
+X012145Y013304D03*
+X012135Y013314D03*
+X012125Y013324D03*
+X012115Y013334D03*
+X012105Y013344D03*
+X012095Y013354D03*
+X012035Y013424D03*
+X012025Y013434D03*
+X012015Y013444D03*
+X012005Y013454D03*
+X011995Y013464D03*
+X011985Y013474D03*
+X011975Y013484D03*
+X011965Y013494D03*
+X011955Y013504D03*
+X011945Y013514D03*
+X011935Y013524D03*
+X011925Y013534D03*
+X011915Y013544D03*
+X011805Y013664D03*
+X004865Y010304D03*
+D60*
+X017645Y009254D03*
+X017645Y007314D03*
+X020105Y013554D03*
+D61*
+X020095Y013544D03*
+X020095Y012604D03*
+X017645Y009244D03*
+X017645Y007324D03*
+X015155Y012174D03*
+X015155Y012184D03*
+D62*
+X015160Y013504D03*
+X015160Y013514D03*
+X013290Y012134D03*
+X011910Y013554D03*
+X011900Y013564D03*
+X011890Y013574D03*
+X011880Y013584D03*
+X011870Y013594D03*
+X011860Y013604D03*
+X011850Y013614D03*
+X011840Y013624D03*
+X011830Y013634D03*
+X011820Y013644D03*
+X011810Y013654D03*
+X011800Y013674D03*
+X011790Y013684D03*
+X011780Y013694D03*
+X015700Y009214D03*
+X016980Y009064D03*
+X018310Y009064D03*
+X016960Y007524D03*
+X014410Y007334D03*
+X014410Y007324D03*
+X019310Y012264D03*
+X004870Y010434D03*
+X004870Y010424D03*
+D63*
+X012665Y008554D03*
+X012665Y008544D03*
+X012665Y008534D03*
+X012665Y008524D03*
+X012665Y008514D03*
+X012665Y008504D03*
+X012665Y008494D03*
+X012665Y008484D03*
+X012665Y008474D03*
+X012665Y008464D03*
+X012665Y008454D03*
+X012665Y008444D03*
+X012665Y008434D03*
+X012665Y008424D03*
+X012665Y008414D03*
+X012665Y008404D03*
+X012665Y008394D03*
+X012665Y008384D03*
+X012665Y008374D03*
+X012665Y008364D03*
+X012665Y008354D03*
+X012665Y008344D03*
+X012665Y008334D03*
+X012665Y008324D03*
+X012665Y008314D03*
+X012665Y008304D03*
+X012665Y008294D03*
+X012665Y008284D03*
+X012665Y008274D03*
+X012665Y008264D03*
+X012665Y008254D03*
+X012665Y008244D03*
+X012665Y008234D03*
+X012665Y008224D03*
+X012665Y008214D03*
+X012665Y008204D03*
+X012665Y008194D03*
+X012665Y008184D03*
+X012665Y008174D03*
+X012665Y008164D03*
+X012665Y008154D03*
+X012665Y008144D03*
+X017645Y007334D03*
+X017645Y009234D03*
+X020085Y011324D03*
+X020085Y012474D03*
+X020085Y013524D03*
+X017585Y012724D03*
+X017585Y012714D03*
+X017585Y012704D03*
+X017585Y012694D03*
+X017585Y012684D03*
+X017585Y012674D03*
+X017585Y012664D03*
+X017585Y012654D03*
+X017585Y012644D03*
+X017585Y012634D03*
+X017585Y012624D03*
+X017585Y012614D03*
+X017585Y012604D03*
+X017585Y012594D03*
+X017585Y012584D03*
+X017585Y012574D03*
+X017585Y012564D03*
+X017585Y012554D03*
+X017585Y012544D03*
+X017585Y012534D03*
+X017585Y012524D03*
+X017585Y012514D03*
+X017585Y012504D03*
+X017585Y012494D03*
+X017585Y012484D03*
+X017585Y012474D03*
+X017585Y012464D03*
+X017585Y012454D03*
+X017585Y012444D03*
+X017585Y012434D03*
+X017585Y012424D03*
+X017585Y012414D03*
+X017585Y012404D03*
+X017585Y012394D03*
+X017585Y012384D03*
+X017585Y012374D03*
+X017585Y012364D03*
+X017585Y012354D03*
+X017585Y012344D03*
+X017585Y012334D03*
+X017585Y012324D03*
+X017585Y012314D03*
+X017585Y012304D03*
+X015155Y012154D03*
+D64*
+X015155Y012134D03*
+X015155Y012124D03*
+X017645Y009224D03*
+X017645Y007344D03*
+X020075Y011334D03*
+X020075Y012464D03*
+X020075Y012634D03*
+X020075Y013504D03*
+D65*
+X019285Y011654D03*
+X015695Y009204D03*
+X015695Y009194D03*
+X016975Y007514D03*
+X018315Y007514D03*
+X018325Y007524D03*
+X014415Y007344D03*
+X013285Y012144D03*
+X012695Y012694D03*
+X004865Y010294D03*
+X004865Y010284D03*
+D66*
+X015155Y012104D03*
+X017645Y009214D03*
+X017645Y007354D03*
+X020065Y011344D03*
+X020065Y012454D03*
+X020065Y012654D03*
+X020065Y013484D03*
+D67*
+X015160Y013484D03*
+X015160Y013474D03*
+X015160Y013494D03*
+X013280Y012154D03*
+X012690Y012684D03*
+X015690Y009184D03*
+X016990Y009074D03*
+X017650Y009534D03*
+X018300Y009074D03*
+X018300Y007504D03*
+X016990Y007504D03*
+X014420Y007364D03*
+X014420Y007354D03*
+X004870Y010444D03*
+X004870Y010454D03*
+D68*
+X015155Y012074D03*
+X015155Y012084D03*
+X017645Y009204D03*
+X017645Y007374D03*
+X017645Y007364D03*
+X020055Y011364D03*
+X020055Y012444D03*
+X020055Y012674D03*
+X020055Y013464D03*
+D69*
+X015685Y009174D03*
+X015685Y009164D03*
+X017005Y009084D03*
+X018285Y009084D03*
+X014425Y007374D03*
+X013275Y012164D03*
+X013275Y012174D03*
+X012695Y012674D03*
+X004865Y010274D03*
+X004865Y010264D03*
+D70*
+X015155Y012054D03*
+X017645Y009194D03*
+X017645Y007384D03*
+X020045Y012424D03*
+X020045Y012704D03*
+X020045Y013434D03*
+D71*
+X015160Y013454D03*
+X015160Y013464D03*
+X013270Y012184D03*
+X015680Y009154D03*
+X015680Y009144D03*
+X017020Y009094D03*
+X017000Y007494D03*
+X018290Y007494D03*
+X014430Y007394D03*
+X014430Y007384D03*
+X004870Y010464D03*
+D72*
+X015155Y012024D03*
+X017645Y009174D03*
+X017645Y007394D03*
+X020035Y011394D03*
+X020035Y012414D03*
+X020035Y013404D03*
+X020035Y013414D03*
+D73*
+X020025Y013384D03*
+X020025Y013374D03*
+X020025Y012394D03*
+X020025Y011414D03*
+X017645Y009164D03*
+X017645Y007404D03*
+X015155Y012004D03*
+D74*
+X013265Y012194D03*
+X012695Y012664D03*
+X015675Y009134D03*
+X014435Y007414D03*
+X014435Y007404D03*
+X017015Y007484D03*
+X018275Y007484D03*
+X018265Y009094D03*
+X019345Y012274D03*
+X004865Y010254D03*
+D75*
+X015155Y011954D03*
+X017645Y009144D03*
+X017645Y007424D03*
+X020005Y011454D03*
+X020005Y011464D03*
+X020005Y012354D03*
+X020005Y013294D03*
+X020005Y013304D03*
+D76*
+X019310Y011644D03*
+X015670Y009124D03*
+X015670Y009114D03*
+X014440Y007424D03*
+X013260Y012204D03*
+X015160Y013424D03*
+X015160Y013434D03*
+X015160Y013444D03*
+X004870Y010484D03*
+X004870Y010474D03*
+D77*
+X015155Y011924D03*
+X019995Y012334D03*
+X019995Y011484D03*
+X017645Y007434D03*
+D78*
+X018185Y007444D03*
+X017655Y009514D03*
+X015615Y008944D03*
+X015615Y008934D03*
+X014495Y007604D03*
+X014495Y007594D03*
+X013205Y012314D03*
+X015155Y013294D03*
+X004865Y010154D03*
+D79*
+X004870Y010164D03*
+X004870Y010564D03*
+X013210Y012304D03*
+X015160Y013304D03*
+X015160Y013314D03*
+X015620Y008954D03*
+X014490Y007584D03*
+X017100Y007444D03*
+D80*
+X017070Y007454D03*
+X015640Y009014D03*
+X015640Y009024D03*
+X014470Y007524D03*
+X014470Y007514D03*
+X013230Y012264D03*
+X012700Y012624D03*
+X015160Y013354D03*
+X015160Y013364D03*
+X004870Y010534D03*
+X004870Y010524D03*
+D81*
+X004870Y010494D03*
+X012700Y012644D03*
+X013250Y012224D03*
+X015160Y013404D03*
+X015160Y013414D03*
+X015660Y009084D03*
+X014450Y007454D03*
+X018250Y009104D03*
+D82*
+X018230Y009114D03*
+X017060Y009114D03*
+X015650Y009054D03*
+X014460Y007494D03*
+X014460Y007484D03*
+X018240Y007464D03*
+X013240Y012244D03*
+X012700Y012634D03*
+X015160Y013374D03*
+X015160Y013384D03*
+X015160Y013394D03*
+X004870Y010514D03*
+X004870Y010214D03*
+D83*
+X004865Y010224D03*
+X004865Y010504D03*
+X013245Y012234D03*
+X015655Y009074D03*
+X015655Y009064D03*
+X014455Y007474D03*
+X014455Y007464D03*
+X017045Y007464D03*
+X019425Y013274D03*
+D84*
+X017655Y009524D03*
+X015645Y009044D03*
+X015645Y009034D03*
+X014465Y007504D03*
+X013235Y012254D03*
+X004865Y010204D03*
+D85*
+X004870Y010544D03*
+X013220Y012284D03*
+X015160Y013324D03*
+X015160Y013334D03*
+X015630Y008994D03*
+X015630Y008984D03*
+X014480Y007554D03*
+D86*
+X014485Y007564D03*
+X014485Y007574D03*
+X015625Y008964D03*
+X015625Y008974D03*
+X013215Y012294D03*
+X019405Y012284D03*
+X004865Y010554D03*
+X004865Y010174D03*
+D87*
+X004870Y010574D03*
+X004870Y010584D03*
+X013200Y012324D03*
+X015160Y013274D03*
+X015160Y013284D03*
+X015610Y008924D03*
+X015610Y008914D03*
+X014500Y007624D03*
+X014500Y007614D03*
+D88*
+X014510Y007644D03*
+X014510Y007654D03*
+X015600Y008884D03*
+X015600Y008894D03*
+X013190Y012344D03*
+X015160Y013254D03*
+X015160Y013264D03*
+X004870Y010594D03*
+X004870Y010134D03*
+D89*
+X004865Y010124D03*
+X004865Y010604D03*
+X013185Y012354D03*
+X015155Y013244D03*
+X015595Y008874D03*
+X014515Y007664D03*
+D90*
+X014520Y007674D03*
+X014520Y007684D03*
+X015590Y008854D03*
+X015590Y008864D03*
+X013180Y012364D03*
+X015160Y013224D03*
+X015160Y013234D03*
+X004870Y010614D03*
+X004870Y010114D03*
+D91*
+X004865Y010104D03*
+X014525Y007704D03*
+X014525Y007694D03*
+X015585Y008834D03*
+X015585Y008844D03*
+X017655Y009504D03*
+D92*
+X015580Y008824D03*
+X014530Y007714D03*
+X019410Y011634D03*
+X015160Y013204D03*
+X015160Y013214D03*
+X004870Y010634D03*
+X004870Y010624D03*
+D93*
+X004870Y010644D03*
+X004870Y010084D03*
+X014540Y007754D03*
+X014540Y007744D03*
+X015570Y008784D03*
+X015570Y008794D03*
+X015160Y013174D03*
+X015160Y013184D03*
+D94*
+X015155Y013164D03*
+X015565Y008774D03*
+X014545Y007764D03*
+X004865Y010074D03*
+X004865Y010654D03*
+D95*
+X004870Y010664D03*
+X014550Y007784D03*
+X014550Y007774D03*
+X015560Y008754D03*
+X015560Y008764D03*
+X015160Y013154D03*
+D96*
+X015155Y013144D03*
+X017655Y009494D03*
+X015555Y008744D03*
+X014555Y007794D03*
+X004865Y010064D03*
+D97*
+X004870Y010054D03*
+X004870Y010674D03*
+X004870Y010684D03*
+X014560Y007814D03*
+X014560Y007804D03*
+X015550Y008724D03*
+X015550Y008734D03*
+X015160Y013124D03*
+X015160Y013134D03*
+D98*
+X015160Y013104D03*
+X015540Y008694D03*
+X014570Y007844D03*
+X004870Y010034D03*
+X004870Y010694D03*
+D99*
+X004865Y010704D03*
+X004865Y010024D03*
+X014575Y007864D03*
+X014575Y007854D03*
+X015535Y008674D03*
+X015535Y008684D03*
+X015155Y013094D03*
+D100*
+X017650Y009484D03*
+X015530Y008664D03*
+X015530Y008654D03*
+X014580Y007874D03*
+X004870Y010714D03*
+D101*
+X004865Y010014D03*
+X014585Y007894D03*
+X014585Y007884D03*
+X015525Y008644D03*
+D102*
+X014305Y008644D03*
+X014305Y008634D03*
+X014305Y008624D03*
+X014305Y008614D03*
+X014305Y008604D03*
+X014305Y008594D03*
+X014305Y008584D03*
+X014305Y008574D03*
+X014305Y008564D03*
+X014305Y008554D03*
+X014305Y008544D03*
+X014305Y008534D03*
+X014305Y008524D03*
+X014305Y008514D03*
+X014305Y008504D03*
+X014305Y008494D03*
+X014305Y008484D03*
+X014305Y008474D03*
+X014305Y008464D03*
+X014305Y008454D03*
+X014305Y008444D03*
+X014305Y008434D03*
+X014305Y008424D03*
+X014305Y008414D03*
+X014305Y008404D03*
+X014305Y008394D03*
+X014305Y008384D03*
+X014305Y008374D03*
+X014305Y008364D03*
+X014305Y008354D03*
+X014305Y008344D03*
+X014305Y008334D03*
+X014305Y008324D03*
+X014305Y008314D03*
+X014305Y008304D03*
+X014305Y008294D03*
+X014305Y008284D03*
+X014305Y008274D03*
+X014305Y008264D03*
+X014305Y008254D03*
+X014305Y008244D03*
+X014305Y008234D03*
+X014305Y008224D03*
+X014305Y008214D03*
+X014305Y008204D03*
+X014305Y008194D03*
+X014305Y008184D03*
+X014305Y008174D03*
+X014305Y008164D03*
+X014305Y008154D03*
+X014305Y008144D03*
+X014305Y008134D03*
+X014305Y008124D03*
+X014305Y008114D03*
+X014305Y008104D03*
+X014305Y008094D03*
+X014305Y008084D03*
+X014305Y008074D03*
+X014305Y008064D03*
+X014305Y008054D03*
+X014305Y008044D03*
+X014305Y008034D03*
+X014305Y008024D03*
+X014305Y008014D03*
+X014305Y008004D03*
+X014305Y007994D03*
+X014305Y007984D03*
+X014305Y007974D03*
+X014305Y007964D03*
+X014305Y007954D03*
+X014305Y007944D03*
+X014305Y007934D03*
+X014305Y007924D03*
+X014305Y007914D03*
+X014305Y007904D03*
+X014305Y008654D03*
+X014305Y008664D03*
+X014305Y008674D03*
+X014305Y008684D03*
+X014305Y008694D03*
+X014305Y008704D03*
+X014305Y008714D03*
+X014305Y008724D03*
+X014305Y008734D03*
+X014305Y008744D03*
+X014305Y008754D03*
+X014305Y008764D03*
+X014305Y008774D03*
+X014305Y008784D03*
+X014305Y008794D03*
+X014305Y008804D03*
+X014305Y008814D03*
+X014305Y008824D03*
+X014305Y008834D03*
+X014305Y008844D03*
+X014305Y008854D03*
+X014305Y008864D03*
+X014305Y008874D03*
+X014305Y008884D03*
+X014305Y008894D03*
+X014305Y008904D03*
+X014305Y008914D03*
+X014305Y008924D03*
+X014305Y008934D03*
+X014305Y008944D03*
+X014305Y008954D03*
+X014305Y008964D03*
+X014305Y008974D03*
+X014305Y008984D03*
+X014305Y008994D03*
+X014305Y009004D03*
+X014305Y009014D03*
+X014305Y009024D03*
+X014305Y009034D03*
+X014305Y009044D03*
+X014305Y009054D03*
+X014305Y009064D03*
+X014305Y009074D03*
+X014305Y009084D03*
+X014305Y009094D03*
+X014305Y009104D03*
+X014305Y009114D03*
+X014305Y009124D03*
+X014305Y009134D03*
+X014305Y009144D03*
+X014305Y009154D03*
+X014305Y009164D03*
+X014305Y009174D03*
+X014305Y009184D03*
+X014305Y009194D03*
+X014305Y009204D03*
+X014305Y009214D03*
+X014305Y009224D03*
+X014305Y009234D03*
+X014305Y009244D03*
+X014305Y009254D03*
+X014305Y009264D03*
+X014305Y009274D03*
+X014305Y009284D03*
+X014305Y009294D03*
+X014305Y009304D03*
+X014305Y009314D03*
+X014305Y009324D03*
+X014305Y009334D03*
+X014305Y009344D03*
+X014305Y009354D03*
+X014305Y009364D03*
+X014305Y009374D03*
+X014305Y009384D03*
+X014305Y009394D03*
+X014305Y009404D03*
+X014305Y009414D03*
+X014305Y009424D03*
+X014305Y009434D03*
+X014305Y009444D03*
+X014305Y009454D03*
+X014305Y009464D03*
+X014305Y009474D03*
+X014305Y009484D03*
+X014305Y009494D03*
+X014305Y009504D03*
+X014305Y009514D03*
+X014305Y009524D03*
+D103*
+X015150Y011864D03*
+X019970Y011564D03*
+X020060Y009524D03*
+X020060Y009514D03*
+X020060Y009504D03*
+X020060Y009494D03*
+X020060Y009484D03*
+X020060Y009474D03*
+X020060Y009464D03*
+X020060Y009454D03*
+X020060Y009444D03*
+X020060Y009434D03*
+X020060Y009424D03*
+X020060Y009414D03*
+X020060Y009404D03*
+X020060Y009394D03*
+X020060Y009384D03*
+X020060Y009374D03*
+X020060Y009364D03*
+X020060Y009354D03*
+X020060Y009344D03*
+X020060Y009334D03*
+X020060Y009324D03*
+X020060Y009314D03*
+X020060Y009304D03*
+X020060Y009294D03*
+X020060Y009284D03*
+X020060Y009274D03*
+X020060Y009264D03*
+X020060Y009254D03*
+X020060Y009244D03*
+X020060Y009234D03*
+X020060Y009224D03*
+X020060Y009214D03*
+X020060Y009204D03*
+X020060Y009194D03*
+X020060Y009184D03*
+X020060Y009174D03*
+X020060Y009164D03*
+X020060Y009154D03*
+X020060Y009144D03*
+X020060Y009134D03*
+X020060Y009124D03*
+X020060Y009114D03*
+D104*
+X017650Y009184D03*
+X020040Y011384D03*
+X017540Y013274D03*
+X017540Y013284D03*
+X017540Y013294D03*
+X017540Y013304D03*
+X017540Y013314D03*
+X017540Y013324D03*
+X017540Y013334D03*
+X017540Y013344D03*
+X017540Y013354D03*
+X017540Y013364D03*
+X017540Y013374D03*
+X017540Y013384D03*
+X017540Y013394D03*
+X017540Y013404D03*
+X017540Y013414D03*
+X017540Y013424D03*
+X017540Y013434D03*
+X017540Y013444D03*
+X017540Y013454D03*
+X017540Y013464D03*
+X017540Y013474D03*
+X017540Y013484D03*
+X017540Y013494D03*
+X017540Y013504D03*
+X017540Y013514D03*
+X017540Y013524D03*
+X017540Y013534D03*
+X017540Y013544D03*
+X017540Y013554D03*
+X017540Y013564D03*
+X017540Y013574D03*
+X017540Y013584D03*
+X017540Y013594D03*
+X017540Y013604D03*
+X017540Y013614D03*
+X017540Y013624D03*
+X017540Y013634D03*
+X017540Y013644D03*
+X017540Y013654D03*
+X017540Y013664D03*
+X017540Y013674D03*
+X017540Y013684D03*
+X017540Y013694D03*
+X020040Y013424D03*
+X015150Y012044D03*
+X015150Y012034D03*
+X012620Y009524D03*
+X012620Y009514D03*
+X012620Y009504D03*
+X012620Y009494D03*
+X012620Y009484D03*
+X012620Y009474D03*
+X012620Y009464D03*
+X012620Y009454D03*
+X012620Y009444D03*
+X012620Y009434D03*
+X012620Y009424D03*
+X012620Y009414D03*
+X012620Y009404D03*
+X012620Y009394D03*
+X012620Y009384D03*
+X012620Y009374D03*
+X012620Y009364D03*
+X012620Y009354D03*
+X012620Y009344D03*
+X012620Y009334D03*
+X012620Y009324D03*
+X012620Y009314D03*
+X012620Y009304D03*
+X012620Y009294D03*
+X012620Y009284D03*
+X012620Y009274D03*
+X012620Y009264D03*
+X012620Y009254D03*
+X012620Y009244D03*
+X012620Y009234D03*
+X012620Y009224D03*
+X012620Y009214D03*
+X012620Y009204D03*
+X012620Y009194D03*
+X012620Y009184D03*
+X012620Y009174D03*
+X012620Y009164D03*
+X012620Y009154D03*
+X012620Y009144D03*
+X012620Y009134D03*
+X012620Y009124D03*
+X012620Y009114D03*
+D105*
+X017645Y009304D03*
+D106*
+X017650Y009324D03*
+X020180Y011244D03*
+D107*
+X017650Y009334D03*
+X020190Y013644D03*
+D108*
+X020205Y013654D03*
+X020205Y011234D03*
+X017645Y009344D03*
+D109*
+X017650Y009354D03*
+X012890Y012384D03*
+X012890Y012394D03*
+X020220Y013664D03*
+D110*
+X017650Y009364D03*
+X012900Y012414D03*
+X012900Y012424D03*
+D111*
+X012920Y012474D03*
+X012920Y012484D03*
+X017650Y009374D03*
+D112*
+X017650Y009384D03*
+X012930Y012504D03*
+X012930Y012514D03*
+D113*
+X012945Y012554D03*
+X012945Y012564D03*
+X017655Y009394D03*
+D114*
+X017650Y009404D03*
+X012960Y012604D03*
+D115*
+X017650Y009414D03*
+D116*
+X017655Y009424D03*
+D117*
+X017650Y009434D03*
+D118*
+X017650Y009444D03*
+D119*
+X017650Y009454D03*
+D120*
+X017655Y009464D03*
+D121*
+X017655Y009474D03*
+D122*
+X015795Y009524D03*
+X015565Y012704D03*
+X015555Y012734D03*
+X015545Y012754D03*
+X015535Y012784D03*
+X015525Y012814D03*
+X015515Y012834D03*
+X015515Y012844D03*
+X015505Y012864D03*
+X015495Y012884D03*
+X015495Y012894D03*
+X015485Y012914D03*
+X015485Y012924D03*
+X015475Y012934D03*
+X015475Y012944D03*
+X015475Y012954D03*
+X015465Y012964D03*
+X015465Y012974D03*
+X015455Y012984D03*
+X015455Y012994D03*
+X015455Y013004D03*
+X015445Y013014D03*
+X015445Y013024D03*
+X015445Y013034D03*
+X015435Y013044D03*
+X015435Y013054D03*
+X015425Y013074D03*
+X015425Y013084D03*
+X019255Y013084D03*
+X019255Y013074D03*
+X019255Y013064D03*
+X019255Y013054D03*
+X019255Y013044D03*
+X019275Y012884D03*
+X019285Y012864D03*
+X019295Y012844D03*
+X019295Y012834D03*
+X019305Y012824D03*
+X004575Y010754D03*
+X004545Y010804D03*
+X004535Y010824D03*
+X004525Y010834D03*
+X004525Y010844D03*
+X004515Y010854D03*
+X004505Y010874D03*
+X004495Y010884D03*
+X004495Y010894D03*
+X004485Y010904D03*
+X004485Y010914D03*
+X004475Y010924D03*
+X004465Y010944D03*
+X004455Y010954D03*
+X004455Y010964D03*
+X004445Y010974D03*
+X004435Y010994D03*
+X004425Y011014D03*
+X004395Y011064D03*
+D123*
+X017650Y009564D03*
+D124*
+X017650Y009574D03*
+D125*
+X009690Y010104D03*
+X009690Y010114D03*
+X009690Y010124D03*
+X009690Y010134D03*
+X009690Y010144D03*
+X009690Y010154D03*
+X009690Y010164D03*
+X009690Y010174D03*
+X009690Y010184D03*
+X009690Y010194D03*
+X009690Y010204D03*
+X009690Y010214D03*
+X009690Y010224D03*
+X009690Y010234D03*
+X009690Y010244D03*
+X009690Y010254D03*
+X009690Y010264D03*
+X009690Y010274D03*
+X009690Y010284D03*
+X009690Y010294D03*
+X009690Y010304D03*
+X009690Y010314D03*
+X009690Y010324D03*
+X009690Y010334D03*
+X009690Y010344D03*
+X009690Y010354D03*
+X009690Y010364D03*
+X009690Y010374D03*
+X009690Y010384D03*
+X009690Y010394D03*
+X009690Y010404D03*
+X009690Y010414D03*
+X009690Y010424D03*
+X009690Y010434D03*
+X009690Y010444D03*
+X009690Y010454D03*
+X009690Y010464D03*
+X009690Y010474D03*
+X009690Y010484D03*
+X009690Y010494D03*
+X009690Y010504D03*
+X009690Y010514D03*
+X009690Y010524D03*
+D126*
+X004590Y010724D03*
+X004590Y010734D03*
+X004580Y010744D03*
+X004570Y010764D03*
+X004560Y010774D03*
+X004560Y010784D03*
+X004550Y010794D03*
+X004540Y010814D03*
+X004510Y010864D03*
+X015430Y013064D03*
+X019250Y013034D03*
+X019250Y013024D03*
+X019250Y013014D03*
+X019250Y013004D03*
+X019250Y012994D03*
+X019250Y012984D03*
+X019250Y012974D03*
+X019250Y012964D03*
+X019260Y012954D03*
+X019260Y012944D03*
+X019260Y012934D03*
+X019260Y012924D03*
+X019260Y012914D03*
+X019270Y012904D03*
+X019270Y012894D03*
+X019280Y012874D03*
+X019290Y012854D03*
+D127*
+X020320Y011214D03*
+D128*
+X020160Y011254D03*
+D129*
+X020120Y011284D03*
+X020120Y013574D03*
+D130*
+X020110Y013564D03*
+X020110Y012584D03*
+X020110Y011294D03*
+D131*
+X020100Y011304D03*
+X020100Y012494D03*
+X020100Y012594D03*
+X015150Y012194D03*
+D132*
+X015150Y012164D03*
+X020090Y012484D03*
+X020090Y012614D03*
+X020090Y013534D03*
+X020090Y011314D03*
+D133*
+X020060Y011354D03*
+X020060Y012664D03*
+X020060Y013474D03*
+X015150Y012094D03*
+D134*
+X015150Y012064D03*
+X020050Y012434D03*
+X020050Y012684D03*
+X020050Y012694D03*
+X020050Y013444D03*
+X020050Y013454D03*
+X020050Y011374D03*
+D135*
+X020030Y011404D03*
+X020030Y012404D03*
+X020030Y013394D03*
+X015150Y012014D03*
+D136*
+X015150Y011994D03*
+X015150Y011984D03*
+X020020Y011424D03*
+X020020Y013354D03*
+X020020Y013364D03*
+D137*
+X020010Y013324D03*
+X020010Y013314D03*
+X020010Y012374D03*
+X020010Y012364D03*
+X020010Y011444D03*
+X015150Y011964D03*
+D138*
+X015150Y011944D03*
+X015150Y011934D03*
+X020000Y012344D03*
+X020000Y011474D03*
+X020000Y013284D03*
+D139*
+X019990Y012324D03*
+X019990Y011504D03*
+X019990Y011494D03*
+X015150Y011914D03*
+D140*
+X015155Y011904D03*
+X019985Y011514D03*
+X019985Y012314D03*
+D141*
+X019980Y012304D03*
+X019980Y011534D03*
+X019980Y011524D03*
+X015150Y011884D03*
+X015150Y011894D03*
+D142*
+X015155Y011874D03*
+X019975Y011554D03*
+X019975Y011544D03*
+X019975Y012294D03*
+D143*
+X019965Y011584D03*
+X019965Y011574D03*
+X015155Y011854D03*
+D144*
+X015150Y011844D03*
+X015150Y011834D03*
+X019960Y011604D03*
+X019960Y011594D03*
+D145*
+X019955Y011614D03*
+X019955Y011624D03*
+X015155Y011824D03*
+D146*
+X015155Y011774D03*
+D147*
+X015150Y011784D03*
+X015150Y011794D03*
+D148*
+X015150Y011804D03*
+X015150Y011814D03*
+D149*
+X015150Y012114D03*
+X020070Y012644D03*
+X020070Y013494D03*
+D150*
+X020080Y013514D03*
+X020080Y012624D03*
+X015150Y012144D03*
+D151*
+X012910Y012444D03*
+X012910Y012454D03*
+D152*
+X012915Y012464D03*
+X020245Y013674D03*
+D153*
+X020130Y012554D03*
+X020130Y012514D03*
+D154*
+X020140Y012524D03*
+X020140Y012544D03*
+D155*
+X020150Y012534D03*
+X020150Y013614D03*
+D156*
+X012940Y012544D03*
+X012940Y012534D03*
+D157*
+X012950Y012574D03*
+X020280Y013684D03*
+D158*
+X009595Y013684D03*
+X009595Y013674D03*
+X009595Y013664D03*
+X009595Y013654D03*
+X009595Y013644D03*
+X009595Y013634D03*
+X009595Y013624D03*
+X009595Y013614D03*
+X009595Y013604D03*
+X009595Y013594D03*
+X009595Y013584D03*
+X009595Y013574D03*
+X009595Y013564D03*
+X009595Y013554D03*
+X009595Y013544D03*
+X009595Y013534D03*
+X009595Y013524D03*
+X009595Y013514D03*
+X009595Y013504D03*
+X009595Y013494D03*
+X009595Y013484D03*
+X009595Y013474D03*
+X009595Y013464D03*
+X009595Y013454D03*
+X009595Y013444D03*
+X009595Y013434D03*
+X009595Y013424D03*
+X009595Y013414D03*
+X009595Y013404D03*
+X009595Y013394D03*
+X009595Y013384D03*
+X009595Y013374D03*
+X009595Y013364D03*
+X009595Y013354D03*
+X009595Y013344D03*
+X009595Y013334D03*
+X009595Y013324D03*
+X009595Y013314D03*
+X009595Y013304D03*
+X009595Y013294D03*
+X009595Y013284D03*
+X009595Y013274D03*
+X009595Y013264D03*
+D159*
+X020345Y013694D03*
+D160*
+X022869Y013789D02*
+X022869Y007639D01*
+M02*
diff --git a/gerber/tests/resources/example_am_exposure_modifier.gbr b/gerber/tests/resources/example_am_exposure_modifier.gbr
new file mode 100644
index 0000000..5f3f3dd
--- /dev/null
+++ b/gerber/tests/resources/example_am_exposure_modifier.gbr
@@ -0,0 +1,16 @@
+G04 Umaco example for exposure modifier and clearing area*
+%FSLAX26Y26*%
+%MOIN*%
+%AMSQUAREWITHHOLE*
+21,0.1,1,1,0,0,0*
+1,0,0.5,0,0*%
+%ADD10SQUAREWITHHOLE*%
+%ADD11C,1*%
+G01*
+%LPD*%
+D11*
+X-1000000Y-250000D02*
+X1000000Y250000D01*
+D10*
+X0Y0D03*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_coincident_hole.gbr b/gerber/tests/resources/example_coincident_hole.gbr
new file mode 100644
index 0000000..4f896ea
--- /dev/null
+++ b/gerber/tests/resources/example_coincident_hole.gbr
@@ -0,0 +1,24 @@
+G04 ex2: overlapping*
+%FSLAX24Y24*%
+%MOMM*%
+%SRX1Y1I0.000J0.000*%
+%ADD10C,1.00000*%
+G01*
+%LPD*%
+G36*
+X0Y50000D02*
+Y100000D01*
+X100000D01*
+Y0D01*
+X0D01*
+Y50000D01*
+G04 first fully coincident linear segment*
+X10000D01*
+X50000Y10000D01*
+X90000Y50000D01*
+X50000Y90000D01*
+X10000Y50000D01*
+G04 second fully coincident linear segment*
+X0D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_cutin.gbr b/gerber/tests/resources/example_cutin.gbr
new file mode 100644
index 0000000..365e5e1
--- /dev/null
+++ b/gerber/tests/resources/example_cutin.gbr
@@ -0,0 +1,18 @@
+G04 Umaco uut-in example*
+%FSLAX24Y24*%
+G75*
+G36*
+X20000Y100000D02*
+G01*
+X120000D01*
+Y20000D01*
+X20000D01*
+Y60000D01*
+X50000D01*
+G03*
+X50000Y60000I30000J0D01*
+G01*
+X20000D01*
+Y100000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_cutin_multiple.gbr b/gerber/tests/resources/example_cutin_multiple.gbr
new file mode 100644
index 0000000..8e19429
--- /dev/null
+++ b/gerber/tests/resources/example_cutin_multiple.gbr
@@ -0,0 +1,28 @@
+G04 multiple cutins*
+%FSLAX24Y24*%
+%MOMM*%
+%SRX1Y1I0.000J0.000*%
+%ADD10C,1.00000*%
+%LPD*%
+G36*
+X1220000Y2570000D02*
+G01*
+Y2720000D01*
+X1310000D01*
+Y2570000D01*
+X1250000D01*
+Y2600000D01*
+X1290000D01*
+Y2640000D01*
+X1250000D01*
+Y2670000D01*
+X1290000D01*
+Y2700000D01*
+X1250000D01*
+Y2670000D01*
+Y2640000D01*
+Y2600000D01*
+Y2570000D01*
+X1220000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_flash_circle.gbr b/gerber/tests/resources/example_flash_circle.gbr
new file mode 100644
index 0000000..20b2566
--- /dev/null
+++ b/gerber/tests/resources/example_flash_circle.gbr
@@ -0,0 +1,10 @@
+G04 Flashes of circular apertures*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10C,0.5*%
+%ADD11C,0.5X0.25*%
+D10*
+X000000Y000000D03*
+D11*
+X010000D03*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_flash_obround.gbr b/gerber/tests/resources/example_flash_obround.gbr
new file mode 100644
index 0000000..5313f82
--- /dev/null
+++ b/gerber/tests/resources/example_flash_obround.gbr
@@ -0,0 +1,10 @@
+G04 Flashes of rectangular apertures*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10O,0.46X0.26*%
+%ADD11O,0.46X0.26X0.19*%
+D10*
+X000000Y000000D03*
+D11*
+X010000D03*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_flash_polygon.gbr b/gerber/tests/resources/example_flash_polygon.gbr
new file mode 100644
index 0000000..177cf9b
--- /dev/null
+++ b/gerber/tests/resources/example_flash_polygon.gbr
@@ -0,0 +1,10 @@
+G04 Flashes of rectangular apertures*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10P,.40X6*%
+%ADD11P,.40X6X0.0X0.19*%
+D10*
+X000000Y000000D03*
+D11*
+X010000D03*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_flash_rectangle.gbr b/gerber/tests/resources/example_flash_rectangle.gbr
new file mode 100644
index 0000000..8fde812
--- /dev/null
+++ b/gerber/tests/resources/example_flash_rectangle.gbr
@@ -0,0 +1,10 @@
+G04 Flashes of rectangular apertures*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10R,0.44X0.25*%
+%ADD11R,0.44X0.25X0.19*%
+D10*
+X000000Y000000D03*
+D11*
+X010000D03*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_fully_coincident.gbr b/gerber/tests/resources/example_fully_coincident.gbr
new file mode 100644
index 0000000..3764128
--- /dev/null
+++ b/gerber/tests/resources/example_fully_coincident.gbr
@@ -0,0 +1,23 @@
+G04 ex1: non overlapping*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10C,1.00000*%
+G01*
+%LPD*%
+G36*
+X0Y50000D02*
+Y100000D01*
+X100000D01*
+Y0D01*
+X0D01*
+Y50000D01*
+G04 first fully coincident linear segment*
+X-10000D01*
+X-50000Y10000D01*
+X-90000Y50000D01*
+X-50000Y90000D01*
+X-10000Y50000D01*
+G04 second fully coincident linear segment*
+X0D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_guess_by_content.g0 b/gerber/tests/resources/example_guess_by_content.g0
new file mode 100644
index 0000000..5b26afe
--- /dev/null
+++ b/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/gerber/tests/resources/example_holes_dont_clear.gbr b/gerber/tests/resources/example_holes_dont_clear.gbr
new file mode 100644
index 0000000..deeebd0
--- /dev/null
+++ b/gerber/tests/resources/example_holes_dont_clear.gbr
@@ -0,0 +1,13 @@
+G04 Demonstrates that apertures with holes do not clear the area - only the aperture hole*
+%FSLAX26Y26*%
+%MOIN*%
+%ADD10C,1X0.5*%
+%ADD11C,0.1*%
+G01*
+%LPD*%
+D11*
+X-1000000Y-250000D02*
+X1000000Y250000D01*
+D10*
+X0Y0D03*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_level_holes.gbr b/gerber/tests/resources/example_level_holes.gbr
new file mode 100644
index 0000000..1b4e189
--- /dev/null
+++ b/gerber/tests/resources/example_level_holes.gbr
@@ -0,0 +1,39 @@
+G04 This file illustrates how to use levels to create holes*
+%FSLAX25Y25*%
+%MOMM*%
+G01*
+G04 First level: big square - dark polarity*
+%LPD*%
+G36*
+X250000Y250000D02*
+X1750000D01*
+Y1750000D01*
+X250000D01*
+Y250000D01*
+G37*
+G04 Second level: big circle - clear polarity*
+%LPC*%
+G36*
+G75*
+X500000Y1000000D02*
+G03*
+X500000Y1000000I500000J0D01*
+G37*
+G04 Third level: small square - dark polarity*
+%LPD*%
+G36*
+X750000Y750000D02*
+X1250000D01*
+Y1250000D01*
+X750000D01*
+Y750000D01*
+G37*
+G04 Fourth level: small circle - clear polarity*
+%LPC*%
+G36*
+G75*
+X1150000Y1000000D02*
+G03*
+X1150000Y1000000I250000J0D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_not_overlapping_contour.gbr b/gerber/tests/resources/example_not_overlapping_contour.gbr
new file mode 100644
index 0000000..e3ea631
--- /dev/null
+++ b/gerber/tests/resources/example_not_overlapping_contour.gbr
@@ -0,0 +1,20 @@
+G04 Non-overlapping contours*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10C,1.00000*%
+G01*
+%LPD*%
+G36*
+X0Y50000D02*
+Y100000D01*
+X100000D01*
+Y0D01*
+X0D01*
+Y50000D01*
+X-10000D02*
+X-50000Y10000D01*
+X-90000Y50000D01*
+X-50000Y90000D01*
+X-10000Y50000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_not_overlapping_touching.gbr b/gerber/tests/resources/example_not_overlapping_touching.gbr
new file mode 100644
index 0000000..3b9b955
--- /dev/null
+++ b/gerber/tests/resources/example_not_overlapping_touching.gbr
@@ -0,0 +1,20 @@
+G04 Non-overlapping and touching*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10C,1.00000*%
+G01*
+%LPD*%
+G36*
+X0Y50000D02*
+Y100000D01*
+X100000D01*
+Y0D01*
+X0D01*
+Y50000D01*
+D02*
+X-50000Y10000D01*
+X-90000Y50000D01*
+X-50000Y90000D01*
+X0Y50000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_overlapping_contour.gbr b/gerber/tests/resources/example_overlapping_contour.gbr
new file mode 100644
index 0000000..74886a2
--- /dev/null
+++ b/gerber/tests/resources/example_overlapping_contour.gbr
@@ -0,0 +1,20 @@
+G04 Overlapping contours*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10C,1.00000*%
+G01*
+%LPD*%
+G36*
+X0Y50000D02*
+Y100000D01*
+X100000D01*
+Y0D01*
+X0D01*
+Y50000D01*
+X10000D02*
+X50000Y10000D01*
+X90000Y50000D01*
+X50000Y90000D01*
+X10000Y50000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_overlapping_touching.gbr b/gerber/tests/resources/example_overlapping_touching.gbr
new file mode 100644
index 0000000..27fce15
--- /dev/null
+++ b/gerber/tests/resources/example_overlapping_touching.gbr
@@ -0,0 +1,20 @@
+G04 Overlapping and touching*
+%FSLAX24Y24*%
+%MOMM*%
+%ADD10C,1.00000*%
+G01*
+%LPD*%
+G36*
+X0Y50000D02*
+Y100000D01*
+X100000D01*
+Y0D01*
+X0D01*
+Y50000D01*
+D02*
+X50000Y10000D01*
+X90000Y50000D01*
+X50000Y90000D01*
+X0Y50000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_simple_contour.gbr b/gerber/tests/resources/example_simple_contour.gbr
new file mode 100644
index 0000000..d851760
--- /dev/null
+++ b/gerber/tests/resources/example_simple_contour.gbr
@@ -0,0 +1,16 @@
+G04 Ucamco ex. 4.6.4: Simple contour*
+%FSLAX25Y25*%
+%MOIN*%
+%ADD10C,0.010*%
+G36*
+X200000Y300000D02*
+G01*
+X700000D01*
+Y100000D01*
+X1100000Y500000D01*
+X700000Y900000D01*
+Y700000D01*
+X200000D01*
+Y300000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_single_contour_1.gbr b/gerber/tests/resources/example_single_contour_1.gbr
new file mode 100644
index 0000000..e9f9a75
--- /dev/null
+++ b/gerber/tests/resources/example_single_contour_1.gbr
@@ -0,0 +1,15 @@
+G04 Ucamco ex. 4.6.5: Single contour #1*
+%FSLAX25Y25*%
+%MOMM*%
+%ADD11C,0.01*%
+G01*
+D11*
+X3000Y5000D01*
+G36*
+X50000Y50000D02*
+X60000D01*
+Y60000D01*
+X50000D01*
+Y50000Y50000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_single_contour_2.gbr b/gerber/tests/resources/example_single_contour_2.gbr
new file mode 100644
index 0000000..085c72c
--- /dev/null
+++ b/gerber/tests/resources/example_single_contour_2.gbr
@@ -0,0 +1,15 @@
+G04 Ucamco ex. 4.6.5: Single contour #2*
+%FSLAX25Y25*%
+%MOMM*%
+%ADD11C,0.01*%
+G01*
+D11*
+X3000Y5000D01*
+X50000Y50000D02*
+G36*
+X60000D01*
+Y60000D01*
+X50000D01*
+Y50000Y50000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_single_contour_3.gbr b/gerber/tests/resources/example_single_contour_3.gbr
new file mode 100644
index 0000000..40de149
--- /dev/null
+++ b/gerber/tests/resources/example_single_contour_3.gbr
@@ -0,0 +1,15 @@
+G04 Ucamco ex. 4.6.5: Single contour #2*
+%FSLAX25Y25*%
+%MOMM*%
+%ADD11C,0.01*%
+G01*
+D11*
+X3000Y5000D01*
+X50000Y50000D01*
+G36*
+X60000D01*
+Y60000D01*
+X50000D01*
+Y50000Y50000D01*
+G37*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_single_quadrant.gbr b/gerber/tests/resources/example_single_quadrant.gbr
new file mode 100644
index 0000000..c398601
--- /dev/null
+++ b/gerber/tests/resources/example_single_quadrant.gbr
@@ -0,0 +1,18 @@
+G04 Ucamco ex. 4.5.8: Single quadrant*
+%FSLAX23Y23*%
+%MOIN*%
+%ADD10C,0.010*%
+G74*
+D10*
+X1100Y600D02*
+G03*
+X700Y1000I400J0D01*
+X300Y600I0J400D01*
+X700Y200I400J0D01*
+X1100Y600I0J400D01*
+X300D02*
+G01*
+X1100D01*
+X700Y200D02*
+Y1000D01*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/example_two_square_boxes.gbr b/gerber/tests/resources/example_two_square_boxes.gbr
new file mode 100644
index 0000000..54a8ac1
--- /dev/null
+++ b/gerber/tests/resources/example_two_square_boxes.gbr
@@ -0,0 +1,19 @@
+G04 Ucamco ex. 1: Two square boxes*
+%FSLAX25Y25*%
+%MOMM*%
+%TF.Part,Other*%
+%LPD*%
+%ADD10C,0.010*%
+D10*
+X0Y0D02*
+G01*
+X500000Y0D01*
+Y500000D01*
+X0D01*
+Y0D01*
+X600000D02*
+X1100000D01*
+Y500000D01*
+X600000D01*
+Y0D01*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/ipc-d-356.ipc b/gerber/tests/resources/ipc-d-356.ipc
new file mode 100644
index 0000000..2ed3f49
--- /dev/null
+++ b/gerber/tests/resources/ipc-d-356.ipc
@@ -0,0 +1,115 @@
+C IPC-D-356 generated by EAGLE Version 7.1.0 Copyright (c) 1988-2014 CadSoft
+C Database /Some/Path/To/File
+C
+P JOB EAGLE 7.1 NETLIST, DATE: 2/20/15 12:00 AM
+P UNITS CUST 0
+P DIM N
+P NNAME1 A_REALLY_LONG_NET_NAME
+317GND VIA D 24PA00X 14900Y 1450X 396Y 396
+317GND VIA D 24PA00X 3850Y 8500X 396Y 396
+317GND VIA D 24PA00X 6200Y 10650X 396Y 396
+317GND VIA D 24PA00X 8950Y 1000X 396Y 396
+317GND VIA D 24PA00X 11800Y 2250X 396Y 396
+317GND VIA D 24PA00X 15350Y 3200X 396Y 396
+317GND VIA D 24PA00X 13200Y 3800X 396Y 396
+317GND VIA D 24PA00X 9700Y 12050X 396Y 396
+317GND VIA D 24PA00X 13950Y 11900X 396Y 396
+317GND VIA D 24PA00X 13050Y 7050X 396Y 396
+317GND VIA D 24PA00X 13000Y 8400X 396Y 396
+317N$3 VIA D 24PA00X 11350Y 10100X 396Y 396
+317N$3 VIA D 24PA00X 13250Y 5700X 396Y 396
+317VCC VIA D 24PA00X 15550Y 6850X 396Y 396
+327N$3 C1 -+ A01X 9700Y 10402X1575Y 630R270
+327GND C1 -- A01X 9700Y 13198X1575Y 630R270
+327VCC C2 -+ A01X 13950Y 9677X1535Y 630R270
+327GND C2 -- A01X 13950Y 13023X1535Y 630R270
+327VCC C3 -1 A01X 3850Y 9924X 512Y 591R270
+327GND C3 -2 A01X 3850Y 9176X 512Y 591R270
+327VCC C4 -1 A01X 10374Y 1000X 512Y 591R180
+327GND C4 -2 A01X 9626Y 1000X 512Y 591R180
+327VCC C5 -1 A01X 14700Y 3924X 512Y 591R270
+327GND C5 -2 A01X 14700Y 3176X 512Y 591R270
+317DMX+ DMX -1 D 40PA00X 5050Y 13900X 600Y1200R 90
+317DMX- DMX -2 D 40PA00X 6050Y 13900X 600Y1200R 90
+317GND DMX -3 D 40PA00X 7050Y 13900X 600Y1200R 90
+317PIC_MCLR J1 -1 D 35PA00X 16900Y 6400X 554Y 554R 90
+317VCC J1 -2 D 35PA00X 17900Y 6900X 554Y 554R 90
+317GND J1 -3 D 35PA00X 16900Y 7400X 554Y 554R 90
+317PIC_PGD J1 -4 D 35PA00X 17900Y 7900X 554Y 554R 90
+317PIC_PGC J1 -5 D 35PA00X 16900Y 8400X 554Y 554R 90
+317 J1 -6 D 35PA00X 17900Y 8900X 554Y 554R 90
+327N$4 L1 -1 A01X 13950Y 6382X 748Y1339R 90
+327VCC L1 -2 A01X 13950Y 7918X 748Y1339R 90
+327N$5 LED1 -A A01X 16313Y 1450X 472Y 472R 0
+327GND LED1 -C A01X 15487Y 1450X 472Y 472R 0
+317 MIDI -1 D 40PA00X 1200Y 9500X 600Y1200R 0
+317 MIDI -2 D 40PA00X 1200Y 8500X 600Y1200R 0
+317 MIDI -3 D 40PA00X 1200Y 7500X 600Y1200R 0
+317N$9 MIDI -4 D 40PA00X 1200Y 6500X 600Y1200R 0
+317N$10 MIDI -5 D 40PA00X 1200Y 5500X 600Y1200R 0
+317N$3 PWR -1 D 40PA00X 17050Y 13750X 600Y1200R 90
+317GND PWR -2 D 40PA00X 18050Y 13750X 600Y1200R 90
+327DMX+ R1 -1 A01X 5076Y 11500X 512Y 591R 0
+327DMX- R1 -2 A01X 5824Y 11500X 512Y 591R 0
+327VCC R2 -1 A01X 14376Y 5300X 512Y 591R 0
+327PIC_MCLR R2 -2 A01X 15124Y 5300X 512Y 591R 0
+327N$9 R3 -1 A01X 3126Y 6500X 512Y 591R 0
+327N$6 R3 -2 A01X 3874Y 6500X 512Y 591R 0
+327PIC_RX R4 -1 A01X 9600Y 2624X 512Y 591R270
+327VCC R4 -2 A01X 9600Y 1876X 512Y 591R270
+327VCC R5 -1 A01X 17974Y 1450X 512Y 591R180
+327N$5 R5 -2 A01X 17226Y 1450X 512Y 591R180
+327N$3 U1 -1 A01X 12330Y 5710X 420Y 850R 90
+327N$4 U1 -2 A01X 12330Y 6380X 420Y 850R 90
+327GND U1 -3 A01X 12330Y 7050X 420Y 850R 90
+327VCC U1 -4 A01X 12330Y 7720X 420Y 850R 90
+327GND U1 -5 A01X 12330Y 8390X 420Y 850R 90
+327 U1 -6 A01X 9050Y 7050X4252Y4098R 90
+327PIC_MCLR U2 -1 A01X 11123Y 4063X 157Y 591R270
+327 U2 -2 A01X 11123Y 3807X 157Y 591R270
+327 U2 -3 A01X 11123Y 3552X 157Y 591R270
+327N$1 U2 -4 A01X 11123Y 3296X 157Y 591R270
+327N$2 U2 -5 A01X 11123Y 3040X 157Y 591R270
+327PIC_RX U2 -6 A01X 11123Y 2784X 157Y 591R270
+327 U2 -7 A01X 11123Y 2528X 157Y 591R270
+327GND U2 -8 A01X 11123Y 2272X 157Y 591R270
+327 U2 -9 A01X 11123Y 2016X 157Y 591R270
+327 U2 -10 A01X 11123Y 1760X 157Y 591R270
+327 U2 -11 A01X 11123Y 1504X 157Y 591R270
+327 U2 -12 A01X 11123Y 1248X 157Y 591R270
+327VCC U2 -13 A01X 11123Y 993X 157Y 591R270
+327 U2 -14 A01X 11123Y 737X 157Y 591R270
+327 U2 -15 A01X 13977Y 737X 157Y 591R270
+327 U2 -16 A01X 13977Y 993X 157Y 591R270
+327 U2 -17 A01X 13977Y 1248X 157Y 591R270
+327 U2 -18 A01X 13977Y 1504X 157Y 591R270
+327 U2 -19 A01X 13977Y 1760X 157Y 591R270
+327 U2 -20 A01X 13977Y 2016X 157Y 591R270
+327PIC_PGD U2 -21 A01X 13977Y 2272X 157Y 591R270
+327PIC_PGC U2 -22 A01X 13977Y 2528X 157Y 591R270
+327 U2 -23 A01X 13977Y 2784X 157Y 591R270
+327 U2 -24 A01X 13977Y 3040X 157Y 591R270
+327 U2 -25 A01X 13977Y 3296X 157Y 591R270
+327 U2 -26 A01X 13977Y 3552X 157Y 591R270
+327GND U2 -27 A01X 13977Y 3807X 157Y 591R270
+327VCC U2 -28 A01X 13977Y 4063X 157Y 591R270
+327N$2 U3 -1 A01X 4700Y 7540X 260Y 800R 0
+327VCC U3 -2 A01X 5200Y 7540X 260Y 800R 0
+327VCC U3 -3 A01X 5700Y 7540X 260Y 800R 0
+327N$1 U3 -4 A01X 6200Y 7540X 260Y 800R 0
+327GND U3 -5 A01X 6200Y 9960X 260Y 800R 0
+327DMX- U3 -6 A01X 5700Y 9960X 260Y 800R 0
+327DMX+ U3 -7 A01X 5200Y 9960X 260Y 800R 0
+327VCC U3 -8 A01X 4700Y 9960X 260Y 800R 0
+327 U4 -1 A01X 4704Y 3850X 394Y 500R 0
+327N$6 U4 -2 A01X 4704Y 2800X 394Y 500R 0
+327N$10 U4 -3 A01X 4704Y 1800X 394Y 500R 0
+327 U4 -4 A01X 4704Y 750X 394Y 500R 0
+327GND U4 -5 A01X 8396Y 750X 394Y 500R 0
+327PIC_RX U4 -6 A01X 8396Y 1800X 394Y 500R 0
+327 U4 -7 A01X 8396Y 2800X 394Y 500R 0
+327VCC U4 -8 A01X 8396Y 3850X 394Y 500R 0
+327NNAME1 NA -69 A01X 8396Y 3850X 394Y 500R 0
+389BOARD_EDGE X0Y0 X22500 Y15000 X0
+089 X1300Y240
+999
diff --git a/gerber/tests/resources/multiline_read.ger b/gerber/tests/resources/multiline_read.ger
new file mode 100644
index 0000000..02242e4
--- /dev/null
+++ b/gerber/tests/resources/multiline_read.ger
@@ -0,0 +1,9 @@
+G75*
+G71*
+%OFA0B0*%
+%FSLAX23Y23*%
+%IPPOS*%
+%LPD*%
+%ADD10C,0.1*%
+%LPD*%D10*
+M02* \ No newline at end of file
diff --git a/gerber/tests/resources/ncdrill.DRD b/gerber/tests/resources/ncdrill.DRD
new file mode 100644
index 0000000..ced00ca
--- /dev/null
+++ b/gerber/tests/resources/ncdrill.DRD
@@ -0,0 +1,51 @@
+%
+M48
+M72
+T01C0.0236
+T02C0.0354
+T03C0.0400
+T04C0.1260
+T05C0.1280
+%
+T01
+X9250Y4064
+X12100Y5314
+X13500Y6864
+X15650Y6264
+X15200Y4514
+X13550Y8764
+X13350Y10114
+X13300Y11464
+X11650Y13164
+X10000Y15114
+X6500Y13714
+X4150Y11564
+X14250Y14964
+X15850Y9914
+T02
+X17200Y9464
+X18200Y9964
+X18200Y10964
+X17200Y10464
+X17200Y11464
+X18200Y11964
+T03
+X18350Y16814
+X17350Y16814
+X7350Y16964
+X6350Y16964
+X5350Y16964
+X1500Y12564
+X1500Y11564
+X1500Y10564
+X1500Y9564
+X1500Y8564
+T04
+X2350Y5114
+X2300Y16064
+X20800Y16064
+X20800Y5064
+T05
+X20700Y8714
+X20700Y12714
+M30
diff --git a/gerber/tests/resources/top_copper.GTL b/gerber/tests/resources/top_copper.GTL
new file mode 100644
index 0000000..01c848e
--- /dev/null
+++ b/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/gerber/tests/resources/top_mask.GTS b/gerber/tests/resources/top_mask.GTS
new file mode 100644
index 0000000..a3886f5
--- /dev/null
+++ b/gerber/tests/resources/top_mask.GTS
@@ -0,0 +1,162 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239X$1,22.5*
+%
+%ADD10R,0.0340X0.0880*%
+%ADD11R,0.0671X0.0237*%
+%ADD12R,0.4178X0.4332*%
+%ADD13R,0.0930X0.0500*%
+%ADD14R,0.0710X0.1655*%
+%ADD15R,0.0671X0.0592*%
+%ADD16R,0.0592X0.0671*%
+%ADD17R,0.0710X0.1615*%
+%ADD18R,0.1419X0.0828*%
+%ADD19C,0.0634*%
+%ADD20C,0.1360*%
+%ADD21R,0.0474X0.0580*%
+%ADD22C,0.0680*%
+%ADD23R,0.0552X0.0552*%
+%ADD24C,0.1340*%
+%ADD25C,0.0476*%
+D10*
+X005000Y010604D03*
+X005500Y010604D03*
+X006000Y010604D03*
+X006500Y010604D03*
+X006500Y013024D03*
+X006000Y013024D03*
+X005500Y013024D03*
+X005000Y013024D03*
+D11*
+X011423Y007128D03*
+X011423Y006872D03*
+X011423Y006616D03*
+X011423Y006360D03*
+X011423Y006104D03*
+X011423Y005848D03*
+X011423Y005592D03*
+X011423Y005336D03*
+X011423Y005080D03*
+X011423Y004825D03*
+X011423Y004569D03*
+X011423Y004313D03*
+X011423Y004057D03*
+X011423Y003801D03*
+X014277Y003801D03*
+X014277Y004057D03*
+X014277Y004313D03*
+X014277Y004569D03*
+X014277Y004825D03*
+X014277Y005080D03*
+X014277Y005336D03*
+X014277Y005592D03*
+X014277Y005848D03*
+X014277Y006104D03*
+X014277Y006360D03*
+X014277Y006616D03*
+X014277Y006872D03*
+X014277Y007128D03*
+D12*
+X009350Y010114D03*
+D13*
+X012630Y010114D03*
+X012630Y010784D03*
+X012630Y011454D03*
+X012630Y009444D03*
+X012630Y008774D03*
+D14*
+X010000Y013467D03*
+X010000Y016262D03*
+D15*
+X004150Y012988D03*
+X004150Y012240D03*
+X009900Y005688D03*
+X009900Y004940D03*
+X015000Y006240D03*
+X015000Y006988D03*
+D16*
+X014676Y008364D03*
+X015424Y008364D03*
+X017526Y004514D03*
+X018274Y004514D03*
+X010674Y004064D03*
+X009926Y004064D03*
+X004174Y009564D03*
+X003426Y009564D03*
+X005376Y014564D03*
+X006124Y014564D03*
+D17*
+X014250Y016088D03*
+X014250Y012741D03*
+D18*
+X014250Y010982D03*
+X014250Y009447D03*
+D19*
+X017200Y009464D03*
+X018200Y009964D03*
+X018200Y010964D03*
+X017200Y010464D03*
+X017200Y011464D03*
+X018200Y011964D03*
+D20*
+X020700Y012714D03*
+X020700Y008714D03*
+D21*
+X005004Y003814D03*
+X005004Y004864D03*
+X005004Y005864D03*
+X005004Y006914D03*
+X008696Y006914D03*
+X008696Y005864D03*
+X008696Y004864D03*
+X008696Y003814D03*
+D22*
+X001800Y008564D02*
+X001200Y008564D01*
+X001200Y009564D02*
+X001800Y009564D01*
+X001800Y010564D02*
+X001200Y010564D01*
+X001200Y011564D02*
+X001800Y011564D01*
+X001800Y012564D02*
+X001200Y012564D01*
+X005350Y016664D02*
+X005350Y017264D01*
+X006350Y017264D02*
+X006350Y016664D01*
+X007350Y016664D02*
+X007350Y017264D01*
+X017350Y017114D02*
+X017350Y016514D01*
+X018350Y016514D02*
+X018350Y017114D01*
+D23*
+X016613Y004514D03*
+X015787Y004514D03*
+D24*
+X020800Y005064D03*
+X020800Y016064D03*
+X002300Y016064D03*
+X002350Y005114D03*
+D25*
+X009250Y004064D03*
+X012100Y005314D03*
+X013500Y006864D03*
+X015650Y006264D03*
+X015200Y004514D03*
+X013550Y008764D03*
+X013350Y010114D03*
+X013300Y011464D03*
+X011650Y013164D03*
+X010000Y015114D03*
+X006500Y013714D03*
+X004150Y011564D03*
+X014250Y014964D03*
+X015850Y009914D03*
+M02*
diff --git a/gerber/tests/resources/top_silk.GTO b/gerber/tests/resources/top_silk.GTO
new file mode 100644
index 0000000..ea46f80
--- /dev/null
+++ b/gerber/tests/resources/top_silk.GTO
@@ -0,0 +1,2099 @@
+G75*
+%MOIN*%
+%OFA0B0*%
+%FSLAX24Y24*%
+%IPPOS*%
+%LPD*%
+%AMOC8*
+5,1,8,0,0,1.08239X$1,22.5*
+%
+%ADD10C,0.0000*%
+%ADD11C,0.0060*%
+%ADD12C,0.0020*%
+%ADD13C,0.0050*%
+%ADD14C,0.0080*%
+%ADD15C,0.0040*%
+%ADD16R,0.0660X0.0380*%
+%ADD17C,0.0030*%
+%ADD18C,0.0004*%
+%ADD19R,0.0450X0.0364*%
+%ADD20C,0.0025*%
+%ADD21C,0.0098*%
+D10*
+X000300Y003064D02*
+X000300Y018064D01*
+X022800Y018064D01*
+X022800Y003064D01*
+X000300Y003064D01*
+X001720Y005114D02*
+X001722Y005164D01*
+X001728Y005214D01*
+X001738Y005263D01*
+X001752Y005311D01*
+X001769Y005358D01*
+X001790Y005403D01*
+X001815Y005447D01*
+X001843Y005488D01*
+X001875Y005527D01*
+X001909Y005564D01*
+X001946Y005598D01*
+X001986Y005628D01*
+X002028Y005655D01*
+X002072Y005679D01*
+X002118Y005700D01*
+X002165Y005716D01*
+X002213Y005729D01*
+X002263Y005738D01*
+X002312Y005743D01*
+X002363Y005744D01*
+X002413Y005741D01*
+X002462Y005734D01*
+X002511Y005723D01*
+X002559Y005708D01*
+X002605Y005690D01*
+X002650Y005668D01*
+X002693Y005642D01*
+X002734Y005613D01*
+X002773Y005581D01*
+X002809Y005546D01*
+X002841Y005508D01*
+X002871Y005468D01*
+X002898Y005425D01*
+X002921Y005381D01*
+X002940Y005335D01*
+X002956Y005287D01*
+X002968Y005238D01*
+X002976Y005189D01*
+X002980Y005139D01*
+X002980Y005089D01*
+X002976Y005039D01*
+X002968Y004990D01*
+X002956Y004941D01*
+X002940Y004893D01*
+X002921Y004847D01*
+X002898Y004803D01*
+X002871Y004760D01*
+X002841Y004720D01*
+X002809Y004682D01*
+X002773Y004647D01*
+X002734Y004615D01*
+X002693Y004586D01*
+X002650Y004560D01*
+X002605Y004538D01*
+X002559Y004520D01*
+X002511Y004505D01*
+X002462Y004494D01*
+X002413Y004487D01*
+X002363Y004484D01*
+X002312Y004485D01*
+X002263Y004490D01*
+X002213Y004499D01*
+X002165Y004512D01*
+X002118Y004528D01*
+X002072Y004549D01*
+X002028Y004573D01*
+X001986Y004600D01*
+X001946Y004630D01*
+X001909Y004664D01*
+X001875Y004701D01*
+X001843Y004740D01*
+X001815Y004781D01*
+X001790Y004825D01*
+X001769Y004870D01*
+X001752Y004917D01*
+X001738Y004965D01*
+X001728Y005014D01*
+X001722Y005064D01*
+X001720Y005114D01*
+X001670Y016064D02*
+X001672Y016114D01*
+X001678Y016164D01*
+X001688Y016213D01*
+X001702Y016261D01*
+X001719Y016308D01*
+X001740Y016353D01*
+X001765Y016397D01*
+X001793Y016438D01*
+X001825Y016477D01*
+X001859Y016514D01*
+X001896Y016548D01*
+X001936Y016578D01*
+X001978Y016605D01*
+X002022Y016629D01*
+X002068Y016650D01*
+X002115Y016666D01*
+X002163Y016679D01*
+X002213Y016688D01*
+X002262Y016693D01*
+X002313Y016694D01*
+X002363Y016691D01*
+X002412Y016684D01*
+X002461Y016673D01*
+X002509Y016658D01*
+X002555Y016640D01*
+X002600Y016618D01*
+X002643Y016592D01*
+X002684Y016563D01*
+X002723Y016531D01*
+X002759Y016496D01*
+X002791Y016458D01*
+X002821Y016418D01*
+X002848Y016375D01*
+X002871Y016331D01*
+X002890Y016285D01*
+X002906Y016237D01*
+X002918Y016188D01*
+X002926Y016139D01*
+X002930Y016089D01*
+X002930Y016039D01*
+X002926Y015989D01*
+X002918Y015940D01*
+X002906Y015891D01*
+X002890Y015843D01*
+X002871Y015797D01*
+X002848Y015753D01*
+X002821Y015710D01*
+X002791Y015670D01*
+X002759Y015632D01*
+X002723Y015597D01*
+X002684Y015565D01*
+X002643Y015536D01*
+X002600Y015510D01*
+X002555Y015488D01*
+X002509Y015470D01*
+X002461Y015455D01*
+X002412Y015444D01*
+X002363Y015437D01*
+X002313Y015434D01*
+X002262Y015435D01*
+X002213Y015440D01*
+X002163Y015449D01*
+X002115Y015462D01*
+X002068Y015478D01*
+X002022Y015499D01*
+X001978Y015523D01*
+X001936Y015550D01*
+X001896Y015580D01*
+X001859Y015614D01*
+X001825Y015651D01*
+X001793Y015690D01*
+X001765Y015731D01*
+X001740Y015775D01*
+X001719Y015820D01*
+X001702Y015867D01*
+X001688Y015915D01*
+X001678Y015964D01*
+X001672Y016014D01*
+X001670Y016064D01*
+X020060Y012714D02*
+X020062Y012764D01*
+X020068Y012814D01*
+X020078Y012863D01*
+X020091Y012912D01*
+X020109Y012959D01*
+X020130Y013005D01*
+X020154Y013048D01*
+X020182Y013090D01*
+X020213Y013130D01*
+X020247Y013167D01*
+X020284Y013201D01*
+X020324Y013232D01*
+X020366Y013260D01*
+X020409Y013284D01*
+X020455Y013305D01*
+X020502Y013323D01*
+X020551Y013336D01*
+X020600Y013346D01*
+X020650Y013352D01*
+X020700Y013354D01*
+X020750Y013352D01*
+X020800Y013346D01*
+X020849Y013336D01*
+X020898Y013323D01*
+X020945Y013305D01*
+X020991Y013284D01*
+X021034Y013260D01*
+X021076Y013232D01*
+X021116Y013201D01*
+X021153Y013167D01*
+X021187Y013130D01*
+X021218Y013090D01*
+X021246Y013048D01*
+X021270Y013005D01*
+X021291Y012959D01*
+X021309Y012912D01*
+X021322Y012863D01*
+X021332Y012814D01*
+X021338Y012764D01*
+X021340Y012714D01*
+X021338Y012664D01*
+X021332Y012614D01*
+X021322Y012565D01*
+X021309Y012516D01*
+X021291Y012469D01*
+X021270Y012423D01*
+X021246Y012380D01*
+X021218Y012338D01*
+X021187Y012298D01*
+X021153Y012261D01*
+X021116Y012227D01*
+X021076Y012196D01*
+X021034Y012168D01*
+X020991Y012144D01*
+X020945Y012123D01*
+X020898Y012105D01*
+X020849Y012092D01*
+X020800Y012082D01*
+X020750Y012076D01*
+X020700Y012074D01*
+X020650Y012076D01*
+X020600Y012082D01*
+X020551Y012092D01*
+X020502Y012105D01*
+X020455Y012123D01*
+X020409Y012144D01*
+X020366Y012168D01*
+X020324Y012196D01*
+X020284Y012227D01*
+X020247Y012261D01*
+X020213Y012298D01*
+X020182Y012338D01*
+X020154Y012380D01*
+X020130Y012423D01*
+X020109Y012469D01*
+X020091Y012516D01*
+X020078Y012565D01*
+X020068Y012614D01*
+X020062Y012664D01*
+X020060Y012714D01*
+X020170Y016064D02*
+X020172Y016114D01*
+X020178Y016164D01*
+X020188Y016213D01*
+X020202Y016261D01*
+X020219Y016308D01*
+X020240Y016353D01*
+X020265Y016397D01*
+X020293Y016438D01*
+X020325Y016477D01*
+X020359Y016514D01*
+X020396Y016548D01*
+X020436Y016578D01*
+X020478Y016605D01*
+X020522Y016629D01*
+X020568Y016650D01*
+X020615Y016666D01*
+X020663Y016679D01*
+X020713Y016688D01*
+X020762Y016693D01*
+X020813Y016694D01*
+X020863Y016691D01*
+X020912Y016684D01*
+X020961Y016673D01*
+X021009Y016658D01*
+X021055Y016640D01*
+X021100Y016618D01*
+X021143Y016592D01*
+X021184Y016563D01*
+X021223Y016531D01*
+X021259Y016496D01*
+X021291Y016458D01*
+X021321Y016418D01*
+X021348Y016375D01*
+X021371Y016331D01*
+X021390Y016285D01*
+X021406Y016237D01*
+X021418Y016188D01*
+X021426Y016139D01*
+X021430Y016089D01*
+X021430Y016039D01*
+X021426Y015989D01*
+X021418Y015940D01*
+X021406Y015891D01*
+X021390Y015843D01*
+X021371Y015797D01*
+X021348Y015753D01*
+X021321Y015710D01*
+X021291Y015670D01*
+X021259Y015632D01*
+X021223Y015597D01*
+X021184Y015565D01*
+X021143Y015536D01*
+X021100Y015510D01*
+X021055Y015488D01*
+X021009Y015470D01*
+X020961Y015455D01*
+X020912Y015444D01*
+X020863Y015437D01*
+X020813Y015434D01*
+X020762Y015435D01*
+X020713Y015440D01*
+X020663Y015449D01*
+X020615Y015462D01*
+X020568Y015478D01*
+X020522Y015499D01*
+X020478Y015523D01*
+X020436Y015550D01*
+X020396Y015580D01*
+X020359Y015614D01*
+X020325Y015651D01*
+X020293Y015690D01*
+X020265Y015731D01*
+X020240Y015775D01*
+X020219Y015820D01*
+X020202Y015867D01*
+X020188Y015915D01*
+X020178Y015964D01*
+X020172Y016014D01*
+X020170Y016064D01*
+X020060Y008714D02*
+X020062Y008764D01*
+X020068Y008814D01*
+X020078Y008863D01*
+X020091Y008912D01*
+X020109Y008959D01*
+X020130Y009005D01*
+X020154Y009048D01*
+X020182Y009090D01*
+X020213Y009130D01*
+X020247Y009167D01*
+X020284Y009201D01*
+X020324Y009232D01*
+X020366Y009260D01*
+X020409Y009284D01*
+X020455Y009305D01*
+X020502Y009323D01*
+X020551Y009336D01*
+X020600Y009346D01*
+X020650Y009352D01*
+X020700Y009354D01*
+X020750Y009352D01*
+X020800Y009346D01*
+X020849Y009336D01*
+X020898Y009323D01*
+X020945Y009305D01*
+X020991Y009284D01*
+X021034Y009260D01*
+X021076Y009232D01*
+X021116Y009201D01*
+X021153Y009167D01*
+X021187Y009130D01*
+X021218Y009090D01*
+X021246Y009048D01*
+X021270Y009005D01*
+X021291Y008959D01*
+X021309Y008912D01*
+X021322Y008863D01*
+X021332Y008814D01*
+X021338Y008764D01*
+X021340Y008714D01*
+X021338Y008664D01*
+X021332Y008614D01*
+X021322Y008565D01*
+X021309Y008516D01*
+X021291Y008469D01*
+X021270Y008423D01*
+X021246Y008380D01*
+X021218Y008338D01*
+X021187Y008298D01*
+X021153Y008261D01*
+X021116Y008227D01*
+X021076Y008196D01*
+X021034Y008168D01*
+X020991Y008144D01*
+X020945Y008123D01*
+X020898Y008105D01*
+X020849Y008092D01*
+X020800Y008082D01*
+X020750Y008076D01*
+X020700Y008074D01*
+X020650Y008076D01*
+X020600Y008082D01*
+X020551Y008092D01*
+X020502Y008105D01*
+X020455Y008123D01*
+X020409Y008144D01*
+X020366Y008168D01*
+X020324Y008196D01*
+X020284Y008227D01*
+X020247Y008261D01*
+X020213Y008298D01*
+X020182Y008338D01*
+X020154Y008380D01*
+X020130Y008423D01*
+X020109Y008469D01*
+X020091Y008516D01*
+X020078Y008565D01*
+X020068Y008614D01*
+X020062Y008664D01*
+X020060Y008714D01*
+X020170Y005064D02*
+X020172Y005114D01*
+X020178Y005164D01*
+X020188Y005213D01*
+X020202Y005261D01*
+X020219Y005308D01*
+X020240Y005353D01*
+X020265Y005397D01*
+X020293Y005438D01*
+X020325Y005477D01*
+X020359Y005514D01*
+X020396Y005548D01*
+X020436Y005578D01*
+X020478Y005605D01*
+X020522Y005629D01*
+X020568Y005650D01*
+X020615Y005666D01*
+X020663Y005679D01*
+X020713Y005688D01*
+X020762Y005693D01*
+X020813Y005694D01*
+X020863Y005691D01*
+X020912Y005684D01*
+X020961Y005673D01*
+X021009Y005658D01*
+X021055Y005640D01*
+X021100Y005618D01*
+X021143Y005592D01*
+X021184Y005563D01*
+X021223Y005531D01*
+X021259Y005496D01*
+X021291Y005458D01*
+X021321Y005418D01*
+X021348Y005375D01*
+X021371Y005331D01*
+X021390Y005285D01*
+X021406Y005237D01*
+X021418Y005188D01*
+X021426Y005139D01*
+X021430Y005089D01*
+X021430Y005039D01*
+X021426Y004989D01*
+X021418Y004940D01*
+X021406Y004891D01*
+X021390Y004843D01*
+X021371Y004797D01*
+X021348Y004753D01*
+X021321Y004710D01*
+X021291Y004670D01*
+X021259Y004632D01*
+X021223Y004597D01*
+X021184Y004565D01*
+X021143Y004536D01*
+X021100Y004510D01*
+X021055Y004488D01*
+X021009Y004470D01*
+X020961Y004455D01*
+X020912Y004444D01*
+X020863Y004437D01*
+X020813Y004434D01*
+X020762Y004435D01*
+X020713Y004440D01*
+X020663Y004449D01*
+X020615Y004462D01*
+X020568Y004478D01*
+X020522Y004499D01*
+X020478Y004523D01*
+X020436Y004550D01*
+X020396Y004580D01*
+X020359Y004614D01*
+X020325Y004651D01*
+X020293Y004690D01*
+X020265Y004731D01*
+X020240Y004775D01*
+X020219Y004820D01*
+X020202Y004867D01*
+X020188Y004915D01*
+X020178Y004964D01*
+X020172Y005014D01*
+X020170Y005064D01*
+D11*
+X019450Y005064D02*
+X019452Y005137D01*
+X019458Y005210D01*
+X019468Y005282D01*
+X019482Y005354D01*
+X019499Y005425D01*
+X019521Y005495D01*
+X019546Y005564D01*
+X019575Y005631D01*
+X019607Y005696D01*
+X019643Y005760D01*
+X019683Y005822D01*
+X019725Y005881D01*
+X019771Y005938D01*
+X019820Y005992D01*
+X019872Y006044D01*
+X019926Y006093D01*
+X019983Y006139D01*
+X020042Y006181D01*
+X020104Y006221D01*
+X020168Y006257D01*
+X020233Y006289D01*
+X020300Y006318D01*
+X020369Y006343D01*
+X020439Y006365D01*
+X020510Y006382D01*
+X020582Y006396D01*
+X020654Y006406D01*
+X020727Y006412D01*
+X020800Y006414D01*
+X020873Y006412D01*
+X020946Y006406D01*
+X021018Y006396D01*
+X021090Y006382D01*
+X021161Y006365D01*
+X021231Y006343D01*
+X021300Y006318D01*
+X021367Y006289D01*
+X021432Y006257D01*
+X021496Y006221D01*
+X021558Y006181D01*
+X021617Y006139D01*
+X021674Y006093D01*
+X021728Y006044D01*
+X021780Y005992D01*
+X021829Y005938D01*
+X021875Y005881D01*
+X021917Y005822D01*
+X021957Y005760D01*
+X021993Y005696D01*
+X022025Y005631D01*
+X022054Y005564D01*
+X022079Y005495D01*
+X022101Y005425D01*
+X022118Y005354D01*
+X022132Y005282D01*
+X022142Y005210D01*
+X022148Y005137D01*
+X022150Y005064D01*
+X022148Y004991D01*
+X022142Y004918D01*
+X022132Y004846D01*
+X022118Y004774D01*
+X022101Y004703D01*
+X022079Y004633D01*
+X022054Y004564D01*
+X022025Y004497D01*
+X021993Y004432D01*
+X021957Y004368D01*
+X021917Y004306D01*
+X021875Y004247D01*
+X021829Y004190D01*
+X021780Y004136D01*
+X021728Y004084D01*
+X021674Y004035D01*
+X021617Y003989D01*
+X021558Y003947D01*
+X021496Y003907D01*
+X021432Y003871D01*
+X021367Y003839D01*
+X021300Y003810D01*
+X021231Y003785D01*
+X021161Y003763D01*
+X021090Y003746D01*
+X021018Y003732D01*
+X020946Y003722D01*
+X020873Y003716D01*
+X020800Y003714D01*
+X020727Y003716D01*
+X020654Y003722D01*
+X020582Y003732D01*
+X020510Y003746D01*
+X020439Y003763D01*
+X020369Y003785D01*
+X020300Y003810D01*
+X020233Y003839D01*
+X020168Y003871D01*
+X020104Y003907D01*
+X020042Y003947D01*
+X019983Y003989D01*
+X019926Y004035D01*
+X019872Y004084D01*
+X019820Y004136D01*
+X019771Y004190D01*
+X019725Y004247D01*
+X019683Y004306D01*
+X019643Y004368D01*
+X019607Y004432D01*
+X019575Y004497D01*
+X019546Y004564D01*
+X019521Y004633D01*
+X019499Y004703D01*
+X019482Y004774D01*
+X019468Y004846D01*
+X019458Y004918D01*
+X019452Y004991D01*
+X019450Y005064D01*
+X019798Y007044D02*
+X019904Y007044D01*
+X020011Y007151D01*
+X020011Y007685D01*
+X019904Y007685D02*
+X020118Y007685D01*
+X020335Y007471D02*
+X020549Y007685D01*
+X020549Y007044D01*
+X020762Y007044D02*
+X020335Y007044D01*
+X019798Y007044D02*
+X019691Y007151D01*
+X019450Y016064D02*
+X019452Y016137D01*
+X019458Y016210D01*
+X019468Y016282D01*
+X019482Y016354D01*
+X019499Y016425D01*
+X019521Y016495D01*
+X019546Y016564D01*
+X019575Y016631D01*
+X019607Y016696D01*
+X019643Y016760D01*
+X019683Y016822D01*
+X019725Y016881D01*
+X019771Y016938D01*
+X019820Y016992D01*
+X019872Y017044D01*
+X019926Y017093D01*
+X019983Y017139D01*
+X020042Y017181D01*
+X020104Y017221D01*
+X020168Y017257D01*
+X020233Y017289D01*
+X020300Y017318D01*
+X020369Y017343D01*
+X020439Y017365D01*
+X020510Y017382D01*
+X020582Y017396D01*
+X020654Y017406D01*
+X020727Y017412D01*
+X020800Y017414D01*
+X020873Y017412D01*
+X020946Y017406D01*
+X021018Y017396D01*
+X021090Y017382D01*
+X021161Y017365D01*
+X021231Y017343D01*
+X021300Y017318D01*
+X021367Y017289D01*
+X021432Y017257D01*
+X021496Y017221D01*
+X021558Y017181D01*
+X021617Y017139D01*
+X021674Y017093D01*
+X021728Y017044D01*
+X021780Y016992D01*
+X021829Y016938D01*
+X021875Y016881D01*
+X021917Y016822D01*
+X021957Y016760D01*
+X021993Y016696D01*
+X022025Y016631D01*
+X022054Y016564D01*
+X022079Y016495D01*
+X022101Y016425D01*
+X022118Y016354D01*
+X022132Y016282D01*
+X022142Y016210D01*
+X022148Y016137D01*
+X022150Y016064D01*
+X022148Y015991D01*
+X022142Y015918D01*
+X022132Y015846D01*
+X022118Y015774D01*
+X022101Y015703D01*
+X022079Y015633D01*
+X022054Y015564D01*
+X022025Y015497D01*
+X021993Y015432D01*
+X021957Y015368D01*
+X021917Y015306D01*
+X021875Y015247D01*
+X021829Y015190D01*
+X021780Y015136D01*
+X021728Y015084D01*
+X021674Y015035D01*
+X021617Y014989D01*
+X021558Y014947D01*
+X021496Y014907D01*
+X021432Y014871D01*
+X021367Y014839D01*
+X021300Y014810D01*
+X021231Y014785D01*
+X021161Y014763D01*
+X021090Y014746D01*
+X021018Y014732D01*
+X020946Y014722D01*
+X020873Y014716D01*
+X020800Y014714D01*
+X020727Y014716D01*
+X020654Y014722D01*
+X020582Y014732D01*
+X020510Y014746D01*
+X020439Y014763D01*
+X020369Y014785D01*
+X020300Y014810D01*
+X020233Y014839D01*
+X020168Y014871D01*
+X020104Y014907D01*
+X020042Y014947D01*
+X019983Y014989D01*
+X019926Y015035D01*
+X019872Y015084D01*
+X019820Y015136D01*
+X019771Y015190D01*
+X019725Y015247D01*
+X019683Y015306D01*
+X019643Y015368D01*
+X019607Y015432D01*
+X019575Y015497D01*
+X019546Y015564D01*
+X019521Y015633D01*
+X019499Y015703D01*
+X019482Y015774D01*
+X019468Y015846D01*
+X019458Y015918D01*
+X019452Y015991D01*
+X019450Y016064D01*
+X018850Y016564D02*
+X018600Y016314D01*
+X018100Y016314D01*
+X017850Y016564D01*
+X017600Y016314D01*
+X017100Y016314D01*
+X016850Y016564D01*
+X016850Y017064D01*
+X017100Y017314D01*
+X017600Y017314D01*
+X017850Y017064D01*
+X018100Y017314D01*
+X018600Y017314D01*
+X018850Y017064D01*
+X018850Y016564D01*
+X017850Y016564D02*
+X017850Y017064D01*
+X007850Y017214D02*
+X007850Y016714D01*
+X007600Y016464D01*
+X007100Y016464D01*
+X006850Y016714D01*
+X006600Y016464D01*
+X006100Y016464D01*
+X005850Y016714D01*
+X005600Y016464D01*
+X005100Y016464D01*
+X004850Y016714D01*
+X004850Y017214D01*
+X005100Y017464D01*
+X005600Y017464D01*
+X005850Y017214D01*
+X006100Y017464D01*
+X006600Y017464D01*
+X006850Y017214D01*
+X007100Y017464D01*
+X007600Y017464D01*
+X007850Y017214D01*
+X006850Y017214D02*
+X006850Y016714D01*
+X005850Y016714D02*
+X005850Y017214D01*
+X000950Y016064D02*
+X000952Y016137D01*
+X000958Y016210D01*
+X000968Y016282D01*
+X000982Y016354D01*
+X000999Y016425D01*
+X001021Y016495D01*
+X001046Y016564D01*
+X001075Y016631D01*
+X001107Y016696D01*
+X001143Y016760D01*
+X001183Y016822D01*
+X001225Y016881D01*
+X001271Y016938D01*
+X001320Y016992D01*
+X001372Y017044D01*
+X001426Y017093D01*
+X001483Y017139D01*
+X001542Y017181D01*
+X001604Y017221D01*
+X001668Y017257D01*
+X001733Y017289D01*
+X001800Y017318D01*
+X001869Y017343D01*
+X001939Y017365D01*
+X002010Y017382D01*
+X002082Y017396D01*
+X002154Y017406D01*
+X002227Y017412D01*
+X002300Y017414D01*
+X002373Y017412D01*
+X002446Y017406D01*
+X002518Y017396D01*
+X002590Y017382D01*
+X002661Y017365D01*
+X002731Y017343D01*
+X002800Y017318D01*
+X002867Y017289D01*
+X002932Y017257D01*
+X002996Y017221D01*
+X003058Y017181D01*
+X003117Y017139D01*
+X003174Y017093D01*
+X003228Y017044D01*
+X003280Y016992D01*
+X003329Y016938D01*
+X003375Y016881D01*
+X003417Y016822D01*
+X003457Y016760D01*
+X003493Y016696D01*
+X003525Y016631D01*
+X003554Y016564D01*
+X003579Y016495D01*
+X003601Y016425D01*
+X003618Y016354D01*
+X003632Y016282D01*
+X003642Y016210D01*
+X003648Y016137D01*
+X003650Y016064D01*
+X003648Y015991D01*
+X003642Y015918D01*
+X003632Y015846D01*
+X003618Y015774D01*
+X003601Y015703D01*
+X003579Y015633D01*
+X003554Y015564D01*
+X003525Y015497D01*
+X003493Y015432D01*
+X003457Y015368D01*
+X003417Y015306D01*
+X003375Y015247D01*
+X003329Y015190D01*
+X003280Y015136D01*
+X003228Y015084D01*
+X003174Y015035D01*
+X003117Y014989D01*
+X003058Y014947D01*
+X002996Y014907D01*
+X002932Y014871D01*
+X002867Y014839D01*
+X002800Y014810D01*
+X002731Y014785D01*
+X002661Y014763D01*
+X002590Y014746D01*
+X002518Y014732D01*
+X002446Y014722D01*
+X002373Y014716D01*
+X002300Y014714D01*
+X002227Y014716D01*
+X002154Y014722D01*
+X002082Y014732D01*
+X002010Y014746D01*
+X001939Y014763D01*
+X001869Y014785D01*
+X001800Y014810D01*
+X001733Y014839D01*
+X001668Y014871D01*
+X001604Y014907D01*
+X001542Y014947D01*
+X001483Y014989D01*
+X001426Y015035D01*
+X001372Y015084D01*
+X001320Y015136D01*
+X001271Y015190D01*
+X001225Y015247D01*
+X001183Y015306D01*
+X001143Y015368D01*
+X001107Y015432D01*
+X001075Y015497D01*
+X001046Y015564D01*
+X001021Y015633D01*
+X000999Y015703D01*
+X000982Y015774D01*
+X000968Y015846D01*
+X000958Y015918D01*
+X000952Y015991D01*
+X000950Y016064D01*
+X001250Y013064D02*
+X001000Y012814D01*
+X001000Y012314D01*
+X001250Y012064D01*
+X001000Y011814D01*
+X001000Y011314D01*
+X001250Y011064D01*
+X001750Y011064D01*
+X002000Y011314D01*
+X002000Y011814D01*
+X001750Y012064D01*
+X001250Y012064D01*
+X001750Y012064D02*
+X002000Y012314D01*
+X002000Y012814D01*
+X001750Y013064D01*
+X001250Y013064D01*
+X001250Y011064D02*
+X001000Y010814D01*
+X001000Y010314D01*
+X001250Y010064D01*
+X001000Y009814D01*
+X001000Y009314D01*
+X001250Y009064D01*
+X001000Y008814D01*
+X001000Y008314D01*
+X001250Y008064D01*
+X001750Y008064D01*
+X002000Y008314D01*
+X002000Y008814D01*
+X001750Y009064D01*
+X001250Y009064D01*
+X001750Y009064D02*
+X002000Y009314D01*
+X002000Y009814D01*
+X001750Y010064D01*
+X001250Y010064D01*
+X001750Y010064D02*
+X002000Y010314D01*
+X002000Y010814D01*
+X001750Y011064D01*
+X004750Y011194D02*
+X004750Y011614D01*
+X004750Y012014D01*
+X004750Y012434D01*
+X004752Y012457D01*
+X004757Y012480D01*
+X004766Y012502D01*
+X004779Y012522D01*
+X004794Y012540D01*
+X004812Y012555D01*
+X004832Y012568D01*
+X004854Y012577D01*
+X004877Y012582D01*
+X004900Y012584D01*
+X006600Y012584D01*
+X006623Y012582D01*
+X006646Y012577D01*
+X006668Y012568D01*
+X006688Y012555D01*
+X006706Y012540D01*
+X006721Y012522D01*
+X006734Y012502D01*
+X006743Y012480D01*
+X006748Y012457D01*
+X006750Y012434D01*
+X006750Y011194D01*
+X006748Y011171D01*
+X006743Y011148D01*
+X006734Y011126D01*
+X006721Y011106D01*
+X006706Y011088D01*
+X006688Y011073D01*
+X006668Y011060D01*
+X006646Y011051D01*
+X006623Y011046D01*
+X006600Y011044D01*
+X004900Y011044D01*
+X004877Y011046D01*
+X004854Y011051D01*
+X004832Y011060D01*
+X004812Y011073D01*
+X004794Y011088D01*
+X004779Y011106D01*
+X004766Y011126D01*
+X004757Y011148D01*
+X004752Y011171D01*
+X004750Y011194D01*
+X004750Y011614D02*
+X004777Y011616D01*
+X004804Y011621D01*
+X004830Y011631D01*
+X004854Y011643D01*
+X004876Y011659D01*
+X004896Y011677D01*
+X004913Y011699D01*
+X004928Y011722D01*
+X004938Y011747D01*
+X004946Y011773D01*
+X004950Y011800D01*
+X004950Y011828D01*
+X004946Y011855D01*
+X004938Y011881D01*
+X004928Y011906D01*
+X004913Y011929D01*
+X004896Y011951D01*
+X004876Y011969D01*
+X004854Y011985D01*
+X004830Y011997D01*
+X004804Y012007D01*
+X004777Y012012D01*
+X004750Y012014D01*
+X001000Y005114D02*
+X001002Y005187D01*
+X001008Y005260D01*
+X001018Y005332D01*
+X001032Y005404D01*
+X001049Y005475D01*
+X001071Y005545D01*
+X001096Y005614D01*
+X001125Y005681D01*
+X001157Y005746D01*
+X001193Y005810D01*
+X001233Y005872D01*
+X001275Y005931D01*
+X001321Y005988D01*
+X001370Y006042D01*
+X001422Y006094D01*
+X001476Y006143D01*
+X001533Y006189D01*
+X001592Y006231D01*
+X001654Y006271D01*
+X001718Y006307D01*
+X001783Y006339D01*
+X001850Y006368D01*
+X001919Y006393D01*
+X001989Y006415D01*
+X002060Y006432D01*
+X002132Y006446D01*
+X002204Y006456D01*
+X002277Y006462D01*
+X002350Y006464D01*
+X002423Y006462D01*
+X002496Y006456D01*
+X002568Y006446D01*
+X002640Y006432D01*
+X002711Y006415D01*
+X002781Y006393D01*
+X002850Y006368D01*
+X002917Y006339D01*
+X002982Y006307D01*
+X003046Y006271D01*
+X003108Y006231D01*
+X003167Y006189D01*
+X003224Y006143D01*
+X003278Y006094D01*
+X003330Y006042D01*
+X003379Y005988D01*
+X003425Y005931D01*
+X003467Y005872D01*
+X003507Y005810D01*
+X003543Y005746D01*
+X003575Y005681D01*
+X003604Y005614D01*
+X003629Y005545D01*
+X003651Y005475D01*
+X003668Y005404D01*
+X003682Y005332D01*
+X003692Y005260D01*
+X003698Y005187D01*
+X003700Y005114D01*
+X003698Y005041D01*
+X003692Y004968D01*
+X003682Y004896D01*
+X003668Y004824D01*
+X003651Y004753D01*
+X003629Y004683D01*
+X003604Y004614D01*
+X003575Y004547D01*
+X003543Y004482D01*
+X003507Y004418D01*
+X003467Y004356D01*
+X003425Y004297D01*
+X003379Y004240D01*
+X003330Y004186D01*
+X003278Y004134D01*
+X003224Y004085D01*
+X003167Y004039D01*
+X003108Y003997D01*
+X003046Y003957D01*
+X002982Y003921D01*
+X002917Y003889D01*
+X002850Y003860D01*
+X002781Y003835D01*
+X002711Y003813D01*
+X002640Y003796D01*
+X002568Y003782D01*
+X002496Y003772D01*
+X002423Y003766D01*
+X002350Y003764D01*
+X002277Y003766D01*
+X002204Y003772D01*
+X002132Y003782D01*
+X002060Y003796D01*
+X001989Y003813D01*
+X001919Y003835D01*
+X001850Y003860D01*
+X001783Y003889D01*
+X001718Y003921D01*
+X001654Y003957D01*
+X001592Y003997D01*
+X001533Y004039D01*
+X001476Y004085D01*
+X001422Y004134D01*
+X001370Y004186D01*
+X001321Y004240D01*
+X001275Y004297D01*
+X001233Y004356D01*
+X001193Y004418D01*
+X001157Y004482D01*
+X001125Y004547D01*
+X001096Y004614D01*
+X001071Y004683D01*
+X001049Y004753D01*
+X001032Y004824D01*
+X001018Y004896D01*
+X001008Y004968D01*
+X001002Y005041D01*
+X001000Y005114D01*
+D12*
+X004750Y011184D02*
+X006750Y011184D01*
+D13*
+X006929Y012889D02*
+X007079Y012889D01*
+X007154Y012964D01*
+X007154Y013340D01*
+X007315Y013265D02*
+X007390Y013340D01*
+X007540Y013340D01*
+X007615Y013265D01*
+X007615Y013190D01*
+X007540Y013115D01*
+X007615Y013039D01*
+X007615Y012964D01*
+X007540Y012889D01*
+X007390Y012889D01*
+X007315Y012964D01*
+X007465Y013115D02*
+X007540Y013115D01*
+X006929Y012889D02*
+X006854Y012964D01*
+X006854Y013340D01*
+X006216Y015659D02*
+X005916Y016110D01*
+X005756Y016110D02*
+X005756Y015659D01*
+X005916Y015659D02*
+X006216Y016110D01*
+X005756Y016110D02*
+X005606Y015960D01*
+X005455Y016110D01*
+X005455Y015659D01*
+X005295Y015734D02*
+X005295Y016035D01*
+X005220Y016110D01*
+X004995Y016110D01*
+X004995Y015659D01*
+X005220Y015659D01*
+X005295Y015734D01*
+X002695Y012963D02*
+X002695Y012812D01*
+X002695Y012887D02*
+X002245Y012887D01*
+X002245Y012812D02*
+X002245Y012963D01*
+X002320Y012652D02*
+X002245Y012577D01*
+X002245Y012352D01*
+X002695Y012352D01*
+X002695Y012577D01*
+X002620Y012652D01*
+X002320Y012652D01*
+X002245Y012195D02*
+X002245Y012045D01*
+X002245Y012120D02*
+X002695Y012120D01*
+X002695Y012045D02*
+X002695Y012195D01*
+X002695Y011885D02*
+X002245Y011885D01*
+X002395Y011735D01*
+X002245Y011585D01*
+X002695Y011585D01*
+X016845Y017559D02*
+X016845Y018010D01*
+X017070Y018010D01*
+X017145Y017935D01*
+X017145Y017785D01*
+X017070Y017709D01*
+X016845Y017709D01*
+X017305Y017559D02*
+X017305Y018010D01*
+X017606Y018010D02*
+X017606Y017559D01*
+X017456Y017709D01*
+X017305Y017559D01*
+X017766Y017559D02*
+X017766Y018010D01*
+X017991Y018010D01*
+X018066Y017935D01*
+X018066Y017785D01*
+X017991Y017709D01*
+X017766Y017709D01*
+X017916Y017709D02*
+X018066Y017559D01*
+D14*
+X020131Y016064D02*
+X020133Y016115D01*
+X020139Y016166D01*
+X020149Y016216D01*
+X020162Y016266D01*
+X020180Y016314D01*
+X020200Y016361D01*
+X020225Y016406D01*
+X020253Y016449D01*
+X020284Y016490D01*
+X020318Y016528D01*
+X020355Y016563D01*
+X020394Y016596D01*
+X020436Y016626D01*
+X020480Y016652D01*
+X020526Y016674D01*
+X020574Y016694D01*
+X020623Y016709D01*
+X020673Y016721D01*
+X020723Y016729D01*
+X020774Y016733D01*
+X020826Y016733D01*
+X020877Y016729D01*
+X020927Y016721D01*
+X020977Y016709D01*
+X021026Y016694D01*
+X021074Y016674D01*
+X021120Y016652D01*
+X021164Y016626D01*
+X021206Y016596D01*
+X021245Y016563D01*
+X021282Y016528D01*
+X021316Y016490D01*
+X021347Y016449D01*
+X021375Y016406D01*
+X021400Y016361D01*
+X021420Y016314D01*
+X021438Y016266D01*
+X021451Y016216D01*
+X021461Y016166D01*
+X021467Y016115D01*
+X021469Y016064D01*
+X021467Y016013D01*
+X021461Y015962D01*
+X021451Y015912D01*
+X021438Y015862D01*
+X021420Y015814D01*
+X021400Y015767D01*
+X021375Y015722D01*
+X021347Y015679D01*
+X021316Y015638D01*
+X021282Y015600D01*
+X021245Y015565D01*
+X021206Y015532D01*
+X021164Y015502D01*
+X021120Y015476D01*
+X021074Y015454D01*
+X021026Y015434D01*
+X020977Y015419D01*
+X020927Y015407D01*
+X020877Y015399D01*
+X020826Y015395D01*
+X020774Y015395D01*
+X020723Y015399D01*
+X020673Y015407D01*
+X020623Y015419D01*
+X020574Y015434D01*
+X020526Y015454D01*
+X020480Y015476D01*
+X020436Y015502D01*
+X020394Y015532D01*
+X020355Y015565D01*
+X020318Y015600D01*
+X020284Y015638D01*
+X020253Y015679D01*
+X020225Y015722D01*
+X020200Y015767D01*
+X020180Y015814D01*
+X020162Y015862D01*
+X020149Y015912D01*
+X020139Y015962D01*
+X020133Y016013D01*
+X020131Y016064D01*
+X023764Y013422D02*
+X016441Y013422D01*
+X016441Y008007D01*
+X023764Y008007D01*
+X023764Y013422D01*
+X013874Y007472D02*
+X013874Y003456D01*
+X011826Y003456D01*
+X011826Y007472D01*
+X011484Y008109D02*
+X011484Y012120D01*
+X008060Y007206D02*
+X005640Y007206D01*
+X005640Y003522D01*
+X008060Y003522D01*
+X008060Y007206D01*
+X001681Y005114D02*
+X001683Y005165D01*
+X001689Y005216D01*
+X001699Y005266D01*
+X001712Y005316D01*
+X001730Y005364D01*
+X001750Y005411D01*
+X001775Y005456D01*
+X001803Y005499D01*
+X001834Y005540D01*
+X001868Y005578D01*
+X001905Y005613D01*
+X001944Y005646D01*
+X001986Y005676D01*
+X002030Y005702D01*
+X002076Y005724D01*
+X002124Y005744D01*
+X002173Y005759D01*
+X002223Y005771D01*
+X002273Y005779D01*
+X002324Y005783D01*
+X002376Y005783D01*
+X002427Y005779D01*
+X002477Y005771D01*
+X002527Y005759D01*
+X002576Y005744D01*
+X002624Y005724D01*
+X002670Y005702D01*
+X002714Y005676D01*
+X002756Y005646D01*
+X002795Y005613D01*
+X002832Y005578D01*
+X002866Y005540D01*
+X002897Y005499D01*
+X002925Y005456D01*
+X002950Y005411D01*
+X002970Y005364D01*
+X002988Y005316D01*
+X003001Y005266D01*
+X003011Y005216D01*
+X003017Y005165D01*
+X003019Y005114D01*
+X003017Y005063D01*
+X003011Y005012D01*
+X003001Y004962D01*
+X002988Y004912D01*
+X002970Y004864D01*
+X002950Y004817D01*
+X002925Y004772D01*
+X002897Y004729D01*
+X002866Y004688D01*
+X002832Y004650D01*
+X002795Y004615D01*
+X002756Y004582D01*
+X002714Y004552D01*
+X002670Y004526D01*
+X002624Y004504D01*
+X002576Y004484D01*
+X002527Y004469D01*
+X002477Y004457D01*
+X002427Y004449D01*
+X002376Y004445D01*
+X002324Y004445D01*
+X002273Y004449D01*
+X002223Y004457D01*
+X002173Y004469D01*
+X002124Y004484D01*
+X002076Y004504D01*
+X002030Y004526D01*
+X001986Y004552D01*
+X001944Y004582D01*
+X001905Y004615D01*
+X001868Y004650D01*
+X001834Y004688D01*
+X001803Y004729D01*
+X001775Y004772D01*
+X001750Y004817D01*
+X001730Y004864D01*
+X001712Y004912D01*
+X001699Y004962D01*
+X001689Y005012D01*
+X001683Y005063D01*
+X001681Y005114D01*
+X001631Y016064D02*
+X001633Y016115D01*
+X001639Y016166D01*
+X001649Y016216D01*
+X001662Y016266D01*
+X001680Y016314D01*
+X001700Y016361D01*
+X001725Y016406D01*
+X001753Y016449D01*
+X001784Y016490D01*
+X001818Y016528D01*
+X001855Y016563D01*
+X001894Y016596D01*
+X001936Y016626D01*
+X001980Y016652D01*
+X002026Y016674D01*
+X002074Y016694D01*
+X002123Y016709D01*
+X002173Y016721D01*
+X002223Y016729D01*
+X002274Y016733D01*
+X002326Y016733D01*
+X002377Y016729D01*
+X002427Y016721D01*
+X002477Y016709D01*
+X002526Y016694D01*
+X002574Y016674D01*
+X002620Y016652D01*
+X002664Y016626D01*
+X002706Y016596D01*
+X002745Y016563D01*
+X002782Y016528D01*
+X002816Y016490D01*
+X002847Y016449D01*
+X002875Y016406D01*
+X002900Y016361D01*
+X002920Y016314D01*
+X002938Y016266D01*
+X002951Y016216D01*
+X002961Y016166D01*
+X002967Y016115D01*
+X002969Y016064D01*
+X002967Y016013D01*
+X002961Y015962D01*
+X002951Y015912D01*
+X002938Y015862D01*
+X002920Y015814D01*
+X002900Y015767D01*
+X002875Y015722D01*
+X002847Y015679D01*
+X002816Y015638D01*
+X002782Y015600D01*
+X002745Y015565D01*
+X002706Y015532D01*
+X002664Y015502D01*
+X002620Y015476D01*
+X002574Y015454D01*
+X002526Y015434D01*
+X002477Y015419D01*
+X002427Y015407D01*
+X002377Y015399D01*
+X002326Y015395D01*
+X002274Y015395D01*
+X002223Y015399D01*
+X002173Y015407D01*
+X002123Y015419D01*
+X002074Y015434D01*
+X002026Y015454D01*
+X001980Y015476D01*
+X001936Y015502D01*
+X001894Y015532D01*
+X001855Y015565D01*
+X001818Y015600D01*
+X001784Y015638D01*
+X001753Y015679D01*
+X001725Y015722D01*
+X001700Y015767D01*
+X001680Y015814D01*
+X001662Y015862D01*
+X001649Y015912D01*
+X001639Y015962D01*
+X001633Y016013D01*
+X001631Y016064D01*
+X020131Y005064D02*
+X020133Y005115D01*
+X020139Y005166D01*
+X020149Y005216D01*
+X020162Y005266D01*
+X020180Y005314D01*
+X020200Y005361D01*
+X020225Y005406D01*
+X020253Y005449D01*
+X020284Y005490D01*
+X020318Y005528D01*
+X020355Y005563D01*
+X020394Y005596D01*
+X020436Y005626D01*
+X020480Y005652D01*
+X020526Y005674D01*
+X020574Y005694D01*
+X020623Y005709D01*
+X020673Y005721D01*
+X020723Y005729D01*
+X020774Y005733D01*
+X020826Y005733D01*
+X020877Y005729D01*
+X020927Y005721D01*
+X020977Y005709D01*
+X021026Y005694D01*
+X021074Y005674D01*
+X021120Y005652D01*
+X021164Y005626D01*
+X021206Y005596D01*
+X021245Y005563D01*
+X021282Y005528D01*
+X021316Y005490D01*
+X021347Y005449D01*
+X021375Y005406D01*
+X021400Y005361D01*
+X021420Y005314D01*
+X021438Y005266D01*
+X021451Y005216D01*
+X021461Y005166D01*
+X021467Y005115D01*
+X021469Y005064D01*
+X021467Y005013D01*
+X021461Y004962D01*
+X021451Y004912D01*
+X021438Y004862D01*
+X021420Y004814D01*
+X021400Y004767D01*
+X021375Y004722D01*
+X021347Y004679D01*
+X021316Y004638D01*
+X021282Y004600D01*
+X021245Y004565D01*
+X021206Y004532D01*
+X021164Y004502D01*
+X021120Y004476D01*
+X021074Y004454D01*
+X021026Y004434D01*
+X020977Y004419D01*
+X020927Y004407D01*
+X020877Y004399D01*
+X020826Y004395D01*
+X020774Y004395D01*
+X020723Y004399D01*
+X020673Y004407D01*
+X020623Y004419D01*
+X020574Y004434D01*
+X020526Y004454D01*
+X020480Y004476D01*
+X020436Y004502D01*
+X020394Y004532D01*
+X020355Y004565D01*
+X020318Y004600D01*
+X020284Y004638D01*
+X020253Y004679D01*
+X020225Y004722D01*
+X020200Y004767D01*
+X020180Y004814D01*
+X020162Y004862D01*
+X020149Y004912D01*
+X020139Y004962D01*
+X020133Y005013D01*
+X020131Y005064D01*
+D15*
+X018017Y003995D02*
+X017710Y003995D01*
+X017710Y003765D01*
+X017863Y003841D01*
+X017940Y003841D01*
+X018017Y003765D01*
+X018017Y003611D01*
+X017940Y003534D01*
+X017786Y003534D01*
+X017710Y003611D01*
+X017556Y003534D02*
+X017403Y003688D01*
+X017479Y003688D02*
+X017249Y003688D01*
+X017249Y003534D02*
+X017249Y003995D01*
+X017479Y003995D01*
+X017556Y003918D01*
+X017556Y003765D01*
+X017479Y003688D01*
+X016918Y003628D02*
+X016611Y003628D01*
+X016764Y003628D02*
+X016764Y004088D01*
+X016611Y003935D01*
+X016457Y004012D02*
+X016457Y003705D01*
+X016380Y003628D01*
+X016150Y003628D01*
+X016150Y004088D01*
+X016380Y004088D01*
+X016457Y004012D01*
+X015997Y004088D02*
+X015690Y004088D01*
+X015690Y003628D01*
+X015997Y003628D01*
+X015843Y003858D02*
+X015690Y003858D01*
+X015536Y003628D02*
+X015229Y003628D01*
+X015229Y004088D01*
+X015596Y006214D02*
+X015903Y006214D01*
+X015980Y006290D01*
+X015980Y006444D01*
+X015903Y006520D01*
+X015903Y006674D02*
+X015980Y006751D01*
+X015980Y006904D01*
+X015903Y006981D01*
+X015750Y006981D01*
+X015673Y006904D01*
+X015673Y006827D01*
+X015750Y006674D01*
+X015520Y006674D01*
+X015520Y006981D01*
+X015596Y006520D02*
+X015520Y006444D01*
+X015520Y006290D01*
+X015596Y006214D01*
+X012602Y007640D02*
+X012295Y007640D01*
+X012602Y007947D01*
+X012602Y008024D01*
+X012525Y008101D01*
+X012372Y008101D01*
+X012295Y008024D01*
+X012142Y008101D02*
+X012142Y007717D01*
+X012065Y007640D01*
+X011911Y007640D01*
+X011835Y007717D01*
+X011835Y008101D01*
+X010261Y006645D02*
+X010030Y006415D01*
+X010337Y006415D01*
+X010261Y006645D02*
+X010261Y006184D01*
+X009877Y006184D02*
+X009723Y006338D01*
+X009800Y006338D02*
+X009570Y006338D01*
+X009570Y006184D02*
+X009570Y006645D01*
+X009800Y006645D01*
+X009877Y006568D01*
+X009877Y006415D01*
+X009800Y006338D01*
+X009847Y003695D02*
+X009770Y003618D01*
+X009770Y003311D01*
+X009847Y003234D01*
+X010000Y003234D01*
+X010077Y003311D01*
+X010230Y003465D02*
+X010537Y003465D01*
+X010461Y003695D02*
+X010461Y003234D01*
+X010230Y003465D02*
+X010461Y003695D01*
+X010077Y003618D02*
+X010000Y003695D01*
+X009847Y003695D01*
+X006311Y007384D02*
+X006311Y007845D01*
+X006080Y007615D01*
+X006387Y007615D01*
+X005927Y007461D02*
+X005927Y007845D01*
+X005620Y007845D02*
+X005620Y007461D01*
+X005697Y007384D01*
+X005850Y007384D01*
+X005927Y007461D01*
+X004261Y010084D02*
+X004107Y010084D01*
+X004030Y010161D01*
+X003877Y010084D02*
+X003723Y010238D01*
+X003800Y010238D02*
+X003570Y010238D01*
+X003570Y010084D02*
+X003570Y010545D01*
+X003800Y010545D01*
+X003877Y010468D01*
+X003877Y010315D01*
+X003800Y010238D01*
+X004030Y010468D02*
+X004107Y010545D01*
+X004261Y010545D01*
+X004337Y010468D01*
+X004337Y010391D01*
+X004261Y010315D01*
+X004337Y010238D01*
+X004337Y010161D01*
+X004261Y010084D01*
+X004261Y010315D02*
+X004184Y010315D01*
+X004207Y013484D02*
+X004130Y013561D01*
+X004207Y013484D02*
+X004361Y013484D01*
+X004437Y013561D01*
+X004437Y013638D01*
+X004361Y013715D01*
+X004284Y013715D01*
+X004361Y013715D02*
+X004437Y013791D01*
+X004437Y013868D01*
+X004361Y013945D01*
+X004207Y013945D01*
+X004130Y013868D01*
+X003977Y013868D02*
+X003900Y013945D01*
+X003747Y013945D01*
+X003670Y013868D01*
+X003670Y013561D01*
+X003747Y013484D01*
+X003900Y013484D01*
+X003977Y013561D01*
+X006649Y014334D02*
+X006649Y014795D01*
+X006879Y014795D01*
+X006956Y014718D01*
+X006956Y014565D01*
+X006879Y014488D01*
+X006649Y014488D01*
+X006803Y014488D02*
+X006956Y014334D01*
+X007110Y014334D02*
+X007417Y014334D01*
+X007263Y014334D02*
+X007263Y014795D01*
+X007110Y014641D01*
+X008386Y014156D02*
+X008386Y016479D01*
+X009606Y016479D01*
+X010394Y016479D02*
+X011614Y016479D01*
+X011614Y014156D01*
+X010709Y013250D01*
+X010394Y013250D01*
+X009606Y013250D02*
+X009291Y013250D01*
+X008386Y014156D01*
+X009646Y013348D02*
+X009569Y013368D01*
+X009494Y013391D01*
+X009420Y013419D01*
+X009348Y013450D01*
+X009277Y013485D01*
+X009208Y013523D01*
+X009142Y013565D01*
+X009077Y013610D01*
+X009015Y013658D01*
+X008955Y013710D01*
+X008898Y013764D01*
+X008844Y013821D01*
+X008792Y013881D01*
+X008744Y013943D01*
+X008699Y014008D01*
+X008658Y014075D01*
+X008620Y014144D01*
+X008585Y014215D01*
+X008554Y014287D01*
+X008526Y014361D01*
+X008503Y014436D01*
+X008483Y014512D01*
+X008467Y014590D01*
+X008455Y014668D01*
+X008447Y014746D01*
+X008443Y014825D01*
+X008443Y014903D01*
+X008447Y014982D01*
+X008455Y015060D01*
+X008467Y015138D01*
+X008483Y015216D01*
+X008503Y015292D01*
+X008526Y015367D01*
+X008554Y015441D01*
+X008585Y015513D01*
+X008620Y015584D01*
+X008658Y015653D01*
+X008699Y015720D01*
+X008744Y015785D01*
+X008792Y015847D01*
+X008844Y015907D01*
+X008898Y015964D01*
+X008955Y016018D01*
+X009015Y016070D01*
+X009077Y016118D01*
+X009142Y016163D01*
+X009208Y016205D01*
+X009277Y016243D01*
+X009348Y016278D01*
+X009420Y016309D01*
+X009494Y016337D01*
+X009569Y016360D01*
+X009646Y016380D01*
+X010354Y016380D02*
+X010431Y016360D01*
+X010506Y016337D01*
+X010580Y016309D01*
+X010652Y016278D01*
+X010723Y016243D01*
+X010792Y016205D01*
+X010858Y016163D01*
+X010923Y016118D01*
+X010985Y016070D01*
+X011045Y016018D01*
+X011102Y015964D01*
+X011156Y015907D01*
+X011208Y015847D01*
+X011256Y015785D01*
+X011301Y015720D01*
+X011342Y015653D01*
+X011380Y015584D01*
+X011415Y015513D01*
+X011446Y015441D01*
+X011474Y015367D01*
+X011497Y015292D01*
+X011517Y015216D01*
+X011533Y015138D01*
+X011545Y015060D01*
+X011553Y014982D01*
+X011557Y014903D01*
+X011557Y014825D01*
+X011553Y014746D01*
+X011545Y014668D01*
+X011533Y014590D01*
+X011517Y014512D01*
+X011497Y014436D01*
+X011474Y014361D01*
+X011446Y014287D01*
+X011415Y014215D01*
+X011380Y014144D01*
+X011342Y014075D01*
+X011301Y014008D01*
+X011256Y013943D01*
+X011208Y013881D01*
+X011156Y013821D01*
+X011102Y013764D01*
+X011045Y013710D01*
+X010985Y013658D01*
+X010923Y013610D01*
+X010858Y013565D01*
+X010792Y013523D01*
+X010723Y013485D01*
+X010652Y013450D01*
+X010580Y013419D01*
+X010506Y013391D01*
+X010431Y013368D01*
+X010354Y013348D01*
+X011749Y012395D02*
+X011749Y012011D01*
+X011826Y011934D01*
+X011979Y011934D01*
+X012056Y012011D01*
+X012056Y012395D01*
+X012210Y012241D02*
+X012363Y012395D01*
+X012363Y011934D01*
+X012210Y011934D02*
+X012517Y011934D01*
+X013148Y012406D02*
+X012242Y013312D01*
+X012242Y016422D01*
+X013856Y016422D01*
+X014644Y016422D02*
+X016258Y016422D01*
+X016258Y013312D01*
+X015352Y012406D01*
+X014644Y012406D01*
+X013856Y012406D02*
+X013148Y012406D01*
+X014849Y010645D02*
+X014849Y010184D01*
+X015156Y010184D01*
+X015310Y010184D02*
+X015617Y010184D01*
+X015463Y010184D02*
+X015463Y010645D01*
+X015310Y010491D01*
+X015320Y009295D02*
+X015550Y009295D01*
+X015627Y009218D01*
+X015627Y009065D01*
+X015550Y008988D01*
+X015320Y008988D01*
+X015473Y008988D02*
+X015627Y008834D01*
+X015780Y008834D02*
+X016087Y009141D01*
+X016087Y009218D01*
+X016011Y009295D01*
+X015857Y009295D01*
+X015780Y009218D01*
+X015780Y008834D02*
+X016087Y008834D01*
+X015320Y008834D02*
+X015320Y009295D01*
+X014644Y012504D02*
+X014729Y012524D01*
+X014813Y012547D01*
+X014896Y012574D01*
+X014978Y012605D01*
+X015058Y012639D01*
+X015137Y012678D01*
+X015214Y012719D01*
+X015289Y012764D01*
+X015362Y012812D01*
+X015433Y012864D01*
+X015501Y012918D01*
+X015567Y012976D01*
+X015630Y013036D01*
+X015690Y013099D01*
+X015748Y013165D01*
+X015802Y013234D01*
+X015854Y013304D01*
+X015902Y013377D01*
+X015946Y013453D01*
+X015988Y013530D01*
+X016026Y013608D01*
+X016060Y013689D01*
+X016091Y013771D01*
+X016118Y013854D01*
+X016141Y013938D01*
+X016160Y014023D01*
+X016176Y014109D01*
+X016188Y014196D01*
+X016196Y014283D01*
+X016200Y014370D01*
+X016200Y014458D01*
+X016196Y014545D01*
+X016188Y014632D01*
+X016176Y014719D01*
+X016160Y014805D01*
+X016141Y014890D01*
+X016118Y014974D01*
+X016091Y015057D01*
+X016060Y015139D01*
+X016026Y015220D01*
+X015988Y015298D01*
+X015946Y015375D01*
+X015902Y015451D01*
+X015854Y015524D01*
+X015802Y015594D01*
+X015748Y015663D01*
+X015690Y015729D01*
+X015630Y015792D01*
+X015567Y015852D01*
+X015501Y015910D01*
+X015433Y015964D01*
+X015362Y016016D01*
+X015289Y016064D01*
+X015214Y016109D01*
+X015137Y016150D01*
+X015058Y016189D01*
+X014978Y016223D01*
+X014896Y016254D01*
+X014813Y016281D01*
+X014729Y016304D01*
+X014644Y016324D01*
+X013856Y016324D02*
+X013771Y016304D01*
+X013687Y016281D01*
+X013604Y016254D01*
+X013522Y016223D01*
+X013442Y016189D01*
+X013363Y016150D01*
+X013286Y016109D01*
+X013211Y016064D01*
+X013138Y016016D01*
+X013067Y015964D01*
+X012999Y015910D01*
+X012933Y015852D01*
+X012870Y015792D01*
+X012810Y015729D01*
+X012752Y015663D01*
+X012698Y015594D01*
+X012646Y015524D01*
+X012598Y015451D01*
+X012554Y015375D01*
+X012512Y015298D01*
+X012474Y015220D01*
+X012440Y015139D01*
+X012409Y015057D01*
+X012382Y014974D01*
+X012359Y014890D01*
+X012340Y014805D01*
+X012324Y014719D01*
+X012312Y014632D01*
+X012304Y014545D01*
+X012300Y014458D01*
+X012300Y014370D01*
+X012304Y014283D01*
+X012312Y014196D01*
+X012324Y014109D01*
+X012340Y014023D01*
+X012359Y013938D01*
+X012382Y013854D01*
+X012409Y013771D01*
+X012440Y013689D01*
+X012474Y013608D01*
+X012512Y013530D01*
+X012554Y013453D01*
+X012598Y013377D01*
+X012646Y013304D01*
+X012698Y013234D01*
+X012752Y013165D01*
+X012810Y013099D01*
+X012870Y013036D01*
+X012933Y012976D01*
+X012999Y012918D01*
+X013067Y012864D01*
+X013138Y012812D01*
+X013211Y012764D01*
+X013286Y012719D01*
+X013363Y012678D01*
+X013442Y012639D01*
+X013522Y012605D01*
+X013604Y012574D01*
+X013687Y012547D01*
+X013771Y012524D01*
+X013856Y012504D01*
+D16*
+X011780Y011454D03*
+X011780Y010784D03*
+X011780Y010114D03*
+X011780Y009444D03*
+X011780Y008774D03*
+D17*
+X015534Y016610D02*
+X015657Y016610D01*
+X015719Y016672D01*
+X015841Y016610D02*
+X016088Y016857D01*
+X016088Y016919D01*
+X016026Y016981D01*
+X015902Y016981D01*
+X015841Y016919D01*
+X015719Y016919D02*
+X015657Y016981D01*
+X015534Y016981D01*
+X015472Y016919D01*
+X015472Y016672D01*
+X015534Y016610D01*
+X015841Y016610D02*
+X016088Y016610D01*
+X011491Y016701D02*
+X011244Y016701D01*
+X011368Y016701D02*
+X011368Y017071D01*
+X011244Y016948D01*
+X011123Y017010D02*
+X011061Y017071D01*
+X010938Y017071D01*
+X010876Y017010D01*
+X010876Y016763D01*
+X010938Y016701D01*
+X011061Y016701D01*
+X011123Y016763D01*
+D18*
+X022869Y013789D02*
+X022869Y007639D01*
+D19*
+X022634Y007796D03*
+X022634Y013633D03*
+D20*
+X016200Y004573D02*
+X016259Y004514D01*
+X016190Y004445D01*
+X016131Y004504D01*
+X016200Y004573D01*
+D21*
+X016092Y004672D03*
+M02*
diff --git a/gerber/tests/test_am_statements.py b/gerber/tests/test_am_statements.py
new file mode 100644
index 0000000..0d100b5
--- /dev/null
+++ b/gerber/tests/test_am_statements.py
@@ -0,0 +1,395 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+
+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) == "<Aperture Macro Comment: Test Comment>"
+
+
+def test_AMCirclePrimitive_ctor():
+ test_cases = (
+ (1, "on", 0, (0, 0)),
+ (1, "off", 1, (0, 1)),
+ (1, "on", 2.5, (0, 2)),
+ (1, "off", 5.0, (3, 3)),
+ )
+ for code, exposure, diameter, position in test_cases:
+ c = AMCirclePrimitive(code, exposure, diameter, position)
+ assert 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/gerber/tests/test_cairo_backend.py b/gerber/tests/test_cairo_backend.py
new file mode 100644
index 0000000..51007a9
--- /dev/null
+++ b/gerber/tests/test_cairo_backend.py
@@ -0,0 +1,279 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Garret Fick <garret@ficksworkshop.com>
+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] == '<?xml version="1.0" encoding="UTF-8"?>'
+
+ shutil.rmtree(temp_dir)
diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py
new file mode 100644
index 0000000..8a71a32
--- /dev/null
+++ b/gerber/tests/test_cam.py
@@ -0,0 +1,151 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+
+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/gerber/tests/test_common.py b/gerber/tests/test_common.py
new file mode 100644
index 0000000..a6b1264
--- /dev/null
+++ b/gerber/tests/test_common.py
@@ -0,0 +1,38 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+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/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py
new file mode 100644
index 0000000..d6e83cc
--- /dev/null
+++ b/gerber/tests/test_excellon.py
@@ -0,0 +1,366 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+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/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py
new file mode 100644
index 0000000..41fe294
--- /dev/null
+++ b/gerber/tests/test_excellon_statements.py
@@ -0,0 +1,734 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+
+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) == "<ExcellonTool 08: 0.125in. dia.>"
+ tool = ExcellonTool.from_dict(
+ FileSettings(units="metric"), {"number": 8, "diameter": 0.125}
+ )
+ assert str(tool) == "<ExcellonTool 08: 0.125mm dia.>"
+
+
+def test_excellontool_equality():
+ t = ExcellonTool.from_dict(FileSettings(), {"number": 8, "diameter": 0.125})
+ t1 = ExcellonTool.from_dict(FileSettings(), {"number": 8, "diameter": 0.125})
+ assert 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) == "<Coordinate Statement: X: 0.966 Y: 0.4639 >"
+
+
+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) == "<Repeat Hole: 4 times, offset X: 1.5 Y: 32>"
+
+
+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) == "<Unknown Statement: TEST>"
+
+
+def test_unknownstmt_dump():
+ stmt = UnknownStmt("TEST")
+ assert stmt.to_excellon(FileSettings()) == "TEST"
diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py
new file mode 100644
index 0000000..140cbd1
--- /dev/null
+++ b/gerber/tests/test_gerber_statements.py
@@ -0,0 +1,959 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+
+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) == "<Format Spec: 2:7 leading zero suppression absolute notation>"
+
+ stmt = {"param": "FS", "zero": "T", "notation": "I", "x": "25"}
+ fs = FSParamStmt.from_dict(stmt)
+ assert (
+ str(fs) == "<Format Spec: 2:5 trailing zero suppression incremental notation>"
+ )
+
+
+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) == "<Mode: inches>"
+
+ stmt = {"param": "MO", "mo": "MM"}
+ mo = MOParamStmt.from_dict(stmt)
+ assert str(mo) == "<Mode: millimeters>"
+
+
+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) == "<Image Polarity: positive>"
+
+ stmt = {"param": "IP", "ip": "NEG"}
+ ip = IPParamStmt.from_dict(stmt)
+ assert str(ip) == "<Image Polarity: negative>"
+
+
+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) == "<Image Angle: 45>"
+
+
+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) == "<Offset: X: 0.123456 Y: 0.123456 >"
+
+
+def test_SFParamStmt_factory():
+ stmt = {"param": "SF", "a": "1.4", "b": "0.9"}
+ sf = SFParamStmt.from_dict(stmt)
+ assert 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) == "<Scale Factor: X: 1.4 Y: 0.9>"
+
+
+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) == "<Level Polarity: dark>"
+
+ stmt = {"param": "LP", "lp": "C"}
+ lp = LPParamStmt.from_dict(stmt)
+ assert str(lp) == "<Level Polarity: clear>"
+
+
+def test_AMParamStmt_factory():
+ name = "DONUTVAR"
+ macro = """0 Test Macro. *
+1,1,1.5,0,0*
+20,1,0.9,0,0.45,12,0.45,0*
+21,1,6.8,1.2,3.4,0.6,0*
+22,1,6.8,1.2,0,0,0*
+4,1,4,0.1,0.1,0.5,0.1,0.5,0.5,0.1,0.5,0.1,0.1,0*
+5,1,8,0,0,8,0*
+6,0,0,5,0.5,0.5,2,0.1,6,0*
+7,0,0,7,6,0.2,0*
+8,THIS IS AN UNSUPPORTED PRIMITIVE*
+"""
+ s = AMParamStmt.from_dict({"param": "AM", "name": name, "macro": macro})
+ s.build()
+ assert 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) == "<Aperture Macro POLYGON: 5,1,8,25.4,25.4,25.4,0*>"
+
+
+def test_ASParamStmt_factory():
+ stmt = {"param": "AS", "mode": "AXBY"}
+ s = ASParamStmt.from_dict(stmt)
+ assert 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) == "<Axis Select: AXBY>"
+
+
+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) == "<Image Name: test>"
+
+
+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) == "<Level Name: test>"
+
+
+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) == "<Comment: A comment>"
+
+
+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()) == "<EOF Statement>"
+
+
+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) == "<Aperture Definition: 0: circle>"
+
+ stmt = {"param": "AD", "d": 0, "shape": "R"}
+ ad = ADParamStmt.from_dict(stmt)
+ assert str(ad) == "<Aperture Definition: 0: rectangle>"
+
+ stmt = {"param": "AD", "d": 0, "shape": "O"}
+ ad = ADParamStmt.from_dict(stmt)
+ assert str(ad) == "<Aperture Definition: 0: obround>"
+
+ stmt = {"param": "AD", "d": 0, "shape": "test"}
+ ad = ADParamStmt.from_dict(stmt)
+ assert str(ad) == "<Aperture Definition: 0: test>"
+
+
+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) == "<Image Mirror: A=1 B=1>"
+
+ stmt = {"param": "MI", "b": 1}
+ mi = MIParamStmt.from_dict(stmt)
+ assert str(mi) == "<Image Mirror: A=0 B=1>"
+
+ stmt = {"param": "MI", "a": 1}
+ mi = MIParamStmt.from_dict(stmt)
+ assert str(mi) == "<Image Mirror: A=1 B=0>"
+
+
+def test_coordstmt_ctor():
+ cs = CoordStmt("G04", 0.0, 0.1, 0.2, 0.3, "D01", FileSettings())
+ assert 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) == "<Coordinate Statement: Fn: G04 X: 0 Y: 1 I: 2 J: 3 Op: Lights On>"
+ )
+ cs = CoordStmt("G04", None, None, None, None, "D02", FileSettings())
+ assert str(cs) == "<Coordinate Statement: Fn: G04 Op: Lights Off>"
+ cs = CoordStmt("G04", None, None, None, None, "D03", FileSettings())
+ assert str(cs) == "<Coordinate Statement: Fn: G04 Op: Flash>"
+ cs = CoordStmt("G04", None, None, None, None, "TEST", FileSettings())
+ assert str(cs) == "<Coordinate Statement: Fn: G04 Op: TEST>"
+
+
+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) == "<Aperture: 3>"
diff --git a/gerber/tests/test_ipc356.py b/gerber/tests/test_ipc356.py
new file mode 100644
index 0000000..77f0782
--- /dev/null
+++ b/gerber/tests/test_ipc356.py
@@ -0,0 +1,148 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+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) == "<IPC-D-356 Comment: Layer Stackup:>"
+
+
+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) == "<IPC-D-356 Parameter: VER=IPC-D-356A>"
+
+
+def test_eof():
+ e = IPC356_EndOfFile()
+ assert e.to_netlist() == "999"
+ assert str(e) == "<IPC-D-356 EOF>"
+
+
+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) == "<IPC-D-356 +5VDC Test Record: through-hole>"
+
+ record_string = (
+ "327+3.3VDC R40 -1 PA01X 032100Y 007124X0236Y0315R180 S0"
+ )
+ r = IPC356_TestRecord.from_line(record_string, FileSettings(units="inch"))
+ assert 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/gerber/tests/test_layers.py b/gerber/tests/test_layers.py
new file mode 100644
index 0000000..2178787
--- /dev/null
+++ b/gerber/tests/test_layers.py
@@ -0,0 +1,158 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# 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/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py
new file mode 100644
index 0000000..ad5b34f
--- /dev/null
+++ b/gerber/tests/test_primitives.py
@@ -0,0 +1,1429 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+
+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/gerber/tests/test_rs274x.py b/gerber/tests/test_rs274x.py
new file mode 100644
index 0000000..e7baf11
--- /dev/null
+++ b/gerber/tests/test_rs274x.py
@@ -0,0 +1,55 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+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/gerber/tests/test_rs274x_backend.py b/gerber/tests/test_rs274x_backend.py
new file mode 100644
index 0000000..13347c5
--- /dev/null
+++ b/gerber/tests/test_rs274x_backend.py
@@ -0,0 +1,232 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Garret Fick <garret@ficksworkshop.com>
+
+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/gerber/tests/test_utils.py b/gerber/tests/test_utils.py
new file mode 100644
index 0000000..68484d1
--- /dev/null
+++ b/gerber/tests/test_utils.py
@@ -0,0 +1,167 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+
+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/gerber/utils.py b/gerber/utils.py
new file mode 100644
index 0000000..3d39df9
--- /dev/null
+++ b/gerber/utils.py
@@ -0,0 +1,458 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+gerber.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(<float>, <float>)
+ Point to rotate about origin or center point
+
+ angle : float
+ Angle to rotate the point [degrees]
+
+ center : tuple(<float>, <float>)
+ Coordinates about which the point is rotated. Defaults to the origin.
+
+ Returns
+ -------
+ rotated_point : tuple(<float>, <float>)
+ `point` rotated about `center` by `angle` degrees.
+ """
+ angle = radians(angle)
+
+ cos_angle = cos(angle)
+ sin_angle = sin(angle)
+
+ return (
+ cos_angle * (point[0] - center[0]) - sin_angle * (point[1] - center[1]) + center[0],
+ sin_angle * (point[0] - center[0]) + cos_angle * (point[1] - center[1]) + center[1])
+
+def nearly_equal(point1, point2, ndigits = 6):
+ '''Are the points nearly equal'''
+
+ return round(point1[0] - point2[0], ndigits) == 0 and round(point1[1] - point2[1], ndigits) == 0
+
+
+def sq_distance(point1, point2):
+
+ diff1 = point1[0] - point2[0]
+ diff2 = point1[1] - point2[1]
+ return diff1 * diff1 + diff2 * diff2
+
+
+def listdir(directory, ignore_hidden=True, ignore_os=True):
+ """ List files in given directory.
+ Differs from os.listdir() in that hidden and OS-generated files are ignored
+ by default.
+
+ Parameters
+ ----------
+ directory : str
+ path to the directory for which to list files.
+
+ ignore_hidden : bool
+ If True, ignore files beginning with a leading '.'
+
+ ignore_os : bool
+ If True, ignore OS-generated files, e.g. Thumbs.db
+
+ Returns
+ -------
+ files : list
+ list of files in specified directory
+ """
+ os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db')
+ files = os.listdir(directory)
+ if ignore_hidden:
+ files = [f for f in files if not f.startswith('.')]
+ if ignore_os:
+ files = [f for f in files if not f in os_files]
+ return files
+
+def 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[minxp][0]):
+ minxp=i;
+ 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(sdiff<pi):sdiff+=2*pi
+ if(sdiff>pi):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]
diff --git a/gerbonara/__init__.py b/gerbonara/__init__.py
new file mode 100644
index 0000000..b8023d8
--- /dev/null
+++ b/gerbonara/__init__.py
@@ -0,0 +1 @@
+__version__ = '0.0.1'
diff --git a/gerbonara/cli.py b/gerbonara/cli.py
new file mode 100644
index 0000000..5d7ef65
--- /dev/null
+++ b/gerbonara/cli.py
@@ -0,0 +1,39 @@
+from os import path, listdir
+from glob import glob
+
+from . import __version__
+
+import click
+
+
+@click.group()
+@click.version_option(__version__)
+def cli():
+ pass
+
+
+@click.command()
+@click.option('-o', '--outfile', type=click.File(mode='wb'), help='Output Filename (extension will be added automatically)')
+@click.option('-t', '--theme', default='default', type=click.Choice(['default', 'OSH Park', 'Blue', 'Transparent Copper', 'Transparent Multilayer'], case_sensitive=False), help='Select render theme')
+@click.option('-w', '--width', type=click.INT, help='Maximum width')
+@click.option('-h', '--height', type=click.INT, help='Maximum height')
+@click.option('-v', '--verbose', is_flag=True, help='Increase verbosity of the output')
+@click.argument('filenames', nargs=-1, type=click.Path(exists=True))
+def render(outfile, theme, width, height, verbose, filenames):
+ """Render gerber files to image. If a directory is provided, it should be provided alone and should contain the gerber files for a single PCB."""
+ if len(filenames) == 0:
+ raise click.UsageError(message='No files or folders provided')
+ if len(filenames) > 1:
+ for f in filenames:
+ if path.isdir(f):
+ raise click.UsageError(message='If a directory is provided, it should be provided alone and should contain the gerber files for a single PCB')
+
+ # list files if folder id given
+ if len(filenames) == 1 and path.isdir(filenames[0]):
+ filenames = listdir(filenames[0])
+ #filenames = [f for f in glob(f'{filenames[0]}/*.txt')]
+
+ click.echo(f'render {filenames} with theme {theme}')
+
+
+cli.add_command(render)
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..ded635c
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,5 @@
+# install base requirements
+-r requirements.txt
+
+ pytest==6.0.1
+ pytest-cov==2.10.1
diff --git a/requirements-docs.txt b/requirements-docs.txt
new file mode 100644
index 0000000..39d1d6f
--- /dev/null
+++ b/requirements-docs.txt
@@ -0,0 +1,6 @@
+# install base requirements
+-r requirements.txt
+
+# documentation generation support
+Sphinx==3.2.1
+numpydoc==1.1.0 \ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..1f769f2
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+# cairo rendering support
+cairocffi==0.6
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2eb5b20
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[tool:pytest]
+flake8-max-line-length = 120
diff --git a/setup.py b/setup.py
index f621b80..f26885a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,57 +1,83 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
-
-import os
-
-def read(fname):
- return open(os.path.join(os.path.dirname(__file__), fname)).read()
-
-METADATA = {
- 'name': 'pcb-tools-extension',
- 'version': "0.9.3",
- 'author': 'Hiroshi Murayama <opiopan@gmail.com>',
- 'author_email': "opiopan@gmail.com",
- 'description': ("Extension for pcb-tools package to panelize gerber files"),
- 'license': "Apache",
- 'keywords': "pcb gerber tools extension",
- 'url': "http://github.com/opiopan/pcb-tools-extension",
- 'packages': ['gerberex'],
- 'long_description': read('README.md'),
- 'long_description_content_type': 'text/markdown',
- 'classifiers': [
- "Development Status :: 4 - Beta",
- "Topic :: Utilities",
- "License :: OSI Approved :: Apache Software License",
+#!/usr/bin/env python3
+
+from os import environ
+from os.path import join, abspath, dirname
+from codecs import open
+from setuptools import setup, find_packages
+from subprocess import check_output
+
+
+def long_description():
+ with open('README.md', 'r') as fh:
+ return fh.read()
+
+
+def version():
+ with open(join(abspath(dirname(__file__)), 'gerbonara/__init__.py')) as fh:
+ for line in fh:
+ if line.startswith('__version__'):
+ ver = line.split("'")[1]
+ if environ.get('CI_COMMIT_SHA', '') != '' and environ.get('CI_COMMIT_TAG', '') == '':
+ # attach commit hash to non tagged test uploads from CI
+ commits = check_output(['/usr/bin/env', 'git', 'rev-list', '--count', 'HEAD'], text=True)
+ return f'{ ver }.dev{ commits.strip() }'
+ return ver
+
+ raise RuntimeError('Unable to find version string.')
+
+
+setup(
+ name='gerbonara',
+ version=version(),
+ author='XenGi, Jaseg',
+ author_email='contact@gerbonara.io',
+ description='Tools to handle Gerber and Excellon files in Python',
+ long_description=long_description(),
+ long_description_content_type='text/markdown',
+ url='https://gitlab.com/gerbonara/gerbonara',
+ project_urls={
+ # 'Documentation': 'https://packaging.python.org/tutorials/distributing-packages/',
+ # 'Funding': 'https://donate.pypi.org',
+ # 'Say Thanks!': 'http://saythanks.io/to/example',
+ 'Source': 'https://gitlab.com/gerbonara/gerbonara',
+ 'Tracker': 'https://gitlab.com/gerbonara/gerbonara/issues',
+ },
+ packages=find_packages(exclude=['tests']),
+ install_requires=['click'],
+ entry_points={
+ 'console_scripts': [
+ 'gerbonara = gerbonara.cli:cli',
+ ],
+ },
+ classifiers=[
+ 'Development Status :: 1 - Planning',
+ #'Development Status :: 3 - Alpha',
+ #'Development Status :: 4 - Beta',
+ #'Development Status :: 5 - Production/Stable',
+ 'Environment :: Console',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: Information Technology',
+ 'Intended Audience :: Manufacturing',
+ 'Intended Audience :: Science/Research',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Natural Language :: English',
+ 'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
+ 'Topic :: Artistic Software',
+ 'Topic :: Multimedia :: Graphics',
+ 'Topic :: Printing',
+ 'Topic :: Scientific/Engineering',
+ 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
+ 'Topic :: Scientific/Engineering :: Image Processing',
+ 'Topic :: Utilities',
+ 'Typing :: Typed',
],
-}
-
-SETUPTOOLS_METADATA = {
- 'install_requires': ['pcb-tools', 'dxfgrabber'],
-}
-
-
-def install():
- """ Install using setuptools, fallback to distutils
- """
- try:
- from setuptools import setup
- METADATA.update(SETUPTOOLS_METADATA)
- setup(**METADATA)
- except ImportError:
- from sys import stderr
- stderr.write('Could not import setuptools, using distutils')
- stderr.write('NOTE: You will need to install dependencies manualy')
- from distutils.core import setup
- setup(**METADATA)
-
-if __name__ == '__main__':
- install()
+ keywords='gerber excellon pcb',
+ python_requires='>=3.6',
+)
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..86c0bfc
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,5 @@
+pytest
+pytest-cov
+pytest-xdist
+pytest-flake8
+twine