summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile16
-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--docs/aperture-macros.rst48
-rw-r--r--docs/apertures.rst138
-rw-r--r--docs/api-concepts.rst60
-rw-r--r--docs/conf.py57
-rw-r--r--docs/file-api.rst25
-rw-r--r--docs/graphic-primitive-api.rst27
-rw-r--r--docs/index.rst115
-rw-r--r--docs/object-api.rst30
-rw-r--r--docs/utilities.rst3
-rw-r--r--gerbonara/apertures.py1
-rwxr-xr-xgerbonara/excellon.py10
-rw-r--r--gerbonara/graphic_objects.py298
-rw-r--r--gerbonara/graphic_primitives.py4
-rw-r--r--gerbonara/rs274x.py11
-rw-r--r--setup.py2
28 files changed, 788 insertions, 941 deletions
diff --git a/.gitignore b/.gitignore
index 1829a16..f5927cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ gerbonara_test_failures
*.egg-info
__pycache__
.tox
+docs/_build/
diff --git a/Makefile b/Makefile
index 966f2a0..c7a70a1 100644
--- a/Makefile
+++ b/Makefile
@@ -1,13 +1,21 @@
-PYTHON ?= python
-PYTEST ?= pytest
+PYTHON ?= python
+PYTEST ?= pytest
+SPHINX_BUILD ?= sphinx-build
+
+all: docs sdist bdist_wheel
.PHONY: clean
-clean: doc-clean
+clean:
find . -name '*.pyc' -delete
rm -rf *.egg-info
rm -f .coverage
rm -f coverage.xml
+ rm -rf docs/_build
+
+.PHONY: docs
+docs:
+ sphinx-build -E docs docs/_build
.PHONY: test
test:
@@ -30,7 +38,7 @@ 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/*
+ twine upload -s -i gerbonara@jaseg.de --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/doc/Makefile b/doc/Makefile
deleted file mode 100644
index 5c6b560..0000000
--- a/doc/Makefile
+++ /dev/null
@@ -1,177 +0,0 @@
-# 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
deleted file mode 100644
index 2860659..0000000
--- a/doc/make.bat
+++ /dev/null
@@ -1,242 +0,0 @@
-@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
deleted file mode 100644
index 85e7184..0000000
--- a/doc/source/about.rst
+++ /dev/null
@@ -1,40 +0,0 @@
-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
deleted file mode 100644
index 238a0b8..0000000
--- a/doc/source/conf.py
+++ /dev/null
@@ -1,262 +0,0 @@
-# -*- 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
deleted file mode 100644
index 9cd7ee1..0000000
--- a/doc/source/documentation/excellon.rst
+++ /dev/null
@@ -1,42 +0,0 @@
-: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 classes 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:
-
diff --git a/doc/source/documentation/index.rst b/doc/source/documentation/index.rst
deleted file mode 100644
index dec83f9..0000000
--- a/doc/source/documentation/index.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 6551236..0000000
--- a/doc/source/documentation/operations.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-: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
deleted file mode 100644
index 324ef71..0000000
--- a/doc/source/documentation/render.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-: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
deleted file mode 100644
index 064c82f..0000000
--- a/doc/source/documentation/rs274x.rst
+++ /dev/null
@@ -1,37 +0,0 @@
-: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 classes 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:
diff --git a/doc/source/features.rst b/doc/source/features.rst
deleted file mode 100644
index 67d9e2a..0000000
--- a/doc/source/features.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-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
deleted file mode 100644
index c96ff8a..0000000
--- a/doc/source/index.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-.. 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/docs/aperture-macros.rst b/docs/aperture-macros.rst
new file mode 100644
index 0000000..1284c49
--- /dev/null
+++ b/docs/aperture-macros.rst
@@ -0,0 +1,48 @@
+Aperture Macros
+===============
+
+.. autoclass:: gerbonara.aperture_macros.parse.ApertureMacro
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.parse.GenericMacros
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.expression.Expression
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.expression.UnitExpression
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.expression.ConstantExpression
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.expression.VariableExpression
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.expression.OperatorExpression
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.Primitive
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.Circle
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.VectorLine
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.CenterLine
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.Polygon
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.Thermal
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.Outline
+ :members:
+
+.. autoclass:: gerbonara.aperture_macros.primitive.Comment
+ :members:
+
diff --git a/docs/apertures.rst b/docs/apertures.rst
new file mode 100644
index 0000000..2a17a04
--- /dev/null
+++ b/docs/apertures.rst
@@ -0,0 +1,138 @@
+Apertures in Gerbonara
+======================
+
+Gerbonara maps all standard Gerber apertures to subclasses of the Aperture_ class. These subclasses: CircleAperture_,
+RectangleAperture_, ObroundAperture_ and PolygonAperture_. Aperture macro instantiations get mapped to
+ApertureMacroInstance_ (also an Aperture_ subclass).
+
+All Aperture_ subclasses have these common attributes:
+
+
+`hole_dia`
+ float with diameter of hole. 0 for no hole.
+
+`hole_rect_h`
+ float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
+
+`unit`
+ LengthUnit_ for all of this aperture's fields
+
+`attrs`
+ GerberX2 attributes of this aperture. Note that this will only contain aperture attributes, not file attributes.
+ File attributes are stored in the `attrs` of GerberFile_.
+
+`original_number`
+ int of aperture index this aperture had when it was read from the Gerber file. This field is purely informational
+ since apertures are de-duplicated and re-numbered when writing a Gerber file. For `D10`, this field would be `10`.
+ If you programmatically create a new aperture, you do not have to set this.
+
+`rotation`
+ Aperture rotation in radians counter-clockwise. This field is not part of the Gerber standard. Standard rectangle
+ and obround apertures do not support rotation. Gerbonara converts rotated apertures into aperture macros during
+ Gerber export as necessary.
+
+CircleAperture
+--------------
+
+This is the only one valid for use in Line_ or Arc_.
+
+Attributes:
+
+Common attributes:
+ `hole_dia`, `hole_rect_h`, `unit`, `attrs`, and `original_number`. `rotation` is present but has no effect in
+ CircleAperture_.
+
+`diameter`
+ float with diameter of aperture in the unit from the aperture's `unit` field.
+
+RectangleAperture
+-----------------
+
+Common attributes:
+ `hole_dia`, `hole_rect_h`, `unit`, `attrs`, `original_number`, and `rotation`
+
+`w`, `h`
+ floats with width or height of rectangle in units from the aperture's `unit` field.
+
+ObroundAperture
+---------------
+
+Aperture whose shape is the convex hull of two circles of equal radii.
+
+Common attributes:
+ `hole_dia`, `hole_rect_h`, `unit`, `attrs`, `original_number`, and `rotation`
+
+`w`, `h`
+ floats with width and height of bounding box of obround. The smaller one of these will be the diameter of the
+ obround's ends. If `w` is larger, the result will be a landscape obround. If `h` is larger, it will be a portrait
+ obround.
+
+PolygonAperture
+---------------
+
+Aperture whose shape is a regular n-sided polygon (e.g. pentagon, hexagon etc.).
+
+
+Common attributes:
+ `hole_dia`, `unit`, `attrs`, `original_number`, and `rotation`. `hole_rect_h` is not supported in PolygonAperture_
+ since the Gerber spec does not list it.
+
+`diameter`
+ float with diameter of circumscribing circle, i.e. the circle that all the polygon's corners lie on.
+
+`n_vertices`
+ int with number of corners of this polygon. Three for a triangle, four for a square, five for a pentagon etc.
+
+ApertureMacroInstance
+---------------------
+
+One instance of an aperture macro. An aperture macro defined with an `AM` statement can be instantiated by multiple `AD`
+aperture definition statements using different parameters. An ApertureMacroInstance_ is one such binding of a macro to a
+particular set of parameters. Note that you still need an ApertureMacroInstance_ even if your ApertureMacro_ has no
+parameters since an ApertureMacro_ is not an Aperture_ by itself.
+
+Attributes:
+
+Common attributes:
+ `unit`, `attrs`, `original_number`, and `rotation`. ApertureMacroInstance_ does not support `hole_dia` or
+ `hole_rect_h`. `rotation` is handled by re-writing the ApertureMacro_ during export.
+
+`macro`
+ The ApertureMacro_ that is bound here
+
+`parameters`
+ list of ints or floats with the parameters for this macro. The first element is `$1`, the second is `$2` etc.
+
+ExcellonTool
+------------
+
+Special Aperture_ subclass for use in ExcellonFile_. Similar to CircleAperture_, but does not have `hole_dia` or
+`hole_rect_h`, and has additional `plated` and `depth_offset` attributes.
+
+
+Common attributes:
+ `unit`, `original_number`
+
+`plated`
+ bool or None. True if this hole/slot is copper-plated, False if not, and None if it is undefined or unknown.
+
+`depth_offset`
+ float with Excellon depth offset for this hole or slot. If the fab supports this, this can be used to create
+ features that do not go all the way through the board.
+
+Aperture generalization
+-----------------------
+
+Gerbonara supports rotating both individual graphic objects and whole files. Alas, this was not a use case that was
+intended when the Gerber format was developed. We can rotate lines, arcs, and regions alright by simply rotatint all of
+their points. Flashes are where things get tricky: Individual flashes cannot be rotated at all in any widely supported
+way. There are some newer additions to the standard, but I would be surprised if any of the cheap board houses
+understand those. The only way to rotate a flash is to rotate the aperture, not the flash. For cirlces, this is a no-op.
+For polygons, we simply change the angle parameter. However, for rectangles and obrounds this gets tricky: Neither one
+supports a rotation parameter. The only way to rotate these is to convert them to an aperture macro, then rotate that.
+
+This behavior of using aperture macros for general rotated rectangles is common behavior among CAD tools. Gerbonara adds
+a non-standard `rotation` attribute to all apertures except CircleAperture_ and transparently converts rotated instances
+to the appropriate ApertureMacroInstance_ objects while it writes out the file. Be aware that this may mean that an
+object that in memory has a RectangleAperture_ might end up with an aperture macro instance in the output Gerber file.
+
diff --git a/docs/api-concepts.rst b/docs/api-concepts.rst
new file mode 100644
index 0000000..075498b
--- /dev/null
+++ b/docs/api-concepts.rst
@@ -0,0 +1,60 @@
+Gerbonara API concepts
+======================
+
+High-level overview
+-------------------
+
+Gerbonara's API is split into three larger sub-areas:
+
+**File API**
+ This is where the main user interface classes live: :py:class:`.LayerStack` (for opening a directory/zip full of
+ files, and automatically matching file roles based on filenames), :py:class:`.GerberFile` (for opening an individual
+ RS-274X file), :py:class:`.ExcellonFile` (for Excellon drill files) and :py:class:`.Netlist` (for IPC-356 netlist
+ files).
+
+**Graphic Object API**
+ This is where the nuts and bolts inside a :py:class:`.GerberFile` or :py:class:`.ExcellonFile` such as
+ :py:class:`~.graphic_objects.Line`, :py:class:`~.graphic_objects.Arc`, :py:class:`.Region` and :py:class:`.Flash`
+ live. Everything in here has explicit unit support. A part of the Graphic object API is the :doc:`Aperture
+ API<apertures>`.
+
+**Graphic Primitive API**
+ This is a rendering abstraction layer. Graphic objects can be converted into graphic primitives for rendering.
+ Graphic primitives are unit-less. Units are converted during :py:class:`.GraphicObject` to
+ :py:class:`.GraphicPrimitive` rendering.
+
+The hierarchy works like: A :py:class:`.LayerStack` contains either a :py:class:`.GerberFile`, an
+:py:class:`.ExcellonFile` or a :py:class:`.Netlist` for each layer. Each of these file objects contains a number of
+:py:class:`.GraphicObject` instances such as :py:class:`~.graphic_objects.Line` or :py:class:`.Flash`. These objects can
+easily be changed or deleted, and new ones can be created programmatically. For rendering, each of these objects as well
+as file objects can be rendered into :py:class:`.GraphicPrimitive` instances using
+:py:meth:`.GraphicObject.to_primitives`.
+
+Apertures
+---------
+
+Gerber apertures are represented by subclasses of :py:class:`.Aperture` such as :py:class:`.CircleAperture`. An instance
+of an aperture class is stored inside the :py:attr:`~.graphic_objects.Line.aperture` field of a
+:py:class:`.GraphicObject`. :py:class:`.GraphicObject` subclasses that have an aperture are
+:py:class:`~.graphic_objects.Line`, :py:class:`~.graphic_objects.Arc` and :py:class:`.Flash`. You can create and
+duplicate :py:class:`.Aperture` objects as needed. They are automatically de-duplicated when a Gerber file is written.
+
+Gerbonara has full aperture macro support. Each aperture macro is represented by an :py:class:`.parse.ApertureMacro`
+instance. Like apertures, :py:class:`.parse.ApertureMacro` instances are de-duplicated when writing a file. An aperture
+macro-based aperture definition is represented by the :py:class:`.ApertureMacroInstance` subclass of
+:py:class:`.Aperture`. An aperture macro instance basically binds an aperture macro to a given set of macro parameters.
+Note that even if a macro does not accept any parameters you still cannot directly stick it into the aperture field of a
+graphic object, and instead need to wrap it inside an :py:class:`.ApertureMacroInstance` first.
+
+Excellon vs. Gerber
+-------------------
+
+Excellon files use the same graphic object classes as Gerber files. Inside an Excellon file, only
+:py:class:`~.graphic_objects.Line`, :py:class:`~.graphic_objects.Arc` and :py:class:`.Flash` are allowed. Lines and arcs map to milled
+Excellon slots. Excellon drills are mapped to :py:class:`.Flash` instances.
+
+Excellon drills are internally handled using a special :py:class:`.ExcellonTool` aperture class. When you put a
+:py:class:`.GraphicObject` from an Excellon file into a Gerber file, these become circular apertures. You can also take
+objects from an Excellon file and put them into a Gerber file if they have a simple :py:class:`.CircleAperture`. Copying
+objects with other apertures into an Excellon file will raise an error when saving.
+
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..915f0dc
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,57 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# 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.
+
+from pathlib import Path
+import sys
+sys.path.insert(0, str(Path(__file__).parent.parent.absolute()))
+
+# -- Project information -----------------------------------------------------
+
+project = 'gerbonara'
+copyright = '2022, Jan Götte'
+author = 'jaseg'
+
+# The full version, including alpha/beta/rc tags
+release = '0.9.0'
+
+
+# -- General configuration ---------------------------------------------------
+
+# 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',
+]
+
+autodoc_member_order = 'groupwise'
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- 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 = 'alabaster'
+
+# 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']
diff --git a/docs/file-api.rst b/docs/file-api.rst
new file mode 100644
index 0000000..d73b5f1
--- /dev/null
+++ b/docs/file-api.rst
@@ -0,0 +1,25 @@
+Layers and Files
+================
+
+Gerbonara currently supports three file types: RS-274-X Gerber as `specified by Ucamco
+<https://www.ucamco.com/en/gerber>`:py:class:`._` through :py:class:`.GerberFile`, Excellon/XNC through
+:py:class:`.ExcellonFile`, and IPC-356 netlists through :py:class:`.Netlist`.
+
+Usually, a PCB is sent to a manufacturer as a bundle of several of these files. Such a bundle of files (each of which is
+either a :py:class:`.GerberFile` or an :py:class:`.ExcellonFile`) is represented by :py:class:`.LayerStack`.
+:py:class:`.LayerStack` contains logic to automatcally
+recognize a wide variety of CAD tools from file name and syntactic hints, and can automatically match all files in a
+folder to their appropriate layers.
+
+.. autoclass:: gerbonara.layers.LayerStack
+ :members:
+
+.. autoclass:: gerbonara.rs274x.GerberFile
+ :members:
+
+.. autoclass:: gerbonara.excellon.ExcellonFile
+ :members:
+
+.. autoclass:: gerbonara.ipc356.Netlist
+ :members:
+
diff --git a/docs/graphic-primitive-api.rst b/docs/graphic-primitive-api.rst
new file mode 100644
index 0000000..d506e87
--- /dev/null
+++ b/docs/graphic-primitive-api.rst
@@ -0,0 +1,27 @@
+Graphic Primitives
+==================
+
+.. autoclass:: gerbonara.graphic_primitives.GraphicPrimitive
+ :members:
+
+.. autoclass:: gerbonara.graphic_primitives.Circle
+ :members:
+
+.. autoclass:: gerbonara.graphic_primitives.Obround
+ :members:
+
+.. autoclass:: gerbonara.graphic_primitives.ArcPoly
+ :members:
+
+.. autoclass:: gerbonara.graphic_primitives.Line
+ :members:
+
+.. autoclass:: gerbonara.graphic_primitives.Arc
+ :members:
+
+.. autoclass:: gerbonara.graphic_primitives.Rectangle
+ :members:
+
+.. autoclass:: gerbonara.graphic_primitives.RegularPolygon
+ :members:
+
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..55d2456
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,115 @@
+Welcome to gerbonara's documentation!
+=====================================
+
+Gerbonara is a library to read, modify and write PCB manufacturing files such as Gerber, Excellon and IPC-356 through a
+pythonic API. Gerbonara can open a folder of manufacturing files, and parse file names and metadata to figure out which
+file contains what. Gerbonara is tested using an extensive library of real-world example files from CAD tools including
+KiCAD, Altium, Eagle, Allegro, gEDA, Fritzing, Siemens/Mentor Graphics PADS, and Target3001!.
+
+Gerbonara's API is built on two principles:
+
+**Meaningful, object-oriented API**
+ Gerbonara abstracts away the details of the underlying file format such as tool indices, coordinate notation and
+ graphical state, and presents meaningful "graphical objects" such as a :py:class:`~primitives.Line`,
+ :py:class:`~primitives.Arc`, or :py:class:`.Region` through its API. These objects can be easily created,
+ manipulated or deleted from code without breaking anything else. You can even copy graphical objects between files,
+ and Gerbonara will automatically convert coordinate format, units etc. for you. :py:class:`.GerberFile` and
+ :py:class:`.ExcellonFile` use the same types of :doc:`graphic objects <object-api>`, so objects can be directly
+ copied between file types without conversion.
+
+**Unit-safety**
+ Gerbonara embeds physical :py:class:`.LengthUnit` information in all objects. The high-level API such as
+ :py:meth:`.LayerStack.merge` or :py:meth:`.GerberFile.offset` accepts arguments with an explicitly given unit and
+ automatically converts them as needed. Objects can be copied between :py:class:`.GerberFile` instances and unit
+ conversion will be handled transparently in the background.
+
+Gerbonara was started as an extensive refactoring of the pcb-tools_ and pcb-tools-extension_ packages. Both of these
+have statement-based APIs, that is, they parse input files into one python object for every line in the file. This means
+that when saving files they can recreate the input file almost byte by byte, but manipulating a file by changing
+statements without breaking things is *hard*.
+
+Gerbonara powers gerbolyze_, a tool for converting SVG_ vector graphics files into Gerber, and embedding SVG_ into
+existing Gerber files exported from a normal PCB tool for artistic purposes.
+
+Features
+========
+
+ * File I/O
+ * Gerber, Excellon (drill file), IPC-356 (netlist) read and write
+ * supports file-level operations: offset, rotate, merge for all file types
+ * Modification API (:py:class:`GraphicObject`)
+ * Rendering API (:py:class:`GraphicPrimitive`)
+ * SVG export
+ * Full aperture macro support, including transformations (offset, rotation)
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ api-concepts
+ file-api
+ object-api
+ apertures
+ aperture-macros
+ graphic-primitive-api
+ utilities
+
+Quick Start
+===========
+
+
+
+Development
+===========
+
+Gerbonara is developed on Gitlab under the gerbolyze org:
+
+https://gitlab.com/gerbolyze/gerbonara/
+
+A mirror of the repository can be found at:
+
+https://git.jaseg.de/gerbonara
+
+Our issue tracker is also on Gitlab:
+
+https://gitlab.com/gerbolyze/gerbonara/-/issues
+
+With Gebronara, we aim to support as many different format variants as possible. If you have a file that Gerbonara can't
+open, please file an issue on our issue tracker. Even if Gerbonara can open all your files, for regression testing we
+are very interested in example files generated by any CAD or CAM tool that is not already on the list of supported
+tools.
+
+Supported CAD Tools
+===================
+
+Compatibility with the output of these CAD tools is tested as part of our test suite using example files generated by
+these tools. Note that not all of these tools come with default Gerber file naming rules, so YMMV if your Gerbers use
+some non-standard naming convention.
+
+ * Allegro
+ * Altium
+ * Diptrace
+ * Eagle
+ * EasyEDA
+ * Fritzing
+ * gEDA
+ * KiCAD
+ * pcb-rnd
+ * Siemens / Mentor Graphics Xpedition
+ * Siemens / Mentor Graphics PADS
+ * Target 3001!
+ * Upverter
+ * Soon: Zuken CADSTAR and CR-8000
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+.. _pcb-tools: https://github.com/opiopan/pcb-tools-extension
+.. _pcb-tools-extension: https://github.com/curtacircuitos/pcb-tools/issues
+.. _gerbolyze: https://github.com/jaseg/gerbolyze
+.. _SVG: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics
+
diff --git a/docs/object-api.rst b/docs/object-api.rst
new file mode 100644
index 0000000..f6345b9
--- /dev/null
+++ b/docs/object-api.rst
@@ -0,0 +1,30 @@
+Graphic Objects
+===============
+
+Graphic objects are the lego blocks a gerbonara :py:class:`gerbonara.rs274x.GerberFile` or
+:py:class:`gerbonara.excellon.ExcellonFile` is built from. They are stored in the file's
+:py:attr:`gerbonara.rs274x.GerberFile.objects` list. You can directly manipulate that list from code.
+
+There are four graphic object types: :py:class:`gerbonara.graphic_objects.Flash`,
+:py:class:`gerbonara.graphic_objects.Line`, :py:class:`gerbonara.graphic_objects.Arc`, and
+:py:class:`gerbonara.graphic_objects.Region` . All of them are derived from
+:py:class:`gerbonara.graphic_objects.GraphicObject`.
+
+.. autoclass:: gerbonara.graphic_objects.GraphicObject
+ :members:
+
+.. autoclass:: gerbonara.graphic_objects.Flash
+ :members:
+
+.. autoclass:: gerbonara.graphic_objects.Line
+ :members:
+
+.. autoclass:: gerbonara.graphic_objects.Arc
+ :members:
+
+.. autoclass:: gerbonara.graphic_objects.Region
+ :members:
+
+.. _pcb-tools: https://github.com/opiopan/pcb-tools-extension
+.. _gerbolyze: https://github.com/jaseg/gerbolyze
+.. _svg-flatten: https://github.com/jaseg/gerbolyze/tree/main/svg-flatten
diff --git a/docs/utilities.rst b/docs/utilities.rst
new file mode 100644
index 0000000..89b1cde
--- /dev/null
+++ b/docs/utilities.rst
@@ -0,0 +1,3 @@
+Utilities
+=========
+
diff --git a/gerbonara/apertures.py b/gerbonara/apertures.py
index ab62077..086d5b1 100644
--- a/gerbonara/apertures.py
+++ b/gerbonara/apertures.py
@@ -95,6 +95,7 @@ class Aperture:
@dataclass(unsafe_hash=True)
class ExcellonTool(Aperture):
+ gerber_shape_code = 'C'
human_readable_shape = 'drill'
diameter : Length(float)
plated : bool = None
diff --git a/gerbonara/excellon.py b/gerbonara/excellon.py
index 3382ffe..96a78be 100755
--- a/gerbonara/excellon.py
+++ b/gerbonara/excellon.py
@@ -197,6 +197,9 @@ class ExcellonFile(CamFile):
else:
self.objects.append(obj_or_comment)
+ def to_excellon(self):
+ return self
+
def to_gerber(self):
apertures = {}
out = GerberFile()
@@ -292,7 +295,7 @@ class ExcellonFile(CamFile):
yield 'M30'
- def to_excellon(self, settings=None, drop_comments=True):
+ def generate_excellon(self, settings=None, drop_comments=True):
''' Export to Excellon format. This function always generates XNC, which is a well-defined subset of Excellon.
'''
if settings is None:
@@ -306,10 +309,11 @@ class ExcellonFile(CamFile):
def save(self, filename, settings=None, drop_comments=True):
with open(filename, 'w') as f:
- f.write(self.to_excellon(settings, drop_comments=drop_comments))
+ f.write(self.generate_excellon(settings, drop_comments=drop_comments))
def offset(self, x=0, y=0, unit=MM):
- self.objects = [ obj.with_offset(x, y, unit) for obj in self.objects ]
+ for obj in self.objects:
+ obj.offset(x, y, unit)
def rotate(self, angle, cx=0, cy=0, unit=MM):
if math.isclose(angle % (2*math.pi), 0):
diff --git a/gerbonara/graphic_objects.py b/gerbonara/graphic_objects.py
index 1f475a6..99f990f 100644
--- a/gerbonara/graphic_objects.py
+++ b/gerbonara/graphic_objects.py
@@ -1,8 +1,9 @@
import math
+import copy
from dataclasses import dataclass, KW_ONLY, astuple, replace, field, fields
-from .utils import MM, InterpMode
+from .utils import MM, InterpMode, to_unit
from . import graphic_primitives as gp
@@ -18,27 +19,94 @@ class Length:
def __init__(self, obj_type):
self.type = obj_type
+ def __repr__(self):
+ # This makes the automatically generated method signatures in the Sphinx docs look nice
+ return 'float'
+
@dataclass
-class GerberObject:
+class GraphicObject:
+ """ Base class for the graphic objects that make up a :py:class:`gerbonara.rs274x.GerberFile` or
+ :py:class:`gerbonara.excellon.ExcellonFile`. """
_ : KW_ONLY
+
+ #: bool representing the *color* of this feature: whether this is a *dark* or *clear* feature. Clear and dark are
+ #: meant in the sense that they are used in the Gerber spec and refer to whether the transparency film that this
+ #: file describes ends up black or clear at this spot. In a standard green PCB, a *polarity_dark=True* line will
+ #: show up as copper on the copper layer, white ink on the silkscreen layer, or an opening on the soldermask layer.
+ #: Clear features erase dark features, they are not transparent in the colloquial meaning. This property is ignored
+ #: for features of an :py:class:`gerbonara.excellon.ExcellonFile`.
polarity_dark : bool = True
+
+ #: :py:class:`gerbonara.utils.LengthUnit` used for all coordinate fields of this feature (such as `x` or `y`).
unit : str = None
+
+
+ #: `dict` containing GerberX2 attributes attached to this feature. Note that this does not include file attributes,
+ #: which are stored in the :py:class:`gerbonara.rs274x.GerberFile` object instead.
attrs : dict = field(default_factory=dict)
def converted(self, unit):
- return replace(self,
- **{ f.name: self.unit.convert_to(unit, getattr(self, f.name))
- for f in fields(self) if type(f.type) is Length })
+ """ Convert this gerber object to another :py:class:`gerbonara.utils.LengthUnit`.
+
+ :param unit: Either a :py:class:`gerbonara.utils.LengthUnit` instance or one of the strings ``'mm'`` or ``'inch'``.
+
+ :returns: A copy of this object using the new unit.
+ """
+ copy = copy.copy(self)
+ copy.convert_to(unit)
+
+ def convert_to(self, unit):
+ """ Convert this gerber object to another :py:class:`gerbonara.utils.LengthUnit` in-place.
+
+ :param unit: Either a :py:class:`gerbonara.utils.LengthUnit` instance or one of the strings ``'mm'`` or ``'inch'``.
+ """
+
+ for f in fields(self):
+ if type(f.type) is Length:
+ setattr(self, f.name, self.unit.convert_to(unit, getattr(self, f.name)))
+
+ self.unit = to_unit(unit)
+
+ def offset(self, dx, dy, unit=MM):
+ """ Add an offset to the location of this feature. The location can be given in either unit, and is
+ automatically converted into this object's local unit.
+
+ :param float dx: X offset, positive values move the object right.
+ :param float dy: Y offset, positive values move the object up. This is the opposite of the normal screen
+ coordinate system used in SVG and other computer graphics APIs.
+ """
- def with_offset(self, dx, dy, unit=MM):
dx, dy = self.unit(dx, unit), self.unit(dy, unit)
- return self._with_offset(dx, dy)
+ self._offset(dx, dy)
def rotate(self, rotation, cx=0, cy=0, unit=MM):
+ """ Rotate this object. The center of rotation can be given in either unit, and is automatically converted into
+ this object's local unit.
+
+ .. note:: The center's Y coordinate as well as the angle's polarity are flipped compared to computer graphics
+ convention since Gerber uses a bottom-to-top Y axis.
+
+ :param float rotation: rotation in radians clockwise.
+ :param float cx: X coordinate of center of rotation in *unit* units.
+ :param float cy: Y coordinate of center of rotation. (0,0) is at the bottom left of the image.
+ :param unit: :py:class:`gerbonara.utils.LengthUnit` or str with unit for *cx* and *cy*
+ """
+
cx, cy = self.unit(cx, unit), self.unit(cy, unit)
self._rotate(rotation, cx, cy)
def bounding_box(self, unit=None):
+ """ Return axis-aligned bounding box of this object in given unit. If no unit is given, return the bounding box
+ in the object's local unit (``self.unit``).
+
+ .. note:: This method returns bounding boxes in a different format than legacy pcb-tools_, which used
+ ``(min_x, max_x), (min_y, max_y)``
+
+ :param unit: :py:class:`gerbonara.utils.LengthUnit` or str with unit for return value.
+
+ :returns: tuple of tuples of floats: ``(min_x, min_y), (max_x, max_y)``
+ """
+
bboxes = [ p.bounding_box() for p in self.to_primitives(unit) ]
min_x = min(min_x for (min_x, _min_y), _ in bboxes)
min_y = min(min_y for (_min_x, min_y), _ in bboxes)
@@ -47,16 +115,62 @@ class GerberObject:
return ((min_x, min_y), (max_x, max_y))
def to_primitives(self, unit=None):
- raise NotImplementedError()
+ """ Render this object into low-level graphical primitives (subclasses of :py:class:`GraphicPrimitive`). This
+ computes out all coordinates in case aperture macros are involved, and resolves units. The output primitives are
+ converted into the given unit, and will be stripped of unit information. If no unit is given, use this object's
+ native unit (``self.unit``).
+
+ :param unit: :py:class:`gerbonara.utils.LengthUnit` or str with unit for return value.
+
+ :rtype: Iterator[:py:class:`GraphicPrimitive`]
+ """
+ return self._to_primitives(unit)
+
+ def _to_statements(self, gs):
+ """ Serialize this object into Gerber statements.
+
+ :param gs: :py:class:`rs274x.GraphicsState` object containing current Gerber state (polarity, selected aperture,
+ interpolation mode etc.).
+
+ :returns: Iterator yielding one string per line of output Gerber
+ :rtype: Iterator[str]
+ """
+ self._to_statements(gs)
+
+ def _to_xnc(self, ctx):
+ """ Serialize this object into XNC Excellon statements.
+
+ :param ctx: :py:class:`excellon.ExcellonContext` object containing current Excellon state (selected tool,
+ interpolation mode etc.).
+
+ :returns: Iterator yielding one string per line of output XNC code
+ :rtype: Iterator[str]
+ """
+ self._to_xnc(ctx)
+
@dataclass
-class Flash(GerberObject):
+class Flash(GraphicObject):
+ """ A flash is what happens when you "stamp" a Gerber aperture at some location. The :py:attr:`polarity_dark`
+ attribute that Flash inherits from :py:class:`GraphicObject` is ``True`` for normal flashes. If you set a Flash's
+ ``polarity_dark`` to ``False``, you invert the polarity of all of its features.
+
+ Flashes are also used to represent drilled holes in an :py:class:`gerbonara.excellon.ExcellonFile`. In this case,
+ :py:attr:`aperture` should be an instance of :py:class:`ExcellonTool`.
+ """
+
+ #: float with X coordinate of the center of this flash.
x : Length(float)
+
+ #: float with Y coordinate of the center of this flash.
y : Length(float)
+
+ #: Flashed Aperture. must be a subclass of :py:class:`Aperture`.
aperture : object
@property
def tool(self):
+ """ Alias for :py:attr:`aperture` for use inside an :py:class:`gerbonara.excellon.ExcellonFile`. """
return self.aperture
@tool.setter
@@ -65,19 +179,23 @@ class Flash(GerberObject):
@property
def plated(self):
- return self.tool.plated
+ """ (Excellon only) Returns if this is a plated hole. ``True`` (plated), ``False`` (non-plated) or ``None``
+ (plating undefined)
+ """
+ return getattr(self.tool, 'plated', None)
- def _with_offset(self, dx, dy):
- return replace(self, x=self.x+dx, y=self.y+dy)
+ def __offset(self, dx, dy):
+ self.x += dx
+ self.y += dy
def _rotate(self, rotation, cx=0, cy=0):
self.x, self.y = gp.rotate_point(self.x, self.y, rotation, cx, cy)
- def to_primitives(self, unit=None):
+ def _to_primitives(self, unit=None):
conv = self.converted(unit)
yield from self.aperture.flash(conv.x, conv.y, unit, self.polarity_dark)
- def to_statements(self, gs):
+ def _to_statements(self, gs):
yield from gs.set_polarity(self.polarity_dark)
yield from gs.set_aperture(self.aperture)
@@ -87,7 +205,7 @@ class Flash(GerberObject):
gs.update_point(self.x, self.y, unit=self.unit)
- def to_xnc(self, ctx):
+ def _to_xnc(self, ctx):
yield from ctx.select_tool(self.tool)
yield from ctx.drill_mode()
@@ -97,11 +215,31 @@ class Flash(GerberObject):
ctx.set_current_point(self.unit, self.x, self.y)
+ # internally used to compute Excellon file path length
def curve_length(self, unit=MM):
return 0
-class Region(GerberObject):
+class Region(GraphicObject):
+ """ Gerber "region", roughly equivalent to what in computer graphics you would call a polygon. A region is a single
+ filled area defined by a list of coordinates on its contour. A region's polarity is its "fill". A region does not
+ have a "stroke", and thus does not have an `aperture` field. Note that regions are a strict subset of what modern
+ computer graphics considers a polygon or path. Be careful when converting shapes from somewhere else into Gerber
+ regions. For arbitrary shapes (e.g. SVG paths) this is non-trivial, and I recommend you hava look at Gerbolyze_ /
+ svg-flatten_. Here's a list of special features of Gerber regions:
+
+ * A region's outline consists of straigt line segments and circular arcs and must always be closed.
+ * A region is always exactly one connected component.
+ * A region must not overlap itself anywhere.
+ * A region cannot have holes.
+
+ There is one exception from the last two rules: To emulate a region with a hole in it, *cut-ins* are allowed. At a
+ cut-in, the region is allowed to touch (but never overlap!) itself.
+
+ :attr poly: :py:class:`graphic_primitives.ArcPoly` describing the actual outline of this Region. The coordinates of
+ this poly are in the unit of this instance's :py:attr:`unit` field.
+ """
+
def __init__(self, outline=None, arc_centers=None, *, unit, polarity_dark):
super().__init__(unit=unit, polarity_dark=polarity_dark)
outline = [] if outline is None else outline
@@ -114,11 +252,8 @@ class Region(GerberObject):
def __bool__(self):
return bool(self.poly)
- def _with_offset(self, dx, dy):
- return Region([ (x+dx, y+dy) for x, y in self.poly.outline ],
- self.poly.arc_centers,
- polarity_dark=self.polarity_dark,
- unit=self.unit)
+ def _offset(self, dx, dy):
+ self.poly.outline = [ (x+dx, y+dy) for x, y in self.poly.outline ]
def _rotate(self, angle, cx=0, cy=0):
self.poly.outline = [ gp.rotate_point(x, y, angle, cx, cy) for x, y in self.poly.outline ]
@@ -138,7 +273,7 @@ class Region(GerberObject):
else:
self.poly.arc_centers.append(None)
- def to_primitives(self, unit=None):
+ def _to_primitives(self, unit=None):
self.poly.polarity_dark = self.polarity_dark # FIXME: is this the right spot to do this?
if unit == self.unit:
yield self.poly
@@ -188,17 +323,37 @@ class Region(GerberObject):
yield 'G37*'
@dataclass
-class Line(GerberObject):
- # Line with *round* end caps.
+class Line(GraphicObject):
+ """ A line is what happens when you "drag" a Gerber :py:class:`Aperture` from one point to another. Note that Gerber
+ lines are substantially funkier than normal lines as we know them from modern computer graphics such as SVG. A
+ Gerber line is defined as the area that is covered when you drag its aperture along. This means that for a
+ rectangular aperture, a horizontal line and a vertical line using the same aperture will have different widths.
+
+ .. warning:: Try to only ever use :py:class:`CircleAperture` with :py:class:`Line` and :py:class:`Arc` since other
+ aperture types are not widely supported by renderers / photoplotters even though they are part of the
+ spec.
+
+ .. note:: If you manipulate a :py:class:`Line`, it is okay to assume that it has round end caps and a defined width
+ as exceptions are really rare.
+ """
+ #: X coordinate of start point
x1 : Length(float)
+ #: Y coordinate of start point
y1 : Length(float)
+ #: X coordinate of end point
x2 : Length(float)
+ #: Y coordinate of end point
y2 : Length(float)
+ #: Aperture for this line. Should be a subclass of :py:class:`CircleAperture`, whose diameter determines the line
+ #: width.
aperture : object
- def _with_offset(self, dx, dy):
- return replace(self, x1=self.x1+dx, y1=self.y1+dy, x2=self.x2+dx, y2=self.y2+dy)
+ def _offset(self, dx, dy):
+ self.x1 += dx
+ self.y1 += dy
+ self.x2 += dx
+ self.y2 += dy
def _rotate(self, rotation, cx=0, cy=0):
self.x1, self.y1 = gp.rotate_point(self.x1, self.y1, rotation, cx, cy)
@@ -206,18 +361,17 @@ class Line(GerberObject):
@property
def p1(self):
+ """ Convenience alias for ``(self.x1, self.y1)`` returning start point of the line. """
return self.x1, self.y1
@property
def p2(self):
+ """ Convenience alias for ``(self.x2, self.y2)`` returning end point of the line. """
return self.x2, self.y2
@property
- def end_point(self):
- return self.p2
-
- @property
def tool(self):
+ """ Alias for :py:attr:`aperture` for use inside an :py:class:`gerbonara.excellon.ExcellonFile`. """
return self.aperture
@tool.setter
@@ -226,14 +380,17 @@ class Line(GerberObject):
@property
def plated(self):
+ """ (Excellon only) Returns if this is a plated hole. ``True`` (plated), ``False`` (non-plated) or ``None``
+ (plating undefined)
+ """
return self.tool.plated
- def to_primitives(self, unit=None):
+ def _to_primitives(self, unit=None):
conv = self.converted(unit)
w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
yield gp.Line(*conv.p1, *conv.p2, w, polarity_dark=self.polarity_dark)
- def to_statements(self, gs):
+ def _to_statements(self, gs):
yield from gs.set_polarity(self.polarity_dark)
yield from gs.set_aperture(self.aperture)
yield from gs.set_interpolation_mode(InterpMode.LINEAR)
@@ -245,7 +402,7 @@ class Line(GerberObject):
gs.update_point(*self.p2, unit=self.unit)
- def to_xnc(self, ctx):
+ def _to_xnc(self, ctx):
yield from ctx.select_tool(self.tool)
yield from ctx.route_mode(self.unit, *self.p1)
@@ -255,26 +412,61 @@ class Line(GerberObject):
ctx.set_current_point(self.unit, *self.p2)
+ # internally used to compute Excellon file path length
def curve_length(self, unit=MM):
return self.unit.convert_to(unit, math.dist(self.p1, self.p2))
@dataclass
-class Arc(GerberObject):
+class Arc(GraphicObject):
+ """ Like :py:class:`Line`, but a circular arc. Has start ``(x1, y1)`` and end ``(x2, y2)`` attributes like a
+ :py:class:`Line`, but additionally has a center ``(cx, cy)`` specified relative to the start point ``(x1, y1)``, as
+ well as a ``clockwise`` attribute indicating the arc's direction.
+
+ .. note:: The same warning on apertures that applies to :py:class:`Line` applies to :py:class:`Arc`, too.
+
+ .. warning:: When creating your own circles, you have to take care yourself that the center is actually the center
+ of a circle that goes through both (x1,y1) and (x2,y2). Elliptical arcs are *not* supported by either
+ us or the Gerber standard.
+ """
+ #: X coordinate of start point
x1 : Length(float)
+ #: Y coordinate of start point
y1 : Length(float)
+ #: X coordinate of end point
x2 : Length(float)
+ #: Y coordinate of end point
y2 : Length(float)
- # relative to (x1, x2)
+ #: X coordinate of arc center relative to ``x1``
cx : Length(float)
+ #: Y coordinate of arc center relative to ``x1``
cy : Length(float)
+ #: Direction of arc. ``True`` means clockwise. For a given center coordinate and endpoints there are always two
+ #: possible arcs, the large one and the small one. Flipping this switches between them.
clockwise : bool
+ #: Aperture for this arc. Should be a subclass of :py:class:`CircleAperture`, whose diameter determines the line
+ #: width.
aperture : object
- def _with_offset(self, dx, dy):
- return replace(self, x1=self.x1+dx, y1=self.y1+dy, x2=self.x2+dx, y2=self.y2+dy)
+ def _offset(self, dx, dy):
+ self.x1 += dx
+ self.y1 += dy
+ self.x2 += dx
+ self.y2 += dy
def numeric_error(self, unit=None):
+ """ Gerber arcs are sligtly over-determined. Since we have not just a radius, but center X and Y coordinates, an
+ "impossible" arc can be specified, where the start and end points do not lie on a circle around its center. This
+ function returns the absolute difference between the two radii (start - center) and (end - center) as an
+ indication on how bad this arc is.
+
+ .. note:: For arcs read from a Gerber file, this value can easily be in the order of magnitude of 1e-4. Gerber
+ files have very limited numerical resolution, and rounding errors will necessarily lead to numerical
+ accuracy issues with arcs.
+
+ :rtype: float
+ """
+ # This function is used internally to determine the right arc in multi-quadrant mode
conv = self.converted(unit)
cx, cy = conv.cx + conv.x1, conv.cy + conv.y1
r1 = math.dist((cx, cy), conv.p1)
@@ -282,6 +474,11 @@ class Arc(GerberObject):
return abs(r1 - r2)
def sweep_angle(self):
+ """ Calculate absolute sweep angle of arc. This is always a positive number.
+
+ :returns: Angle in clockwise radian between ``0`` and ``2*math.pi``
+ :rtype: float
+ """
cx, cy = self.cx + self.x1, self.cy + self.y1
x1, y1 = self.x1 - cx, self.y1 - cy
x2, y2 = self.x2 - cx, self.y2 - cy
@@ -301,26 +498,35 @@ class Arc(GerberObject):
@property
def p1(self):
+ """ Convenience alias for ``(self.x1, self.y1)`` returning start point of the arc. """
return self.x1, self.y1
@property
def p2(self):
+ """ Convenience alias for ``(self.x2, self.y2)`` returning end point of the arc. """
return self.x2, self.y2
@property
def center(self):
+ """ Returns the center of the arc in **absolute** coordinates.
+
+ :returns: ``(self.x1 + self.cx, self.y1 + self.cy)``
+ :rtype: tuple(float)
+ """
return self.cx + self.x1, self.cy + self.y1
@property
def center_relative(self):
- return self.cx, self.cy
+ """ Returns the center of the arc in relative coordinates.
- @property
- def end_point(self):
- return self.p2
+ :returns: ``(self.cx, self.cy)``
+ :rtype: tuple(float)
+ """
+ return self.cx, self.cy
@property
def tool(self):
+ """ Alias for :py:attr:`aperture` for use inside an :py:class:`gerbonara.excellon.ExcellonFile`. """
return self.aperture
@tool.setter
@@ -329,6 +535,9 @@ class Arc(GerberObject):
@property
def plated(self):
+ """ (Excellon only) Returns if this is a plated hole. ``True`` (plated), ``False`` (non-plated) or ``None``
+ (plating undefined)
+ """
return self.tool.plated
def _rotate(self, rotation, cx=0, cy=0):
@@ -338,7 +547,7 @@ class Arc(GerberObject):
self.x2, self.y2 = gp.rotate_point(self.x2, self.y2, rotation, cx, cy)
self.cx, self.cy = new_cx - self.x1, new_cy - self.y1
- def to_primitives(self, unit=None):
+ def _to_primitives(self, unit=None):
conv = self.converted(unit)
w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
yield gp.Arc(x1=conv.x1, y1=conv.y1,
@@ -348,7 +557,7 @@ class Arc(GerberObject):
width=w,
polarity_dark=self.polarity_dark)
- def to_statements(self, gs):
+ def _to_statements(self, gs):
yield from gs.set_polarity(self.polarity_dark)
yield from gs.set_aperture(self.aperture)
# TODO is the following line correct?
@@ -363,7 +572,7 @@ class Arc(GerberObject):
gs.update_point(*self.p2, unit=self.unit)
- def to_xnc(self, ctx):
+ def _to_xnc(self, ctx):
yield from ctx.select_tool(self.tool)
yield from ctx.route_mode(self.unit, self.x1, self.y1)
code = 'G02' if self.clockwise else 'G03'
@@ -376,6 +585,7 @@ class Arc(GerberObject):
ctx.set_current_point(self.unit, self.x2, self.y2)
+ # internally used to compute Excellon file path length
def curve_length(self, unit=MM):
return self.unit.convert_to(unit, math.hypot(self.cx, self.cy) * self.sweep_angle)
diff --git a/gerbonara/graphic_primitives.py b/gerbonara/graphic_primitives.py
index 65aa28c..889aa92 100644
--- a/gerbonara/graphic_primitives.py
+++ b/gerbonara/graphic_primitives.py
@@ -201,10 +201,10 @@ class ArcPoly(GraphicPrimitive):
# list of (x : float, y : float) tuples. Describes closed outline, i.e. first and last point are considered
# connected.
- outline : [(float,)]
+ outline : list
# must be either None (all segments are straight lines) or same length as outline.
# Straight line segments have None entry.
- arc_centers : [(float,)] = None
+ arc_centers : list = None
@property
def segments(self):
diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py
index 8bd622b..837440f 100644
--- a/gerbonara/rs274x.py
+++ b/gerbonara/rs274x.py
@@ -79,6 +79,9 @@ class GerberFile(CamFile):
return ExcellonFile(objects=new_objs, comments=self.comments)
+ def to_gerber(self):
+ return
+
def merge(self, other):
""" Merge other GerberFile into this one """
if other is None:
@@ -222,9 +225,9 @@ class GerberFile(CamFile):
def save(self, filename, settings=None, drop_comments=True):
with open(filename, 'w', encoding='utf-8') as f: # Encoding is specified as UTF-8 by spec.
- f.write(self.to_gerber(settings, drop_comments=drop_comments))
+ f.write(self.generate_gerber(settings, drop_comments=drop_comments))
- def to_gerber(self, settings=None, drop_comments=True):
+ def generate_gerber(self, settings=None, drop_comments=True):
# Use given settings, or use same settings as original file if not given, or use defaults if not imported from a
# file
if settings is None:
@@ -245,8 +248,8 @@ class GerberFile(CamFile):
def offset(self, dx=0, dy=0, unit=MM):
# TODO round offset to file resolution
-
- self.objects = [ obj.with_offset(dx, dy, unit) for obj in self.objects ]
+ for obj in self.objects:
+ obj.with_offset(dx, dy, unit)
def rotate(self, angle:'radian', center=(0,0), unit=MM):
""" Rotate file contents around given point.
diff --git a/setup.py b/setup.py
index d1a99eb..94b1e56 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@ setup(
name='gerbonara',
version=version(),
author='jaseg, XenGi',
- author_email='contact@gerbonara.jaseg.de',
+ author_email='gerbonara@jaseg.de',
description='Tools to handle Gerber and Excellon files in Python',
long_description=long_description(),
long_description_content_type='text/markdown',