Merge pull request #4 from hamiltonkibbe/master
Many fixes in parsing, rendering and new features
4
.coveragerc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[report]
|
||||
omit =
|
||||
*/python?.?/*
|
||||
*/site-packages/nose/*
|
||||
3
.gitignore
vendored
|
|
@ -38,6 +38,9 @@ nosetests.xml
|
|||
.idea/misc.xml
|
||||
.idea
|
||||
|
||||
# Komodo Files
|
||||
*.komodoproject
|
||||
|
||||
# OS Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
16
.travis.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
|
||||
# command to install dependencies
|
||||
install:
|
||||
- "pip install -r requirements.txt"
|
||||
- "pip install coveralls"
|
||||
|
||||
# command to run tests
|
||||
script:
|
||||
- make test-coverage
|
||||
|
||||
# Coveralls
|
||||
after_success:
|
||||
- coveralls
|
||||
27
Makefile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
PYTHON ?= python
|
||||
NOSETESTS ?= nosetests
|
||||
|
||||
DOC_ROOT = doc
|
||||
|
||||
clean: doc-clean
|
||||
#$(PYTHON) setup.py clean
|
||||
find . -name '*.pyc' -delete
|
||||
rm -rf coverage .coverage
|
||||
rm -rf *.egg-info
|
||||
|
||||
test:
|
||||
$(NOSETESTS) -s -v gerber
|
||||
|
||||
test-coverage:
|
||||
rm -rf coverage .coverage
|
||||
$(NOSETESTS) -s -v --with-coverage --cover-package=gerber
|
||||
|
||||
doc-html:
|
||||
(cd $(DOC_ROOT); make html)
|
||||
|
||||
|
||||
doc-clean:
|
||||
(cd $(DOC_ROOT); make clean)
|
||||
|
||||
|
||||
40
README.md
|
|
@ -1,9 +1,31 @@
|
|||
gerber-tools
|
||||
============
|
||||
|
||||
This hopefully will be a useful set of tools to handle Gerber files in Python.
|
||||
|
||||
Right now we have a working parser and I am working on simple Gerber to SVG converter.
|
||||
|
||||
See gerber.md for some random information regardind Gerber format.
|
||||
|
||||
gerber-tools
|
||||
============
|
||||

|
||||
[](https://coveralls.io/r/hamiltonkibbe/gerber-tools?branch=master)
|
||||
|
||||
Tools to handle Gerber and Excellon files in Python.
|
||||
|
||||
Useage Example:
|
||||
---------------
|
||||
import gerber
|
||||
from gerber.render import GerberSvgContext
|
||||
|
||||
# Read gerber and Excellon files
|
||||
top_copper = gerber.read('example.GTL')
|
||||
nc_drill = gerber.read('example.txt')
|
||||
|
||||
# Rendering context
|
||||
ctx = GerberSvgContext()
|
||||
|
||||
# Create SVG image
|
||||
top_copper.render(ctx)
|
||||
nc_drill.render(ctx, 'composite.svg')
|
||||
|
||||
|
||||
Rendering Examples:
|
||||
-------------------
|
||||
###Top Composite rendering
|
||||

|
||||
|
||||
###Bottom Composite rendering
|
||||

|
||||
3
TODO.md
|
|
@ -1,3 +0,0 @@
|
|||
* add command line utilities: gerber svg, gerber transform --rotate --scale --translate, gerber merge --blueprint
|
||||
|
||||
* AM defined apertures
|
||||
177
doc/Makefile
Normal file
|
|
@ -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."
|
||||
242
doc/make.bat
Normal file
|
|
@ -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
|
||||
262
doc/source/conf.py
Normal file
|
|
@ -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'Gerber Tools'
|
||||
copyright = u'2014, Hamilton Kibbe'
|
||||
|
||||
# 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 = True
|
||||
|
||||
# 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 = 'GerberToolsdoc'
|
||||
|
||||
|
||||
# -- 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', 'GerberTools.tex', u'Gerber 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', 'gerbertools', u'Gerber 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', 'GerberTools', u'Gerber Tools Documentation',
|
||||
u'Hamilton Kibbe', 'GerberTools', 'One line description of project.',
|
||||
'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
|
||||
42
doc/source/documentation/excellon.rst
Normal file
|
|
@ -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:
|
||||
|
||||
11
doc/source/documentation/index.rst
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Gerber Tools Reference
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
Gerber (RS-274X) Files <rs274x>
|
||||
Excellon Files <excellon>
|
||||
Rendering <render>
|
||||
|
||||
|
||||
11
doc/source/documentation/render.rst
Normal file
|
|
@ -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:
|
||||
37
doc/source/documentation/rs274x.rst
Normal file
|
|
@ -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:
|
||||
23
doc/source/index.rst
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.. Gerber 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.
|
||||
|
||||
Gerber-Tools!
|
||||
========================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
intro
|
||||
documentation/index
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
19
doc/source/intro.rst
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Gerber Tools Intro
|
||||
==================
|
||||
|
||||
PCB CAM (Gerber) Files
|
||||
------------
|
||||
|
||||
PCB design files (artwork) are most often stored in `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.
|
||||
|
||||
|
||||
Gerber-Tools
|
||||
------------
|
||||
|
||||
The gerber-tools module provides tools for working with and rendering Gerber
|
||||
and Excellon files.
|
||||
|
||||
|
||||
5128
examples/board.html
|
Before Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 331 KiB |
5128
examples/board.svg
|
Before Width: | Height: | Size: 477 KiB |
BIN
examples/composite_bottom.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
2
examples/composite_bottom.svg
Normal file
|
After Width: | Height: | Size: 832 KiB |
BIN
examples/composite_top.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
2
examples/composite_top.svg
Normal file
|
After Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 132 KiB |
2852
examples/silk.svg
|
Before Width: | Height: | Size: 265 KiB |
BIN
examples/top.png
|
Before Width: | Height: | Size: 211 KiB |
2278
examples/top.svg
|
Before Width: | Height: | Size: 212 KiB |
73
gerber.md
|
|
@ -1,73 +0,0 @@
|
|||
|
||||
# Gerber (RS-274X or Extended Gerber) is a bilevel, resolution independent image format.
|
||||
|
||||
# // graphic objects
|
||||
# // draw: line segment, thickness, round or square line endings. (solid circle and rectangule apertures only)
|
||||
# // arc: circular arc, thickness, round endings. (solid circle standard aperture only)
|
||||
# // flash: replication of a given apertura (shape)
|
||||
# // region: are defined by a countour (linear/arc segments.)
|
||||
#
|
||||
# // draw/arc: can have zero length (just flash the aperture)
|
||||
# // flash: any aperture can be flashed
|
||||
#
|
||||
# // operation codes operates on coordinate data blocks. each operation code is for one coordinate data block pair and vice-versa.
|
||||
# // D01: stroke an aperture from current point to coordinate pair. region mode off. lights-on move.
|
||||
# // D02: move current point to this coordinate pair
|
||||
# // D03: flash current aperture at this coordinate pair.
|
||||
#
|
||||
# // graphics state
|
||||
# // all state controlled by codes and parameters, except current point
|
||||
# //
|
||||
# // state fixed? initial value
|
||||
# // coordinate format fixed undefined
|
||||
# // unit fixed undefined
|
||||
# // image polarity fixed positive
|
||||
# // steps/repeat variable 1,1,-,-
|
||||
# // level polarity variable dark
|
||||
# // region mode variable off
|
||||
# // current aperture variable undefined
|
||||
# // quadrant mode variable undefined
|
||||
# // interpolation mode variable undefined
|
||||
# // current point variable (0,0)
|
||||
#
|
||||
# // attributes: metadata, both standard and custom. No change on image.
|
||||
#
|
||||
# // G01: linear
|
||||
# // G04: comment
|
||||
# // M02: end of file
|
||||
# // D: select aperture
|
||||
# // G75: multi quadrant mode (circles)
|
||||
# // G36: region begin
|
||||
# // G37: region end
|
||||
#
|
||||
# // [G01] [Xnnfffff] [Ynnffff] D01*
|
||||
#
|
||||
# // ASCII 32-126, CR LF.
|
||||
# // * end-of-block
|
||||
# // % parameer delimiter
|
||||
# // , field separator
|
||||
# // <space> only in comments
|
||||
# // case sensitive
|
||||
#
|
||||
# // int: +/- 32 bit signed
|
||||
# // decimal: +/- digits
|
||||
# // names: [a-zA-Z_$]{[a-zA-Z_$0-9]+} (255)
|
||||
# // strings: [a-zA-Z0-9_+-/!?<>”’(){}.\|&@# ]+ (65535)
|
||||
#
|
||||
# // data block: end in *
|
||||
# // statement: one or more data block, if contain parameters starts and end in % (parameter statement)
|
||||
# // statement: [%]<Data Block>{<Data Block>}[%]
|
||||
# // statements: function code, coordinate data, parameters
|
||||
#
|
||||
# // function code: operation codes (D01..) or code that set state.
|
||||
# // function codes applies before operation codes act on coordinates
|
||||
#
|
||||
# // coordinate data: <Coordinate data>: [X<Number>][Y<Number>][I<Number>][J<Number>](D01|D02|D03)
|
||||
# // offsets are not modal
|
||||
#
|
||||
# // parameter: %Parameter code<required modifiers>[optional modifiers]*%
|
||||
# // code: 2 characters
|
||||
#
|
||||
# // parameters can have line separators: %<Parameter>{{<Line separator>}<Parameter>}%
|
||||
#
|
||||
# // function code: (GDM){1}[number], parameters: [AZ]{2}
|
||||
|
|
@ -14,3 +14,13 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Gerber Tools
|
||||
============
|
||||
**Gerber Tools**
|
||||
|
||||
gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon
|
||||
files in python.
|
||||
"""
|
||||
|
||||
from .common import read
|
||||
|
|
@ -10,22 +10,32 @@
|
|||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
if __name__ == '__main__':
|
||||
from .parser import GerberParser
|
||||
from .render import GerberContext
|
||||
|
||||
from .common import read
|
||||
from .render import GerberSvgContext
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "Usage: python -m gerber <filename> <filename>..."
|
||||
sys.exit(1)
|
||||
|
||||
ctx = GerberSvgContext()
|
||||
ctx.set_alpha(0.95)
|
||||
for filename in sys.argv[1:]:
|
||||
print "parsing %s" % filename
|
||||
g = GerberParser(GerberContext())
|
||||
g.parse(filename)
|
||||
if 'GTO' in filename or 'GBO' in filename:
|
||||
ctx.set_color((1, 1, 1))
|
||||
ctx.set_alpha(0.8)
|
||||
elif 'GTS' in filename or 'GBS' in filename:
|
||||
ctx.set_color((0.2, 0.2, 0.75))
|
||||
ctx.set_alpha(0.8)
|
||||
gerberfile = read(filename)
|
||||
gerberfile.render(ctx)
|
||||
|
||||
print('Saving image to test.svg')
|
||||
ctx.dump('test.svg')
|
||||
|
|
|
|||
178
gerber/cam.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#! /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
|
||||
"""
|
||||
def __init__(self, notation='absolute', units='inch',
|
||||
zero_suppression='trailing', format=(2, 5)):
|
||||
if notation not in ['absolute', 'incremental']:
|
||||
raise ValueError('Notation must be either absolute or incremental')
|
||||
self.notation = notation
|
||||
|
||||
if units not in ['inch', 'metric']:
|
||||
raise ValueError('Units must be either inch or metric')
|
||||
self.units = units
|
||||
|
||||
if zero_suppression not in ['leading', 'trailing']:
|
||||
raise ValueError('Zero suppression must be either leading or \
|
||||
trailling')
|
||||
self.zero_suppression = zero_suppression
|
||||
|
||||
if len(format) != 2:
|
||||
raise ValueError('Format must be a tuple(n=2) of integers')
|
||||
self.format = format
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == 'notation':
|
||||
return self.notation
|
||||
elif key == 'units':
|
||||
return self.units
|
||||
elif key == 'zero_suppression':
|
||||
return self.zero_suppression
|
||||
elif key == 'format':
|
||||
return self.format
|
||||
else:
|
||||
raise KeyError()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key == 'notation':
|
||||
if value not in ['absolute', 'incremental']:
|
||||
raise ValueError('Notation must be either \
|
||||
absolute or incremental')
|
||||
self.notation = value
|
||||
elif key == 'units':
|
||||
if value not in ['inch', 'metric']:
|
||||
raise ValueError('Units must be either inch or metric')
|
||||
self.units = value
|
||||
elif key == 'zero_suppression':
|
||||
if value not in ['leading', 'trailing']:
|
||||
raise ValueError('Zero suppression must be either leading or \
|
||||
trailling')
|
||||
self.zero_suppression = value
|
||||
elif key == 'format':
|
||||
if len(value) != 2:
|
||||
raise ValueError('Format must be a tuple(n=2) of integers')
|
||||
self.format = value
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.notation == other.notation and
|
||||
self.units == other.units and
|
||||
self.zero_suppression == other.zero_suppression and
|
||||
self.format == other.format)
|
||||
|
||||
|
||||
|
||||
class CamFile(object):
|
||||
""" Base class for Gerber/Excellon files.
|
||||
|
||||
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.format = settings['format']
|
||||
else:
|
||||
self.notation = 'absolute'
|
||||
self.units = 'inch'
|
||||
self.zero_suppression = 'trailing'
|
||||
self.format = (2, 5)
|
||||
self.statements = statements if statements is not None else []
|
||||
self.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 baundaries
|
||||
"""
|
||||
pass
|
||||
|
||||
def render(self, ctx, 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`
|
||||
"""
|
||||
ctx.set_bounds(self.bounds)
|
||||
for p in self.primitives:
|
||||
ctx.render(p)
|
||||
if filename is not None:
|
||||
ctx.dump(filename)
|
||||
42
gerber/common.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#! /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.
|
||||
|
||||
|
||||
def read(filename):
|
||||
""" Read a gerber or excellon file and return a representative object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of the file to read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : CncFile subclass
|
||||
CncFile object representing the file, either GerberFile or
|
||||
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
|
||||
"""
|
||||
import rs274x
|
||||
import excellon
|
||||
from utils import detect_file_format
|
||||
fmt = detect_file_format(filename)
|
||||
if fmt == 'rs274x':
|
||||
return rs274x.read(filename)
|
||||
elif fmt == 'excellon':
|
||||
return excellon.read(filename)
|
||||
else:
|
||||
return None
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
|
@ -13,138 +13,229 @@
|
|||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
This module provides Excellon file classes and parsing utilities
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
from itertools import tee, izip
|
||||
from .utils import parse_gerber_value
|
||||
|
||||
from .excellon_statements import *
|
||||
from .cam import CamFile, FileSettings
|
||||
from .primitives import Drill
|
||||
import math
|
||||
|
||||
def read(filename):
|
||||
""" Read data from filename and return an ExcellonFile
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of file to parse
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.excellon.ExcellonFile`
|
||||
An ExcellonFile created from the specified file.
|
||||
|
||||
"""
|
||||
detected_settings = detect_excellon_format(filename)
|
||||
settings = FileSettings(**detected_settings)
|
||||
zeros = ''
|
||||
return ExcellonParser(settings).parse(filename)
|
||||
|
||||
|
||||
class ExcellonFile(CamFile):
|
||||
""" A class representing a single excellon file
|
||||
|
||||
class Tool(object):
|
||||
The ExcellonFile class represents a single excellon file.
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line, settings):
|
||||
commands = re.split('([BCFHSTZ])', line)[1:]
|
||||
commands = [(command, value) for command, value in pairwise(commands)]
|
||||
args = {}
|
||||
format = settings['format']
|
||||
zero_suppression = settings['zero_suppression']
|
||||
for cmd, val in commands:
|
||||
if cmd == 'B':
|
||||
args['retract_rate'] = parse_gerber_value(val, format, zero_suppression)
|
||||
elif cmd == 'C':
|
||||
args['diameter'] = parse_gerber_value(val, format, zero_suppression)
|
||||
elif cmd == 'F':
|
||||
args['feed_rate'] = parse_gerber_value(val, format, zero_suppression)
|
||||
elif cmd == 'H':
|
||||
args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression)
|
||||
elif cmd == 'S':
|
||||
args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression)
|
||||
elif cmd == 'T':
|
||||
args['number'] = int(val)
|
||||
elif cmd == 'Z':
|
||||
args['depth_offset'] = parse_gerber_value(val, format, zero_suppression)
|
||||
return cls(settings, **args)
|
||||
|
||||
def __init__(self, settings, **kwargs):
|
||||
self.number = kwargs.get('number')
|
||||
self.feed_rate = kwargs.get('feed_rate')
|
||||
self.retract_rate = kwargs.get('retract_rate')
|
||||
self.rpm = kwargs.get('rpm')
|
||||
self.diameter = kwargs.get('diameter')
|
||||
self.max_hit_count = kwargs.get('max_hit_count')
|
||||
self.depth_offset = kwargs.get('depth_offset')
|
||||
self.units = settings.get('units', 'inch')
|
||||
|
||||
def __repr__(self):
|
||||
unit = 'in.' if self.units == 'inch' else 'mm'
|
||||
return '<Tool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
|
||||
|
||||
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
|
||||
self.primitives = [Drill(position, tool.diameter)
|
||||
for tool, position in self.hits]
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
xmin = ymin = 100000000000
|
||||
xmax = ymax = -100000000000
|
||||
for tool, position in self.hits:
|
||||
radius = tool.diameter / 2.
|
||||
x = position[0]
|
||||
y = position[1]
|
||||
xmin = min(x - radius, xmin)
|
||||
xmax = max(x + radius, xmax)
|
||||
ymin = min(y - radius, ymin)
|
||||
ymax = max(y + radius, ymax)
|
||||
return ((xmin, xmax), (ymin, ymax))
|
||||
|
||||
def report(self):
|
||||
""" Print drill report
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def write(self, filename):
|
||||
with open(filename, 'w') as f:
|
||||
for statement in self.statements:
|
||||
f.write(statement.to_excellon() + '\n')
|
||||
|
||||
|
||||
class ExcellonParser(object):
|
||||
def __init__(self, ctx=None):
|
||||
self.ctx=ctx
|
||||
""" Excellon File Parser
|
||||
|
||||
Parameters
|
||||
----------
|
||||
settings : FileSettings or dict-like
|
||||
Excellon file settings to use when interpreting the excellon file.
|
||||
"""
|
||||
def __init__(self, settings=None):
|
||||
self.notation = 'absolute'
|
||||
self.units = 'inch'
|
||||
self.zero_suppression = 'trailing'
|
||||
self.format = (2,5)
|
||||
self.format = (2, 5)
|
||||
self.state = 'INIT'
|
||||
self.statements = []
|
||||
self.tools = {}
|
||||
self.hits = []
|
||||
self.active_tool = None
|
||||
self.pos = [0., 0.]
|
||||
if ctx is not None:
|
||||
zeros = 'L' if self.zero_suppression == 'leading' else 'T'
|
||||
x = self.format
|
||||
y = self.format
|
||||
self.ctx.set_coord_format(zeros, x, y)
|
||||
self.pos = [0., 0.]
|
||||
if settings is not None:
|
||||
self.units = settings.units
|
||||
self.zero_suppression = settings.zero_suppression
|
||||
self.notation = settings.notation
|
||||
self.format = settings.format
|
||||
|
||||
|
||||
@property
|
||||
def coordinates(self):
|
||||
return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
xmin = ymin = 100000000000
|
||||
xmax = ymax = -100000000000
|
||||
for x, y in self.coordinates:
|
||||
if x is not None:
|
||||
xmin = x if x < xmin else xmin
|
||||
xmax = x if x > xmax else xmax
|
||||
if y is not None:
|
||||
ymin = y if y < ymin else ymin
|
||||
ymax = y if y > ymax else ymax
|
||||
return ((xmin, xmax), (ymin, ymax))
|
||||
|
||||
@property
|
||||
def hole_sizes(self):
|
||||
return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)]
|
||||
|
||||
@property
|
||||
def hole_count(self):
|
||||
return len(self.hits)
|
||||
|
||||
def parse(self, filename):
|
||||
with open(filename, 'r') as f:
|
||||
for line in f:
|
||||
self._parse(line)
|
||||
|
||||
def dump(self, filename='teste.svg'):
|
||||
if self.ctx is not None:
|
||||
self.ctx.dump(filename)
|
||||
|
||||
self._parse(line.strip())
|
||||
return ExcellonFile(self.statements, self.tools, self.hits,
|
||||
self._settings(), filename)
|
||||
|
||||
def _parse(self, line):
|
||||
if 'M48' in line:
|
||||
#line = line.strip()
|
||||
zs = self._settings().zero_suppression
|
||||
fmt = self._settings().format
|
||||
if line[0] == ';':
|
||||
self.statements.append(CommentStmt.from_excellon(line))
|
||||
|
||||
elif line[:3] == 'M48':
|
||||
self.statements.append(HeaderBeginStmt())
|
||||
self.state = 'HEADER'
|
||||
|
||||
if 'G00' in line:
|
||||
|
||||
elif line[0] == '%':
|
||||
self.statements.append(RewindStopStmt())
|
||||
if self.state == 'HEADER':
|
||||
self.state = 'DRILL'
|
||||
|
||||
elif line[:3] == 'M95':
|
||||
self.statements.append(HeaderEndStmt())
|
||||
if self.state == 'HEADER':
|
||||
self.state = 'DRILL'
|
||||
|
||||
elif line[:3] == 'M30':
|
||||
stmt = EndOfProgramStmt.from_excellon(line)
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'G00':
|
||||
self.state = 'ROUT'
|
||||
|
||||
if 'G05' in line:
|
||||
self.state = 'DRILL'
|
||||
|
||||
elif line[0] == '%' and self.state == 'HEADER':
|
||||
self.state = 'DRILL'
|
||||
|
||||
if 'INCH' in line or line.strip() == 'M72':
|
||||
self.units = 'inch'
|
||||
|
||||
elif 'METRIC' in line or line.strip() == 'M71':
|
||||
self.units = 'metric'
|
||||
|
||||
if 'LZ' in line:
|
||||
self.zero_suppression = 'trailing'
|
||||
|
||||
elif 'TZ' in line:
|
||||
self.zero_suppression = 'leading'
|
||||
|
||||
if 'ICI' in line and 'ON' in line or line.strip() == 'G91':
|
||||
self.notation = 'incremental'
|
||||
|
||||
if 'ICI' in line and 'OFF' in line or line.strip() == 'G90':
|
||||
self.notation = 'absolute'
|
||||
|
||||
zs = self._settings()['zero_suppression']
|
||||
fmt = self._settings()['format']
|
||||
|
||||
# tool definition
|
||||
if line[0] == 'T' and self.state == 'HEADER':
|
||||
tool = Tool.from_line(line,self._settings())
|
||||
elif line[:3] == 'G05':
|
||||
self.state = 'DRILL'
|
||||
|
||||
elif (('INCH' in line or 'METRIC' in line) and
|
||||
('LZ' in line or 'TZ' in line)):
|
||||
stmt = UnitStmt.from_excellon(line)
|
||||
self.units = stmt.units
|
||||
self.zero_suppression = stmt.zero_suppression
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'M71' or line [:3] == 'M72':
|
||||
stmt = MeasuringModeStmt.from_excellon(line)
|
||||
self.units = stmt.units
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'ICI':
|
||||
stmt = IncrementalModeStmt.from_excellon(line)
|
||||
self.notation = 'incremental' if stmt.mode == 'on' else 'absolute'
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'VER':
|
||||
stmt = VersionStmt.from_excellon(line)
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:4] == 'FMAT':
|
||||
stmt = FormatStmt.from_excellon(line)
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[0] == 'T' and self.state == 'HEADER':
|
||||
tool = ExcellonTool.from_excellon(line, self._settings())
|
||||
self.tools[tool.number] = tool
|
||||
|
||||
elif line[0] == 'T' and self.state != 'HEADER':
|
||||
self.active_tool = self.tools[int(line.strip().split('T')[1])]
|
||||
self.statements.append(tool)
|
||||
|
||||
|
||||
if line[0] in ['X', 'Y']:
|
||||
x = None
|
||||
y = None
|
||||
if line[0] == 'X':
|
||||
splitline = line.strip('X').split('Y')
|
||||
x = parse_gerber_value(splitline[0].strip(), fmt, zs)
|
||||
if len(splitline) == 2:
|
||||
y = parse_gerber_value(splitline[1].strip(), fmt,zs)
|
||||
else:
|
||||
y = parse_gerber_value(line.strip(' Y'), fmt,zs)
|
||||
|
||||
elif line[0] == 'T' and self.state != 'HEADER':
|
||||
stmt = ToolSelectionStmt.from_excellon(line)
|
||||
self.active_tool = self.tools[stmt.tool]
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[0] in ['X', 'Y']:
|
||||
stmt = CoordinateStmt.from_excellon(line, fmt, zs)
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
|
|
@ -155,26 +246,115 @@ class ExcellonParser(object):
|
|||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
if self.state == 'DRILL':
|
||||
self.hits.append((self.active_tool, self.pos))
|
||||
if self.ctx is not None:
|
||||
self.ctx.drill(self.pos[0], self.pos[1],
|
||||
self.active_tool.diameter)
|
||||
|
||||
if self.state == 'DRILL':
|
||||
self.hits.append((self.active_tool, tuple(self.pos)))
|
||||
self.active_tool._hit()
|
||||
else:
|
||||
self.statements.append(UnknownStmt.from_excellon(line))
|
||||
|
||||
def _settings(self):
|
||||
return {'units':self.units, 'zero_suppression':self.zero_suppression,
|
||||
'format': self.format}
|
||||
|
||||
def pairwise(iterator):
|
||||
itr = iter(iterator)
|
||||
while True:
|
||||
yield tuple([itr.next() for i in range(2)])
|
||||
|
||||
if __name__ == '__main__':
|
||||
from .render_svg import GerberSvgContext
|
||||
tools = []
|
||||
p = ExcellonParser(GerberSvgContext())
|
||||
p.parse('examples/ncdrill.txt')
|
||||
p.dump('excellon.svg')
|
||||
|
||||
|
||||
return FileSettings(units=self.units, format=self.format,
|
||||
zero_suppression=self.zero_suppression,
|
||||
notation=self.notation)
|
||||
|
||||
|
||||
def detect_excellon_format(filename):
|
||||
""" Detect excellon file decimal format and zero-suppression settings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Name of the file to parse. This does not check if the file is actually
|
||||
an Excellon file, so do that before calling this.
|
||||
|
||||
Returns
|
||||
-------
|
||||
settings : dict
|
||||
Detected excellon file settings. Keys are
|
||||
- `format`: decimal format as tuple (<int part>, <decimal part>)
|
||||
- `zero_suppression`: zero suppression, 'leading' or 'trailing'
|
||||
"""
|
||||
results = {}
|
||||
detected_zeros = None
|
||||
detected_format = None
|
||||
zs_options = ('leading', 'trailing', )
|
||||
format_options = ((2, 4), (2, 5), (3, 3),)
|
||||
|
||||
# Check for obvious clues:
|
||||
p = ExcellonParser()
|
||||
p.parse(filename)
|
||||
|
||||
# Get zero_suppression from a unit statement
|
||||
zero_statements = [stmt.zero_suppression for stmt in p.statements
|
||||
if isinstance(stmt, UnitStmt)]
|
||||
|
||||
# get format from altium comment
|
||||
format_comment = [stmt.comment for stmt in p.statements
|
||||
if isinstance(stmt, CommentStmt)
|
||||
and 'FILE_FORMAT' in stmt.comment]
|
||||
|
||||
detected_format = (tuple([int(val) for val in
|
||||
format_comment[0].split('=')[1].split(':')])
|
||||
if len(format_comment) == 1 else None)
|
||||
detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None
|
||||
|
||||
# Bail out here if possible
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zero_suppression': detected_zeros}
|
||||
|
||||
# Only look at remaining options
|
||||
if detected_format is not None:
|
||||
format_options = (detected_format,)
|
||||
if detected_zeros is not None:
|
||||
zs_options = (detected_zeros,)
|
||||
|
||||
# Brute force all remaining options, and pick the best looking one...
|
||||
for zs in zs_options:
|
||||
for fmt in format_options:
|
||||
key = (fmt, zs)
|
||||
settings = FileSettings(zero_suppression=zs, format=fmt)
|
||||
try:
|
||||
p = ExcellonParser(settings)
|
||||
p.parse(filename)
|
||||
size = tuple([t[1] - t[0] for t in p.bounds])
|
||||
hole_area = 0.0
|
||||
for hit in p.hits:
|
||||
tool = hit[0]
|
||||
hole_area += math.pow(math.pi * tool.diameter / 2., 2)
|
||||
results[key] = (size, p.hole_count, hole_area)
|
||||
except:
|
||||
pass
|
||||
|
||||
# See if any of the dimensions are left with only a single option
|
||||
formats = set(key[0] for key in results.iterkeys())
|
||||
zeros = set(key[1] for key in results.iterkeys())
|
||||
if len(formats) == 1:
|
||||
detected_format = formats.pop()
|
||||
if len(zeros) == 1:
|
||||
detected_zeros = zeros.pop()
|
||||
|
||||
# Bail out here if we got everything....
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zero_suppression': detected_zeros}
|
||||
|
||||
# Otherwise score each option and pick the best candidate
|
||||
else:
|
||||
scores = {}
|
||||
for key in results.keys():
|
||||
size, count, diameter = results[key]
|
||||
scores[key] = _layer_size_score(size, count, diameter)
|
||||
minscore = min(scores.values())
|
||||
for key in scores.iterkeys():
|
||||
if scores[key] == minscore:
|
||||
return {'format': key[0], 'zero_suppression': key[1]}
|
||||
|
||||
|
||||
def _layer_size_score(size, hole_count, hole_area):
|
||||
""" Heuristic used for determining the correct file number interpretation.
|
||||
Lower is better.
|
||||
"""
|
||||
board_area = size[0] * size[1]
|
||||
hole_percentage = hole_area / board_area
|
||||
hole_score = (hole_percentage - 0.25) ** 2
|
||||
size_score = (board_area - 8) **2
|
||||
return hole_score * size_score
|
||||
|
|
|
|||
431
gerber/excellon_statements.py
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
#!/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**
|
||||
|
||||
"""
|
||||
|
||||
from .utils import parse_gerber_value, write_gerber_value, decimal_string
|
||||
import re
|
||||
|
||||
__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
|
||||
'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt',
|
||||
'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt',
|
||||
'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt',
|
||||
'MeasuringModeStmt', 'UnknownStmt',
|
||||
]
|
||||
|
||||
|
||||
class ExcellonStatement(object):
|
||||
""" Excellon Statement abstract base class
|
||||
"""
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
pass
|
||||
|
||||
def to_excellon(self):
|
||||
pass
|
||||
|
||||
|
||||
class ExcellonTool(ExcellonStatement):
|
||||
""" Excellon Tool class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
settings : FileSettings (dict-like)
|
||||
File-wide settings.
|
||||
|
||||
kwargs : dict-like
|
||||
Tool settings from the excellon statement. Valid keys are:
|
||||
- `diameter` : Tool diameter [expressed in file units]
|
||||
- `rpm` : Tool RPM
|
||||
- `feed_rate` : Z-axis tool feed rate
|
||||
- `retract_rate` : Z-axis tool retraction rate
|
||||
- `max_hit_count` : Number of hits allowed before a tool change
|
||||
- `depth_offset` : Offset of tool depth from tip of tool.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
number : integer
|
||||
Tool number from the excellon file
|
||||
|
||||
diameter : float
|
||||
Tool diameter in file units
|
||||
|
||||
rpm : float
|
||||
Tool RPM
|
||||
|
||||
feed_rate : float
|
||||
Tool Z-axis feed rate.
|
||||
|
||||
retract_rate : float
|
||||
Tool Z-axis retract rate
|
||||
|
||||
depth_offset : float
|
||||
Offset of depth measurement from tip of tool
|
||||
|
||||
max_hit_count : integer
|
||||
Maximum number of tool hits allowed before a tool change
|
||||
|
||||
hit_count : integer
|
||||
Number of tool hits in excellon file.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings):
|
||||
""" Create a Tool from an excellon file tool definition line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : string
|
||||
Tool definition line from an excellon file.
|
||||
|
||||
settings : FileSettings (dict-like)
|
||||
Excellon file-wide settings
|
||||
|
||||
Returns
|
||||
-------
|
||||
tool : Tool
|
||||
An ExcellonTool representing the tool defined in `line`
|
||||
"""
|
||||
commands = re.split('([BCFHSTZ])', line)[1:]
|
||||
commands = [(command, value) for command, value in pairwise(commands)]
|
||||
args = {}
|
||||
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)
|
||||
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):
|
||||
self.settings = settings
|
||||
self.number = kwargs.get('number')
|
||||
self.feed_rate = kwargs.get('feed_rate')
|
||||
self.retract_rate = kwargs.get('retract_rate')
|
||||
self.rpm = kwargs.get('rpm')
|
||||
self.diameter = kwargs.get('diameter')
|
||||
self.max_hit_count = kwargs.get('max_hit_count')
|
||||
self.depth_offset = kwargs.get('depth_offset')
|
||||
self.hit_count = 0
|
||||
|
||||
def to_excellon(self):
|
||||
fmt = self.settings.format
|
||||
zs = self.settings.format
|
||||
stmt = 'T%d' % 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 _hit(self):
|
||||
self.hit_count += 1
|
||||
|
||||
def __repr__(self):
|
||||
unit = 'in.' if self.settings.units == 'inch' else 'mm'
|
||||
return '<ExcellonTool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
|
||||
|
||||
|
||||
class ToolSelectionStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
""" Create a ToolSelectionStmt from an excellon file line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : string
|
||||
Line from an Excellon file
|
||||
|
||||
Returns
|
||||
-------
|
||||
tool_statement : ToolSelectionStmt
|
||||
ToolSelectionStmt representation of `line.`
|
||||
"""
|
||||
line = line[1:]
|
||||
compensation_index = None
|
||||
tool = int(line[:2])
|
||||
if len(line) > 2:
|
||||
compensation_index = int(line[2:])
|
||||
return cls(tool, compensation_index)
|
||||
|
||||
def __init__(self, tool, compensation_index=None):
|
||||
tool = int(tool)
|
||||
compensation_index = (int(compensation_index) if compensation_index
|
||||
is not None else None)
|
||||
self.tool = tool
|
||||
self.compensation_index = compensation_index
|
||||
|
||||
def to_excellon(self):
|
||||
stmt = 'T%02d' % self.tool
|
||||
if self.compensation_index is not None:
|
||||
stmt += '%02d' % self.compensation_index
|
||||
return stmt
|
||||
|
||||
|
||||
class CoordinateStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, nformat=(2, 5), zero_suppression='trailing'):
|
||||
x_coord = None
|
||||
y_coord = None
|
||||
if line[0] == 'X':
|
||||
splitline = line.strip('X').split('Y')
|
||||
x_coord = parse_gerber_value(splitline[0], nformat,
|
||||
zero_suppression)
|
||||
if len(splitline) == 2:
|
||||
y_coord = parse_gerber_value(splitline[1], nformat,
|
||||
zero_suppression)
|
||||
else:
|
||||
y_coord = parse_gerber_value(line.strip(' Y'), nformat,
|
||||
zero_suppression)
|
||||
return cls(x_coord, y_coord)
|
||||
|
||||
def __init__(self, x=None, y=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_excellon(self):
|
||||
stmt = ''
|
||||
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
|
||||
|
||||
|
||||
class CommentStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
return cls(line.lstrip(';'))
|
||||
|
||||
def __init__(self, comment):
|
||||
self.comment = comment
|
||||
|
||||
def to_excellon(self):
|
||||
return ';%s' % self.comment
|
||||
|
||||
|
||||
class HeaderBeginStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def to_excellon(self):
|
||||
return 'M48'
|
||||
|
||||
|
||||
class HeaderEndStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def to_excellon(self):
|
||||
return 'M95'
|
||||
|
||||
|
||||
class RewindStopStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def to_excellon(self):
|
||||
return '%'
|
||||
|
||||
|
||||
class EndOfProgramStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, x=None, y=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_excellon(self):
|
||||
stmt = 'M30'
|
||||
if self.x is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x)
|
||||
if self.y is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y)
|
||||
return stmt
|
||||
|
||||
|
||||
class UnitStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
units = 'inch' if 'INCH' in line else 'metric'
|
||||
zero_suppression = 'trailing' if 'LZ' in line else 'leading'
|
||||
return cls(units, zero_suppression)
|
||||
|
||||
def __init__(self, units='inch', zero_suppression='trailing'):
|
||||
self.units = units.lower()
|
||||
self.zero_suppression = zero_suppression
|
||||
|
||||
def to_excellon(self):
|
||||
stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
|
||||
'LZ' if self.zero_suppression == 'trailing'
|
||||
else 'TZ')
|
||||
return stmt
|
||||
|
||||
|
||||
class IncrementalModeStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
return cls('off') if 'OFF' in line else cls('on')
|
||||
|
||||
def __init__(self, mode='off'):
|
||||
if mode.lower() not in ['on', 'off']:
|
||||
raise ValueError('Mode may be "on" or "off"')
|
||||
self.mode = mode
|
||||
|
||||
def to_excellon(self):
|
||||
return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON')
|
||||
|
||||
|
||||
class VersionStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
version = int(line.split(',')[1])
|
||||
return cls(version)
|
||||
|
||||
def __init__(self, version=1):
|
||||
version = int(version)
|
||||
if version not in [1, 2]:
|
||||
raise ValueError('Valid versions are 1 or 2')
|
||||
self.version = version
|
||||
|
||||
def to_excellon(self):
|
||||
return 'VER,%d' % self.version
|
||||
|
||||
|
||||
class FormatStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
fmt = int(line.split(',')[1])
|
||||
return cls(fmt)
|
||||
|
||||
def __init__(self, format=1):
|
||||
format = int(format)
|
||||
if format not in [1, 2]:
|
||||
raise ValueError('Valid formats are 1 or 2')
|
||||
self.format = format
|
||||
|
||||
def to_excellon(self):
|
||||
return 'FMAT,%d' % self.format
|
||||
|
||||
|
||||
class LinkToolStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
linked = [int(tool) for tool in line.split('/')]
|
||||
return cls(linked)
|
||||
|
||||
def __init__(self, linked_tools):
|
||||
self.linked_tools = [int(x) for x in linked_tools]
|
||||
|
||||
def to_excellon(self):
|
||||
return '/'.join([str(x) for x in self.linked_tools])
|
||||
|
||||
|
||||
class MeasuringModeStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
if not ('M71' in line or 'M72' in line):
|
||||
raise ValueError('Not a measuring mode statement')
|
||||
return cls('inch') if 'M72' in line else cls('metric')
|
||||
|
||||
def __init__(self, units='inch'):
|
||||
units = units.lower()
|
||||
if units not in ['inch', 'metric']:
|
||||
raise ValueError('units must be "inch" or "metric"')
|
||||
self.units = units
|
||||
|
||||
def to_excellon(self):
|
||||
return 'M72' if self.units == 'inch' else 'M71'
|
||||
|
||||
|
||||
class UnknownStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
return cls(line)
|
||||
|
||||
def __init__(self, stmt):
|
||||
self.stmt = stmt
|
||||
|
||||
def to_excellon(self):
|
||||
return self.stmt
|
||||
|
||||
|
||||
def pairwise(iterator):
|
||||
""" Iterate over list taking two elements at a time.
|
||||
|
||||
e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)]
|
||||
"""
|
||||
itr = iter(iterator)
|
||||
while True:
|
||||
yield tuple([itr.next() for i in range(2)])
|
||||
676
gerber/gerber_statements.py
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Gerber (RS-274X) Statements
|
||||
===========================
|
||||
**Gerber RS-274X file statement classes**
|
||||
|
||||
"""
|
||||
from .utils import parse_gerber_value, write_gerber_value, decimal_string
|
||||
|
||||
|
||||
__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt',
|
||||
'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt',
|
||||
'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt',
|
||||
'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt',
|
||||
'ParamStmt']
|
||||
|
||||
|
||||
class Statement(object):
|
||||
""" Gerber statement Base class
|
||||
|
||||
The statement class provides a type attribute.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type : string
|
||||
String identifying the statement type.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
type : string
|
||||
String identifying the statement type.
|
||||
"""
|
||||
def __init__(self, stype):
|
||||
self.type = stype
|
||||
|
||||
def __str__(self):
|
||||
s = "<{0} ".format(self.__class__.__name__)
|
||||
|
||||
for key, value in self.__dict__.items():
|
||||
s += "{0}={1} ".format(key, value)
|
||||
|
||||
s = s.rstrip() + ">"
|
||||
return s
|
||||
|
||||
|
||||
class ParamStmt(Statement):
|
||||
""" 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_dict(cls, stmt_dict):
|
||||
"""
|
||||
"""
|
||||
param = stmt_dict.get('param')
|
||||
zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing'
|
||||
notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental'
|
||||
x = map(int, stmt_dict.get('x'))
|
||||
fmt = (x[0], x[1])
|
||||
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' or 'trailing'
|
||||
|
||||
notation : string
|
||||
Notation mode. May be either 'absolute' or 'incremental'
|
||||
|
||||
format : tuple (int, int)
|
||||
Gerber precision format expressed as a tuple containing:
|
||||
(number of integer-part digits, number of decimal-part digits)
|
||||
|
||||
Returns
|
||||
-------
|
||||
ParamStmt : FSParamStmt
|
||||
Initialized FSParamStmt class.
|
||||
|
||||
"""
|
||||
ParamStmt.__init__(self, param)
|
||||
self.zero_suppression = zero_suppression
|
||||
self.notation = notation
|
||||
self.format = format
|
||||
|
||||
def to_gerber(self):
|
||||
zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T'
|
||||
notation = 'A' if self.notation == 'absolute' else 'I'
|
||||
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_dict(cls, stmt_dict):
|
||||
param = stmt_dict.get('param')
|
||||
if stmt_dict.get('mo').lower() == 'in':
|
||||
mo = 'inch'
|
||||
elif stmt_dict.get('mo').lower() == 'mm':
|
||||
mo = 'metric'
|
||||
else:
|
||||
mo = None
|
||||
return cls(param, mo)
|
||||
|
||||
def __init__(self, param, mo):
|
||||
""" Initialize MOParamStmt class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param : string
|
||||
Parameter.
|
||||
|
||||
mo : string
|
||||
Measurement units. May be either 'inch' or 'metric'
|
||||
|
||||
Returns
|
||||
-------
|
||||
ParamStmt : MOParamStmt
|
||||
Initialized MOParamStmt class.
|
||||
|
||||
"""
|
||||
ParamStmt.__init__(self, param)
|
||||
self.mode = mo
|
||||
|
||||
def to_gerber(self):
|
||||
mode = 'MM' if self.mode == 'metric' else 'IN'
|
||||
return '%MO{0}*%'.format(mode)
|
||||
|
||||
def __str__(self):
|
||||
mode_str = 'millimeters' if self.mode == 'metric' else 'inches'
|
||||
return ('<Mode: %s>' % mode_str)
|
||||
|
||||
|
||||
class IPParamStmt(ParamStmt):
|
||||
""" IP - Gerber Image Polarity Statement. (Deprecated)
|
||||
"""
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
param = stmt_dict.get('param')
|
||||
ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative'
|
||||
return cls(param, ip)
|
||||
|
||||
def __init__(self, param, ip):
|
||||
""" Initialize IPParamStmt class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param : string
|
||||
Parameter string.
|
||||
|
||||
ip : string
|
||||
Image polarity. May be either'positive' or 'negative'
|
||||
|
||||
Returns
|
||||
-------
|
||||
ParamStmt : IPParamStmt
|
||||
Initialized IPParamStmt class.
|
||||
|
||||
"""
|
||||
ParamStmt.__init__(self, param)
|
||||
self.ip = ip
|
||||
|
||||
def to_gerber(self):
|
||||
ip = 'POS' if self.ip == 'positive' else 'NEG'
|
||||
return '%IP{0}*%'.format(ip)
|
||||
|
||||
def __str__(self):
|
||||
return ('<Image Polarity: %s>' % self.ip)
|
||||
|
||||
|
||||
class OFParamStmt(ParamStmt):
|
||||
""" OF - Gerber Offset statement (Deprecated)
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
param = stmt_dict.get('param')
|
||||
a = float(stmt_dict.get('a'))
|
||||
b = float(stmt_dict.get('b'))
|
||||
return cls(param, a, b)
|
||||
|
||||
def __init__(self, param, a, b):
|
||||
""" Initialize OFParamStmt class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param : string
|
||||
Parameter
|
||||
|
||||
a : float
|
||||
Offset along the output device A axis
|
||||
|
||||
b : float
|
||||
Offset along the output device B axis
|
||||
|
||||
Returns
|
||||
-------
|
||||
ParamStmt : OFParamStmt
|
||||
Initialized OFParamStmt class.
|
||||
|
||||
"""
|
||||
ParamStmt.__init__(self, param)
|
||||
self.a = a
|
||||
self.b = b
|
||||
|
||||
def to_gerber(self):
|
||||
ret = '%OF'
|
||||
if self.a:
|
||||
ret += 'A' + decimal_string(self.a, precision=6)
|
||||
if self.b:
|
||||
ret += 'B' + decimal_string(self.b, precision=6)
|
||||
return ret + '*%'
|
||||
|
||||
def __str__(self):
|
||||
offset_str = ''
|
||||
if self.a:
|
||||
offset_str += ('X: %f' % self.a)
|
||||
if self.b:
|
||||
offset_str += ('Y: %f' % self.b)
|
||||
return ('<Offset: %s>' % offset_str)
|
||||
|
||||
|
||||
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):
|
||||
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 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 : LPParamStmt
|
||||
Initialized LPParamStmt class.
|
||||
|
||||
"""
|
||||
ParamStmt.__init__(self, param)
|
||||
self.d = d
|
||||
self.shape = shape
|
||||
if modifiers is not None:
|
||||
self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")]
|
||||
else:
|
||||
self.modifiers = []
|
||||
|
||||
def to_gerber(self, settings):
|
||||
return '%ADD{0}{1},{2}*%'.format(self.d, self.shape,
|
||||
','.join(['X'.join(e) for e in self.modifiers]))
|
||||
|
||||
def __str__(self):
|
||||
if self.shape == 'C':
|
||||
shape = 'circle'
|
||||
elif self.shape == 'R':
|
||||
shape = 'rectangle'
|
||||
elif self.shape == 'O':
|
||||
shape = 'oblong'
|
||||
else:
|
||||
shape = self.shape
|
||||
|
||||
return '<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
|
||||
|
||||
def to_gerber(self):
|
||||
return '%AM{0}*{1}*%'.format(self.name, self.macro)
|
||||
|
||||
def __str__(self):
|
||||
return '<Aperture Macro %s: %s>' % (self.name, macro)
|
||||
|
||||
|
||||
class INParamStmt(ParamStmt):
|
||||
""" IN - Image Name Statement
|
||||
"""
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
return cls(**stmt_dict)
|
||||
|
||||
def __init__(self, param, name):
|
||||
""" Initialize INParamStmt class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param : string
|
||||
Parameter code
|
||||
|
||||
name : string
|
||||
Image name
|
||||
|
||||
Returns
|
||||
-------
|
||||
ParamStmt : INParamStmt
|
||||
Initialized INParamStmt class.
|
||||
|
||||
"""
|
||||
ParamStmt.__init__(self, param)
|
||||
self.name = name
|
||||
|
||||
def to_gerber(self):
|
||||
return '%IN{0}*%'.format(self.name)
|
||||
|
||||
def __str__(self):
|
||||
return '<Image Name: %s>' % self.name
|
||||
|
||||
|
||||
class LNParamStmt(ParamStmt):
|
||||
""" LN - Level Name Statement (Deprecated)
|
||||
"""
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
return cls(**stmt_dict)
|
||||
|
||||
def __init__(self, param, name):
|
||||
""" Initialize LNParamStmt class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param : string
|
||||
Parameter code
|
||||
|
||||
name : string
|
||||
Level name
|
||||
|
||||
Returns
|
||||
-------
|
||||
ParamStmt : LNParamStmt
|
||||
Initialized LNParamStmt class.
|
||||
|
||||
"""
|
||||
ParamStmt.__init__(self, param)
|
||||
self.name = name
|
||||
|
||||
def to_gerber(self):
|
||||
return '%LN{0}*%'.format(self.name)
|
||||
|
||||
def __str__(self):
|
||||
return '<Level Name: %s>' % self.name
|
||||
|
||||
|
||||
class CoordStmt(Statement):
|
||||
""" Coordinate Data Block
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict, settings):
|
||||
zeros = settings.zero_suppression
|
||||
format = settings.format
|
||||
function = stmt_dict['function']
|
||||
x = stmt_dict.get('x')
|
||||
y = stmt_dict.get('y')
|
||||
i = stmt_dict.get('i')
|
||||
j = stmt_dict.get('j')
|
||||
op = stmt_dict.get('op')
|
||||
|
||||
if x is not None:
|
||||
x = parse_gerber_value(stmt_dict.get('x'),
|
||||
format, zeros)
|
||||
if y is not None:
|
||||
y = parse_gerber_value(stmt_dict.get('y'),
|
||||
format, zeros)
|
||||
if i is not None:
|
||||
i = parse_gerber_value(stmt_dict.get('i'),
|
||||
format, zeros)
|
||||
if j is not None:
|
||||
j = parse_gerber_value(stmt_dict.get('j'),
|
||||
format, zeros)
|
||||
return cls(function, x, y, i, j, op, settings)
|
||||
|
||||
def __init__(self, function, x, y, i, j, op, settings):
|
||||
""" Initialize CoordStmt class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
function : string
|
||||
function
|
||||
|
||||
x : float
|
||||
X coordinate
|
||||
|
||||
y : float
|
||||
Y coordinate
|
||||
|
||||
i : float
|
||||
Coordinate offset in the X direction
|
||||
|
||||
j : float
|
||||
Coordinate offset in the Y direction
|
||||
|
||||
op : string
|
||||
Operation code
|
||||
|
||||
settings : dict {'zero_suppression', 'format'}
|
||||
Gerber file coordinate format
|
||||
|
||||
Returns
|
||||
-------
|
||||
Statement : CoordStmt
|
||||
Initialized CoordStmt class.
|
||||
|
||||
"""
|
||||
Statement.__init__(self, "COORD")
|
||||
self.zero_suppression = settings.zero_suppression
|
||||
self.format = settings.format
|
||||
self.function = function
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.i = i
|
||||
self.j = j
|
||||
self.op = op
|
||||
|
||||
def to_gerber(self):
|
||||
ret = ''
|
||||
if self.function:
|
||||
ret += self.function
|
||||
if self.x:
|
||||
ret += 'X{0}'.format(write_gerber_value(self.x, self.zeros,
|
||||
self.format))
|
||||
if self.y:
|
||||
ret += 'Y{0}'.format(write_gerber_value(self.y, self. zeros,
|
||||
self.format))
|
||||
if self.i:
|
||||
ret += 'I{0}'.format(write_gerber_value(self.i, self.zeros,
|
||||
self.format))
|
||||
if self.j:
|
||||
ret += 'J{0}'.format(write_gerber_value(self.j, self.zeros,
|
||||
self.format))
|
||||
if self.op:
|
||||
ret += self.op
|
||||
return ret + '*'
|
||||
|
||||
def __str__(self):
|
||||
coord_str = ''
|
||||
if self.function:
|
||||
coord_str += 'Fn: %s ' % self.function
|
||||
if self.x:
|
||||
coord_str += 'X: %f ' % self.x
|
||||
if self.y:
|
||||
coord_str += 'Y: %f ' % self.y
|
||||
if self.i:
|
||||
coord_str += 'I: %f ' % self.i
|
||||
if self.j:
|
||||
coord_str += 'J: %f ' % self.j
|
||||
if self.op:
|
||||
if self.op == 'D01':
|
||||
op = 'Lights On'
|
||||
elif self.op == 'D02':
|
||||
op = 'Lights Off'
|
||||
elif self.op == 'D03':
|
||||
op = 'Flash'
|
||||
else:
|
||||
op = self.op
|
||||
coord_str += 'Op: %s' % op
|
||||
|
||||
return '<Coordinate Statement: %s>' % coord_str
|
||||
|
||||
|
||||
class ApertureStmt(Statement):
|
||||
""" Aperture Statement
|
||||
"""
|
||||
def __init__(self, d):
|
||||
Statement.__init__(self, "APERTURE")
|
||||
self.d = int(d)
|
||||
|
||||
def to_gerber(self):
|
||||
return 'G54D{0}*'.format(self.d)
|
||||
|
||||
def __str__(self):
|
||||
return '<Aperture: %d>' % self.d
|
||||
|
||||
|
||||
class CommentStmt(Statement):
|
||||
""" Comment Statment
|
||||
"""
|
||||
|
||||
def __init__(self, comment):
|
||||
Statement.__init__(self, "COMMENT")
|
||||
self.comment = comment
|
||||
|
||||
def to_gerber(self):
|
||||
return 'G04{0}*'.format(self.comment)
|
||||
|
||||
def __str__(self):
|
||||
return '<Comment: %s>' % self.comment
|
||||
|
||||
|
||||
class EofStmt(Statement):
|
||||
""" EOF Statement
|
||||
"""
|
||||
def __init__(self):
|
||||
Statement.__init__(self, "EOF")
|
||||
|
||||
def to_gerber(self):
|
||||
return 'M02*'
|
||||
|
||||
def __str__(self):
|
||||
return '<EOF Statement>'
|
||||
|
||||
|
||||
class QuadrantModeStmt(Statement):
|
||||
|
||||
@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):
|
||||
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'))
|
||||
|
||||
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):
|
||||
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):
|
||||
return self.line
|
||||
54
gerber/layers.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#! /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.
|
||||
|
||||
top_copper_ext = ['gtl', 'cmp', 'top', ]
|
||||
top_copper_name = ['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
|
||||
|
||||
bottom_copper_ext = ['gbl', 'sld', 'bot', 'sol', ]
|
||||
bottom_coppper_name = ['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
|
||||
|
||||
internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
|
||||
'g2', 'g3', 'g4', 'g5', 'g6', ]
|
||||
internal_layer_name = ['art', 'internal']
|
||||
|
||||
power_plane_name = ['pgp', 'pwr', ]
|
||||
ground_plane_name = ['gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', 'gnd',
|
||||
'ground', ]
|
||||
|
||||
top_silk_ext = ['gto', 'sst', 'plc', 'ts', 'skt', ]
|
||||
top_silk_name = ['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
|
||||
|
||||
bottom_silk_ext = ['gbo', 'ssb', 'pls', 'bs', 'skb', ]
|
||||
bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ]
|
||||
|
||||
top_mask_ext = ['gts', 'stc', 'tmk', 'smt', 'tr', ]
|
||||
top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
|
||||
'mst', ]
|
||||
|
||||
bottom_mask_ext = ['gbs', 'sts', 'bmk', 'smb', 'br', ]
|
||||
bottom_mask_name = ['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
|
||||
|
||||
top_paste_ext = ['gtp', 'tm']
|
||||
top_paste_name = ['sp01', 'toppaste', 'pst']
|
||||
|
||||
bottom_paste_ext = ['gbp', 'bm']
|
||||
bottom_paste_name = ['sp02', 'botpaste', 'psb']
|
||||
|
||||
board_outline_ext = ['gko']
|
||||
board_outline_name = ['BDR', 'border', 'out', ]
|
||||
|
||||
|
||||
370
gerber/parser.py
|
|
@ -1,370 +0,0 @@
|
|||
#! /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 re
|
||||
import json
|
||||
|
||||
|
||||
class Statement(object):
|
||||
def __init__(self, type):
|
||||
self.type = type
|
||||
|
||||
def __str__(self):
|
||||
s = "<{0} ".format(self.__class__.__name__)
|
||||
|
||||
for key, value in self.__dict__.items():
|
||||
s += "{0}={1} ".format(key, value)
|
||||
|
||||
s = s.rstrip() + ">"
|
||||
return s
|
||||
|
||||
|
||||
class ParamStmt(Statement):
|
||||
def __init__(self, param):
|
||||
Statement.__init__(self, "PARAM")
|
||||
self.param = param
|
||||
|
||||
|
||||
|
||||
class FSParamStmt(ParamStmt):
|
||||
def __init__(self, param, zero="L", notation="A", x="24", y="24"):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.zero = zero
|
||||
self.notation = notation
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_gerber(self):
|
||||
return '%FS{0}{1}X{2}Y{3}*%'.format(self.zero, self.notation,
|
||||
self.x, self.y)
|
||||
|
||||
class MOParamStmt(ParamStmt):
|
||||
def __init__(self, param, mo):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.mo = mo
|
||||
|
||||
def to_gerber(self):
|
||||
return '%MO{0}*%'.format(self.mo)
|
||||
|
||||
class IPParamStmt(ParamStmt):
|
||||
def __init__(self, param, ip):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.ip = ip
|
||||
|
||||
def to_gerber(self):
|
||||
return '%IP{0}*%'.format(self.ip)
|
||||
|
||||
|
||||
class OFParamStmt(ParamStmt):
|
||||
def __init__(self, param, a, b):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.a = a
|
||||
self.b = b
|
||||
|
||||
def to_gerber(self):
|
||||
ret = '%OF'
|
||||
if self.a:
|
||||
ret += 'A' + self.a
|
||||
if self.b:
|
||||
ret += 'B' + self.b
|
||||
return ret + '*%'
|
||||
|
||||
|
||||
class LPParamStmt(ParamStmt):
|
||||
def __init__(self, param, lp):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.lp = lp
|
||||
|
||||
def to_gerber(self):
|
||||
return '%LP{0}*%'.format(self.lp)
|
||||
|
||||
|
||||
class ADParamStmt(ParamStmt):
|
||||
def __init__(self, param, d, shape, modifiers):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.d = d
|
||||
self.shape = shape
|
||||
if modifiers is not None:
|
||||
self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")]
|
||||
else:
|
||||
self.modifiers = []
|
||||
|
||||
def to_gerber(self):
|
||||
return '%ADD{0}{1},{2}*%'.format(self.d, self.shape,
|
||||
','.join(['X'.join(e) for e in self.modifiers]))
|
||||
|
||||
class AMParamStmt(ParamStmt):
|
||||
def __init__(self, param, name, macro):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.name = name
|
||||
self.macro = macro
|
||||
|
||||
def to_gerber(self):
|
||||
#think this is right...
|
||||
return '%AM{0}*{1}*%'.format(self.name, self.macro)
|
||||
|
||||
class INParamStmt(ParamStmt):
|
||||
def __init__(self, param, name):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.name = name
|
||||
|
||||
def to_gerber(self):
|
||||
return '%IN{0}*%'.format(self.name)
|
||||
|
||||
|
||||
class LNParamStmt(ParamStmt):
|
||||
def __init__(self, param, name):
|
||||
ParamStmt.__init__(self, param)
|
||||
self.name = name
|
||||
|
||||
def to_gerber(self):
|
||||
return '%LN{0}*%'.format(self.name)
|
||||
|
||||
class CoordStmt(Statement):
|
||||
def __init__(self, function, x, y, i, j, op):
|
||||
Statement.__init__(self, "COORD")
|
||||
self.function = function
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.i = i
|
||||
self.j = j
|
||||
self.op = op
|
||||
|
||||
def to_gerber(self):
|
||||
ret = ''
|
||||
if self.function:
|
||||
ret += self.function
|
||||
if self.x:
|
||||
ret += 'X{0}'.format(self.x)
|
||||
if self.y:
|
||||
ret += 'Y{0}'.format(self.y)
|
||||
if self.i:
|
||||
ret += 'I{0}'.format(self.i)
|
||||
if self.j:
|
||||
ret += 'J{0}'.format(self.j)
|
||||
if self.op:
|
||||
ret += self.op
|
||||
return ret + '*'
|
||||
|
||||
|
||||
class ApertureStmt(Statement):
|
||||
def __init__(self, d):
|
||||
Statement.__init__(self, "APERTURE")
|
||||
self.d = int(d)
|
||||
|
||||
def to_gerber(self):
|
||||
return 'G54D{0}*'.format(self.d)
|
||||
|
||||
class CommentStmt(Statement):
|
||||
def __init__(self, comment):
|
||||
Statement.__init__(self, "COMMENT")
|
||||
self.comment = comment
|
||||
|
||||
def to_gerber(self):
|
||||
return 'G04{0}*'.format(self.comment)
|
||||
|
||||
class EofStmt(Statement):
|
||||
def __init__(self):
|
||||
Statement.__init__(self, "EOF")
|
||||
|
||||
def to_gerber(self):
|
||||
return 'M02*'
|
||||
|
||||
class UnknownStmt(Statement):
|
||||
def __init__(self, line):
|
||||
Statement.__init__(self, "UNKNOWN")
|
||||
self.line = line
|
||||
|
||||
|
||||
class GerberParser(object):
|
||||
NUMBER = r"[\+-]?\d+"
|
||||
DECIMAL = r"[\+-]?\d+([.]?\d+)?"
|
||||
STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+"
|
||||
NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+"
|
||||
FUNCTION = r"G\d{2}"
|
||||
|
||||
COORD_OP = r"D[0]?[123]"
|
||||
|
||||
FS = r"(?P<param>FS)(?P<zero>(L|T))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])"
|
||||
MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
|
||||
IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
|
||||
LP = r"(?P<param>LP)(?P<lp>(D|C))"
|
||||
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,](?P<modifiers>[^,]*)"
|
||||
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,]?(?P<modifiers>[^,]+)?"
|
||||
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,]*)"
|
||||
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,]*)"
|
||||
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,]+)?".format(name=NAME)
|
||||
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>.*)".format(name=NAME)
|
||||
|
||||
# begin deprecated
|
||||
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
|
||||
IN = r"(?P<param>IN)(?P<name>.*)"
|
||||
LN = r"(?P<param>LN)(?P<name>.*)"
|
||||
# end deprecated
|
||||
|
||||
PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN)
|
||||
PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS]
|
||||
|
||||
COORD_STMT = re.compile((
|
||||
r"(?P<function>{function})?"
|
||||
r"(X(?P<x>{number}))?(Y(?P<y>{number}))?"
|
||||
r"(I(?P<i>{number}))?(J(?P<j>{number}))?"
|
||||
r"(?P<op>{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP)))
|
||||
|
||||
APERTURE_STMT = re.compile(r"(G54)?D(?P<d>\d+)\*")
|
||||
|
||||
#COMMENT_STMT = re.compile(r"G04(?P<comment>{string})(\*)?".format(string=STRING))
|
||||
#spec is unclear on whether all chars allowed in comment string -
|
||||
#seems reasonable to be more permissive.
|
||||
COMMENT_STMT = re.compile(r"G04(?P<comment>[^*]*)(\*)?")
|
||||
|
||||
EOF_STMT = re.compile(r"(?P<eof>M02)\*")
|
||||
|
||||
def __init__(self, ctx=None):
|
||||
self.statements = []
|
||||
self.ctx = ctx
|
||||
|
||||
def parse(self, filename):
|
||||
fp = open(filename, "r")
|
||||
data = fp.readlines()
|
||||
|
||||
for stmt in self._parse(data):
|
||||
self.statements.append(stmt)
|
||||
if self.ctx:
|
||||
self.ctx.evaluate(stmt)
|
||||
|
||||
def dump_json(self):
|
||||
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
|
||||
return json.dumps(stmts)
|
||||
|
||||
def dump_str(self):
|
||||
s = ""
|
||||
for stmt in self.statements:
|
||||
s += str(stmt) + "\n"
|
||||
return s
|
||||
|
||||
def dump(self):
|
||||
self.ctx.dump()
|
||||
|
||||
def _parse(self, data):
|
||||
oldline = ''
|
||||
|
||||
for i, line in enumerate(data):
|
||||
line = oldline + line.strip()
|
||||
|
||||
# skip empty lines
|
||||
if not len(line):
|
||||
continue
|
||||
|
||||
# deal with multi-line parameters
|
||||
if line.startswith("%") and not line.endswith("%"):
|
||||
oldline = line
|
||||
continue
|
||||
|
||||
did_something = True # make sure we do at least one loop
|
||||
while did_something and len(line) > 0:
|
||||
did_something = False
|
||||
# coord
|
||||
(coord, r) = self._match_one(self.COORD_STMT, line)
|
||||
if coord:
|
||||
yield CoordStmt(**coord)
|
||||
line = r
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# aperture selection
|
||||
(aperture, r) = self._match_one(self.APERTURE_STMT, line)
|
||||
if aperture:
|
||||
yield ApertureStmt(**aperture)
|
||||
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# comment
|
||||
(comment, r) = self._match_one(self.COMMENT_STMT, line)
|
||||
if comment:
|
||||
yield CommentStmt(comment["comment"])
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# parameter
|
||||
(param, r) = self._match_one_from_many(self.PARAM_STMT, line)
|
||||
if param:
|
||||
if param["param"] == "FS":
|
||||
yield FSParamStmt(**param)
|
||||
elif param["param"] == "MO":
|
||||
yield MOParamStmt(**param)
|
||||
elif param["param"] == "IP":
|
||||
yield IPParamStmt(**param)
|
||||
elif param["param"] == "LP":
|
||||
yield LPParamStmt(**param)
|
||||
elif param["param"] == "AD":
|
||||
yield ADParamStmt(**param)
|
||||
elif param["param"] == "AM":
|
||||
yield AMParamStmt(**param)
|
||||
elif param["param"] == "OF":
|
||||
yield OFParamStmt(**param)
|
||||
elif param["param"] == "IN":
|
||||
yield INParamStmt(**param)
|
||||
elif param["param"] == "LN":
|
||||
yield LNParamStmt(**param)
|
||||
else:
|
||||
yield UnknownStmt(line)
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# eof
|
||||
(eof, r) = self._match_one(self.EOF_STMT, line)
|
||||
if eof:
|
||||
yield EofStmt()
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
if False:
|
||||
print self.COORD_STMT.pattern
|
||||
print self.APERTURE_STMT.pattern
|
||||
print self.COMMENT_STMT.pattern
|
||||
print self.EOF_STMT.pattern
|
||||
for i in self.PARAM_STMT:
|
||||
print i.pattern
|
||||
|
||||
if line.find('*') > 0:
|
||||
yield UnknownStmt(line)
|
||||
did_something = True
|
||||
line = ""
|
||||
continue
|
||||
|
||||
oldline = line
|
||||
|
||||
def _match_one(self, expr, data):
|
||||
match = expr.match(data)
|
||||
if match is None:
|
||||
return ({}, None)
|
||||
else:
|
||||
return (match.groupdict(), data[match.end(0):])
|
||||
|
||||
def _match_one_from_many(self, exprs, data):
|
||||
for expr in exprs:
|
||||
match = expr.match(data)
|
||||
if match:
|
||||
return (match.groupdict(), data[match.end(0):])
|
||||
|
||||
return ({}, None)
|
||||
228
gerber/primitives.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#! /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 math
|
||||
from operator import sub
|
||||
|
||||
|
||||
class Primitive(object):
|
||||
|
||||
def __init__(self, level_polarity='dark'):
|
||||
self.level_polarity = level_polarity
|
||||
|
||||
def bounding_box(self):
|
||||
""" Calculate bounding box
|
||||
|
||||
will be helpful for sweep & prune during DRC clearance checks.
|
||||
|
||||
Return ((min x, max x), (min y, max y))
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Line(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, start, end, width, level_polarity='dark'):
|
||||
super(Line, self).__init__(level_polarity)
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.width = width
|
||||
|
||||
@property
|
||||
def angle(self):
|
||||
delta_x, delta_y = tuple(map(sub, end, start))
|
||||
angle = degrees(math.tan(delta_y/delta_x))
|
||||
return angle
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
width_2 = self.width / 2.
|
||||
min_x = min(self.start[0], self.end[0]) - width_2
|
||||
max_x = max(self.start[0], self.end[0]) + width_2
|
||||
min_y = min(self.start[1], self.end[1]) - width_2
|
||||
max_y = max(self.start[1], self.end[1]) + width_2
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
|
||||
class Arc(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, start, end, center, direction, width, level_polarity='dark'):
|
||||
super(Arc, self).__init__(level_polarity)
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.center = center
|
||||
self.direction = direction
|
||||
self.width = width
|
||||
|
||||
@property
|
||||
def start_angle(self):
|
||||
dy, dx = map(sub, self.start, self.center)
|
||||
return math.atan2(dy, dx)
|
||||
|
||||
@property
|
||||
def end_angle(self):
|
||||
dy, dx = map(sub, self.end, self.center)
|
||||
return math.atan2(dy, dx)
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
pass
|
||||
|
||||
class Circle(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, position, diameter, level_polarity='dark'):
|
||||
super(Circle, self).__init__(level_polarity)
|
||||
self.position = position
|
||||
self.diameter = diameter
|
||||
|
||||
@property
|
||||
def radius(self):
|
||||
return self.diameter / 2.
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = self.position[0] - self.radius
|
||||
max_x = self.position[0] + self.radius
|
||||
min_y = self.position[1] - self.radius
|
||||
max_y = self.position[1] + self.radius
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
@property
|
||||
def stroke_width(self):
|
||||
return self.diameter
|
||||
|
||||
|
||||
class Rectangle(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, position, width, height, level_polarity='dark'):
|
||||
super(Rectangle, self).__init__(level_polarity)
|
||||
self.position = position
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
@property
|
||||
def lower_left(self):
|
||||
return (self.position[0] - (self.width / 2.),
|
||||
self.position[1] - (self.height / 2.))
|
||||
|
||||
@property
|
||||
def upper_right(self):
|
||||
return (self.position[0] + (self.width / 2.),
|
||||
self.position[1] + (self.height / 2.))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = self.lower_left[0]
|
||||
max_x = self.upper_right[0]
|
||||
min_y = self.lower_left[1]
|
||||
max_y = self.upper_right[1]
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
@property
|
||||
def stroke_width(self):
|
||||
return max((self.width, self.height))
|
||||
|
||||
|
||||
class Obround(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, position, width, height, level_polarity='dark'):
|
||||
super(Obround, self).__init__(level_polarity)
|
||||
self.position = position
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
@property
|
||||
def orientation(self):
|
||||
return 'vertical' if self.height > self.width else 'horizontal'
|
||||
|
||||
@property
|
||||
def lower_left(self):
|
||||
return (self.position[0] - (self.width / 2.),
|
||||
self.position[1] - (self.height / 2.))
|
||||
|
||||
@property
|
||||
def upper_right(self):
|
||||
return (self.position[0] + (self.width / 2.),
|
||||
self.position[1] + (self.height / 2.))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = self.lower_left[0]
|
||||
max_x = self.upper_right[0]
|
||||
min_y = self.lower_left[1]
|
||||
max_y = self.upper_right[1]
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
|
||||
class Polygon(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, position, sides, radius, level_polarity='dark'):
|
||||
super(Polygon, self).__init__(level_polarity)
|
||||
self.position = position
|
||||
self.sides = sides
|
||||
self.radius = radius
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = self.position[0] - self.radius
|
||||
max_x = self.position[0] + self.radius
|
||||
min_y = self.position[1] - self.radius
|
||||
max_y = self.position[1] + self.radius
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
|
||||
class Region(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, points, level_polarity='dark'):
|
||||
super(Region, self).__init__(level_polarity)
|
||||
self.points = points
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
x_list, y_list = zip(*self.points)
|
||||
min_x = min(x_list)
|
||||
max_x = max(x_list)
|
||||
min_y = min(y_list)
|
||||
max_y = max(y_list)
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
|
||||
class Drill(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, position, diameter):
|
||||
super(Drill, self).__init__('dark')
|
||||
self.position = position
|
||||
self.diameter = diameter
|
||||
|
||||
@property
|
||||
def radius(self):
|
||||
return self.diameter / 2.
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = self.position[0] - self.radius
|
||||
max_x = self.position[0] + self.radius
|
||||
min_y = self.position[1] - self.radius
|
||||
max_y = self.position[1] + self.radius
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
188
gerber/render.py
|
|
@ -1,188 +0,0 @@
|
|||
#! /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 .parser import CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt
|
||||
|
||||
IMAGE_POLARITY_POSITIVE = 1
|
||||
IMAGE_POLARITY_NEGATIVE = 2
|
||||
|
||||
LEVEL_POLARITY_DARK = 1
|
||||
LEVEL_POLARITY_CLEAR = 2
|
||||
|
||||
NOTATION_ABSOLUTE = 1
|
||||
NOTATION_INCREMENTAL = 2
|
||||
|
||||
UNIT_INCH = 1
|
||||
UNIT_MM = 2
|
||||
|
||||
INTERPOLATION_LINEAR = 1
|
||||
INTERPOLATION_ARC = 2
|
||||
|
||||
|
||||
class GerberCoordFormat(object):
|
||||
def __init__(self, zeroes, x, y):
|
||||
self.omit_leading_zeroes = True if zeroes == "L" else False
|
||||
self.omit_trailing_zeroes = True if zeroes == "T" else False
|
||||
self.x_int_digits, self.x_dec_digits = [int(d) for d in x]
|
||||
self.y_int_digits, self.y_dec_digits = [int(d) for d in y]
|
||||
|
||||
def resolve(self, x, y):
|
||||
new_x = x.replace("+", "") if x else None
|
||||
new_y = y.replace("+", "") if y else None
|
||||
|
||||
if new_x is not None:
|
||||
negative = "-" in new_x
|
||||
new_x = new_x.replace("-", "")
|
||||
|
||||
missing_zeroes = (self.x_int_digits + self.x_dec_digits) - len(new_x)
|
||||
|
||||
if missing_zeroes and self.omit_leading_zeroes:
|
||||
new_x = (missing_zeroes * "0") + new_x
|
||||
elif missing_zeroes and self.omit_trailing_zeroes:
|
||||
new_x += missing_zeroes * "0"
|
||||
|
||||
new_x = float("{0}{1}.{2}".format("-" if negative else "",
|
||||
new_x[:self.x_int_digits],
|
||||
new_x[self.x_int_digits:]))
|
||||
|
||||
if new_y is not None:
|
||||
negative = "-" in new_y
|
||||
new_y = new_y.replace("-", "")
|
||||
|
||||
missing_zeroes = (self.y_int_digits + self.y_dec_digits) - len(new_y)
|
||||
|
||||
if missing_zeroes and self.omit_leading_zeroes:
|
||||
new_y = (missing_zeroes * "0") + new_y
|
||||
elif missing_zeroes and self.omit_trailing_zeroes:
|
||||
new_y += missing_zeroes * "0"
|
||||
|
||||
new_y = float("{0}{1}.{2}".format("-" if negative else "",
|
||||
new_y[:self.y_int_digits],
|
||||
new_y[self.y_int_digits:]))
|
||||
|
||||
return new_x, new_y
|
||||
|
||||
|
||||
class GerberContext(object):
|
||||
coord_format = None
|
||||
coord_notation = NOTATION_ABSOLUTE
|
||||
coord_unit = None
|
||||
|
||||
x = 0
|
||||
y = 0
|
||||
|
||||
aperture = 0
|
||||
interpolation = INTERPOLATION_LINEAR
|
||||
|
||||
image_polarity = IMAGE_POLARITY_POSITIVE
|
||||
level_polarity = LEVEL_POLARITY_DARK
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def set_coord_format(self, zeroes, x, y):
|
||||
self.coord_format = GerberCoordFormat(zeroes, x, y)
|
||||
|
||||
def set_coord_notation(self, notation):
|
||||
self.coord_notation = NOTATION_ABSOLUTE if notation == "A" else NOTATION_INCREMENTAL
|
||||
|
||||
def set_coord_unit(self, unit):
|
||||
self.coord_unit = UNIT_INCH if unit == "IN" else UNIT_MM
|
||||
|
||||
def set_image_polarity(self, polarity):
|
||||
self.image_polarity = IMAGE_POLARITY_POSITIVE if polarity == "POS" else IMAGE_POLARITY_NEGATIVE
|
||||
|
||||
def set_level_polarity(self, polarity):
|
||||
self.level_polarity = LEVEL_POLARITY_DARK if polarity == "D" else LEVEL_POLARITY_CLEAR
|
||||
|
||||
def set_interpolation(self, interpolation):
|
||||
self.interpolation = INTERPOLATION_LINEAR if interpolation in ("G01", "G1") else INTERPOLATION_ARC
|
||||
|
||||
def set_aperture(self, d):
|
||||
self.aperture = d
|
||||
|
||||
def resolve(self, x, y):
|
||||
x, y = self.coord_format.resolve(x, y)
|
||||
return x if x is not None else self.x, y if y is not None else self.y
|
||||
|
||||
def define_aperture(self, d, shape, modifiers):
|
||||
pass
|
||||
|
||||
def move(self, x, y, resolve=True):
|
||||
if resolve:
|
||||
self.x, self.y = self.resolve(x, y)
|
||||
else:
|
||||
self.x, self.y = x, y
|
||||
|
||||
def stroke(self, x, y):
|
||||
pass
|
||||
|
||||
def line(self, x, y):
|
||||
pass
|
||||
|
||||
def arc(self, x, y):
|
||||
pass
|
||||
|
||||
def flash(self, x, y):
|
||||
pass
|
||||
|
||||
def drill(self, x, y, diameter):
|
||||
pass
|
||||
|
||||
def evaluate(self, stmt):
|
||||
if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)):
|
||||
return
|
||||
|
||||
elif isinstance(stmt, ParamStmt):
|
||||
self._evaluate_param(stmt)
|
||||
|
||||
elif isinstance(stmt, CoordStmt):
|
||||
self._evaluate_coord(stmt)
|
||||
|
||||
elif isinstance(stmt, ApertureStmt):
|
||||
self._evaluate_aperture(stmt)
|
||||
|
||||
else:
|
||||
raise Exception("Invalid statement to evaluate")
|
||||
|
||||
def _evaluate_param(self, stmt):
|
||||
if stmt.param == "FS":
|
||||
self.set_coord_format(stmt.zero, stmt.x, stmt.y)
|
||||
self.set_coord_notation(stmt.notation)
|
||||
elif stmt.param == "MO:":
|
||||
self.set_coord_unit(stmt.mo)
|
||||
elif stmt.param == "IP:":
|
||||
self.set_image_polarity(stmt.ip)
|
||||
elif stmt.param == "LP:":
|
||||
self.set_level_polarity(stmt.lp)
|
||||
elif stmt.param == "AD":
|
||||
self.define_aperture(stmt.d, stmt.shape, stmt.modifiers)
|
||||
|
||||
def _evaluate_coord(self, stmt):
|
||||
|
||||
if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"):
|
||||
self.set_interpolation(stmt.function)
|
||||
|
||||
if stmt.op == "D01":
|
||||
self.stroke(stmt.x, stmt.y)
|
||||
elif stmt.op == "D02":
|
||||
self.move(stmt.x, stmt.y)
|
||||
elif stmt.op == "D03":
|
||||
self.flash(stmt.x, stmt.y)
|
||||
|
||||
def _evaluate_aperture(self, stmt):
|
||||
self.set_aperture(stmt.d)
|
||||
28
gerber/render/__init__.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#! /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 svgwrite_backend import GerberSvgContext
|
||||
|
||||
76
gerber/render/apertures.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
gerber.render.apertures
|
||||
============
|
||||
**Gerber Aperture base classes**
|
||||
|
||||
This module provides base classes for gerber apertures. These are used by
|
||||
the rendering engine to draw the gerber file.
|
||||
"""
|
||||
import math
|
||||
|
||||
class Aperture(object):
|
||||
""" Gerber Aperture base class
|
||||
"""
|
||||
def draw(self, ctx, x, y):
|
||||
raise NotImplementedError('The draw method must be implemented \
|
||||
in an Aperture subclass.')
|
||||
|
||||
def flash(self, ctx, x, y):
|
||||
raise NotImplementedError('The flash method must be implemented \
|
||||
in an Aperture subclass.')
|
||||
|
||||
def _arc_params(self, startx, starty, x, y, i, j):
|
||||
center = (startx + i, starty + j)
|
||||
radius = math.sqrt(math.pow(center[0] - x, 2) +
|
||||
math.pow(center[1] - y, 2))
|
||||
delta_x0 = startx - center[0]
|
||||
delta_y0 = center[1] - starty
|
||||
delta_x1 = x - center[0]
|
||||
delta_y1 = center[1] - y
|
||||
start_angle = math.atan2(delta_y0, delta_x0)
|
||||
end_angle = math.atan2(delta_y1, delta_x1)
|
||||
return {'center': center, 'radius': radius,
|
||||
'start_angle': start_angle, 'end_angle': end_angle}
|
||||
|
||||
|
||||
class Circle(Aperture):
|
||||
""" Circular Aperture base class
|
||||
"""
|
||||
def __init__(self, diameter=0.0):
|
||||
self.diameter = diameter
|
||||
|
||||
|
||||
class Rect(Aperture):
|
||||
""" Rectangular Aperture base class
|
||||
"""
|
||||
def __init__(self, size=(0, 0)):
|
||||
self.size = size
|
||||
|
||||
|
||||
class Obround(Aperture):
|
||||
""" Obround Aperture base class
|
||||
"""
|
||||
def __init__(self, size=(0, 0)):
|
||||
self.size = size
|
||||
|
||||
|
||||
class Polygon(Aperture):
|
||||
""" Polygon Aperture base class
|
||||
"""
|
||||
pass
|
||||
174
gerber/render/render.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#! /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 ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt,
|
||||
CoordStmt, ApertureStmt, RegionModeStmt,
|
||||
QuadrantModeStmt,
|
||||
)
|
||||
|
||||
from ..primitives import *
|
||||
|
||||
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
|
||||
|
||||
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.drill_color = (0.25, 0.25, 0.25)
|
||||
self.background_color = (0.0, 0.0, 0.0)
|
||||
self.alpha = 1.0
|
||||
|
||||
def set_units(self, units):
|
||||
""" Set context measurement units
|
||||
|
||||
Parameters
|
||||
----------
|
||||
unit : string
|
||||
Measurement units. may be 'inch' or 'metric'
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If `unit` is not 'inch' or 'metric'
|
||||
"""
|
||||
if units not in ('inch', 'metric'):
|
||||
raise ValueError('Units may be "inch" or "metric"')
|
||||
self.units = units
|
||||
|
||||
def set_color(self, color):
|
||||
""" Set rendering color.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color as a tuple of (red, green, blue) values. Each channel is
|
||||
represented as a float value in (0, 1)
|
||||
"""
|
||||
self.color = color
|
||||
|
||||
def set_drill_color(self, color):
|
||||
""" Set color used for rendering drill hits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color as a tuple of (red, green, blue) values. Each channel is
|
||||
represented as a float value in (0, 1)
|
||||
"""
|
||||
self.drill_color = color
|
||||
|
||||
def set_background_color(self, color):
|
||||
""" Set rendering background color
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color as a tuple of (red, green, blue) values. Each channel is
|
||||
represented as a float value in (0, 1)
|
||||
"""
|
||||
self.background_color = color
|
||||
|
||||
def set_alpha(self, alpha):
|
||||
""" Set layer rendering opacity
|
||||
|
||||
.. note::
|
||||
Not all backends/rendering devices support this parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha : float
|
||||
Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque)
|
||||
"""
|
||||
self.alpha = alpha
|
||||
|
||||
def render(self, primitive):
|
||||
color = (self.color if primitive.level_polarity == 'dark'
|
||||
else self.background_color)
|
||||
if isinstance(primitive, Line):
|
||||
self._render_line(primitive, color)
|
||||
elif isinstance(primitive, Arc):
|
||||
self._render_arc(primitive, color)
|
||||
elif isinstance(primitive, Region):
|
||||
self._render_region(primitive, color)
|
||||
elif isinstance(primitive, Circle):
|
||||
self._render_circle(primitive, color)
|
||||
elif isinstance(primitive, Rectangle):
|
||||
self._render_rectangle(primitive, color)
|
||||
elif isinstance(primitive, Obround):
|
||||
self._render_obround(primitive, color)
|
||||
elif isinstance(primitive, Polygon):
|
||||
self._render_polygon(Polygon, color)
|
||||
elif isinstance(primitive, Drill):
|
||||
self._render_drill(primitive, self.drill_color)
|
||||
else:
|
||||
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
|
||||
|
||||
155
gerber/render/svgwrite_backend.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Based on render_svg.py by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .render import GerberContext
|
||||
from operator import mul
|
||||
import svgwrite
|
||||
|
||||
SCALE = 300
|
||||
|
||||
|
||||
def svg_color(color):
|
||||
color = tuple([int(ch * 255) for ch in color])
|
||||
return 'rgb(%d, %d, %d)' % color
|
||||
|
||||
|
||||
class GerberSvgContext(GerberContext):
|
||||
def __init__(self):
|
||||
GerberContext.__init__(self)
|
||||
self.scale = (SCALE, -SCALE)
|
||||
self.dwg = svgwrite.Drawing()
|
||||
self.background = False
|
||||
|
||||
def dump(self, filename):
|
||||
self.dwg.saveas(filename)
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
xbounds, ybounds = bounds
|
||||
size = (SCALE * (xbounds[1] - xbounds[0]),
|
||||
SCALE * (ybounds[1] - ybounds[0]))
|
||||
if not self.background:
|
||||
vbox = '%f, %f, %f, %f' % (SCALE * xbounds[0], -SCALE * ybounds[1],
|
||||
size[0], size[1])
|
||||
self.dwg = svgwrite.Drawing(viewBox=vbox)
|
||||
rect = self.dwg.rect(insert=(SCALE * xbounds[0],
|
||||
-SCALE * ybounds[1]),
|
||||
size=size,
|
||||
fill=svg_color(self.background_color))
|
||||
self.dwg.add(rect)
|
||||
self.background = True
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = map(mul, line.start, self.scale)
|
||||
end = map(mul, line.end, self.scale)
|
||||
aline = self.dwg.line(start=start, end=end,
|
||||
stroke=svg_color(color),
|
||||
stroke_width=SCALE * line.width,
|
||||
stroke_linecap='round')
|
||||
aline.stroke(opacity=self.alpha)
|
||||
self.dwg.add(aline)
|
||||
|
||||
def _render_region(self, region, color):
|
||||
points = [tuple(map(mul, point, self.scale)) for point in region.points]
|
||||
region_path = self.dwg.path(d='M %f, %f' % points[0],
|
||||
fill=svg_color(color),
|
||||
stroke='none')
|
||||
region_path.fill(opacity=self.alpha)
|
||||
for point in points[1:]:
|
||||
region_path.push('L %f, %f' % point)
|
||||
self.dwg.add(region_path)
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
center = map(mul, circle.position, self.scale)
|
||||
acircle = self.dwg.circle(center=center,
|
||||
r = SCALE * circle.radius,
|
||||
fill=svg_color(color))
|
||||
acircle.fill(opacity=self.alpha)
|
||||
self.dwg.add(acircle)
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
center = map(mul, rectangle.position, self.scale)
|
||||
size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale)))
|
||||
insert = center[0] - size[0] / 2., center[1] - size[1] / 2.
|
||||
arect = self.dwg.rect(insert=insert, size=size,
|
||||
fill=svg_color(color))
|
||||
arect.fill(opacity=self.alpha)
|
||||
self.dwg.add(arect)
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
x, y = tuple(map(mul, obround.position, self.scale))
|
||||
xsize, ysize = tuple(map(mul, (obround.width, obround.height),
|
||||
self.scale))
|
||||
xscale, yscale = self.scale
|
||||
|
||||
# Corner case...
|
||||
if xsize == ysize:
|
||||
circle = self.dwg.circle(center=(x, y),
|
||||
r = (xsize / 2.0),
|
||||
fill=svg_color(color))
|
||||
circle.fill(opacity=self.alpha)
|
||||
self.dwg.add(circle)
|
||||
|
||||
# Horizontal obround
|
||||
elif xsize > ysize:
|
||||
rectx = xsize - ysize
|
||||
recty = ysize
|
||||
c1 = self.dwg.circle(center=(x - (rectx / 2.0), y),
|
||||
r = (ysize / 2.0),
|
||||
fill=svg_color(color))
|
||||
|
||||
c2 = self.dwg.circle(center=(x + (rectx / 2.0), y),
|
||||
r = (ysize / 2.0),
|
||||
fill=svg_color(color))
|
||||
|
||||
rect = self.dwg.rect(insert=(x, y),
|
||||
size=(xsize, ysize),
|
||||
fill=svg_color(color))
|
||||
c1.fill(opacity=self.alpha)
|
||||
c2.fill(opacity=self.alpha)
|
||||
rect.fill(opacity=self.alpha)
|
||||
self.dwg.add(c1)
|
||||
self.dwg.add(c2)
|
||||
self.dwg.add(rect)
|
||||
|
||||
# Vertical obround
|
||||
else:
|
||||
rectx = xsize
|
||||
recty = ysize - xsize
|
||||
c1 = self.dwg.circle(center=(x, y - (recty / 2.)),
|
||||
r = (xsize / 2.),
|
||||
fill=svg_color(color))
|
||||
|
||||
c2 = self.dwg.circle(center=(x, y + (recty / 2.)),
|
||||
r = (xsize / 2.),
|
||||
fill=svg_color(color))
|
||||
|
||||
rect = self.dwg.rect(insert=(x, y),
|
||||
size=(xsize, ysize),
|
||||
fill=svg_color(color))
|
||||
c1.fill(opacity=self.alpha)
|
||||
c2.fill(opacity=self.alpha)
|
||||
rect.fill(opacity=self.alpha)
|
||||
self.dwg.add(c1)
|
||||
self.dwg.add(c2)
|
||||
self.dwg.add(rect)
|
||||
|
||||
def _render_drill(self, primitive, color):
|
||||
center = map(mul, primitive.position, self.scale)
|
||||
hit = self.dwg.circle(center=center, r=SCALE * primitive.radius,
|
||||
fill=svg_color(color))
|
||||
self.dwg.add(hit)
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
#! /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 GerberContext, INTERPOLATION_LINEAR, INTERPOLATION_ARC
|
||||
import svgwrite
|
||||
|
||||
|
||||
class Shape(object):
|
||||
pass
|
||||
|
||||
|
||||
class Circle(Shape):
|
||||
def __init__(self, diameter=0.0):
|
||||
self.diameter = diameter
|
||||
|
||||
def draw(self, ctx, x, y):
|
||||
return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)",
|
||||
stroke_width=2, stroke_linecap="round")
|
||||
|
||||
def flash(self, ctx, x, y):
|
||||
return ctx.dwg.circle(center=(x*300, y*300), r=300*(self.diameter/2.0), fill="rgb(184, 115, 51)")
|
||||
|
||||
|
||||
class Rect(Shape):
|
||||
def __init__(self, size=(0, 0)):
|
||||
self.size = size
|
||||
|
||||
def draw(self, ctx, x, y):
|
||||
return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)",
|
||||
stroke_width=2, stroke_linecap="butt")
|
||||
|
||||
def flash(self, ctx, x, y):
|
||||
# Center the rectange on x,y
|
||||
x -= (self.size[0] / 2.0)
|
||||
y -= (self.size[0] / 2.0)
|
||||
return ctx.dwg.rect(insert=(300*x, 300*y), size=(300*float(self.size[0]), 300*float(self.size[1])),
|
||||
fill="rgb(184, 115, 51)")
|
||||
|
||||
|
||||
class GerberSvgContext(GerberContext):
|
||||
def __init__(self):
|
||||
GerberContext.__init__(self)
|
||||
|
||||
self.apertures = {}
|
||||
self.dwg = svgwrite.Drawing()
|
||||
self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black"))
|
||||
|
||||
def define_aperture(self, d, shape, modifiers):
|
||||
aperture = None
|
||||
if shape == "C":
|
||||
aperture = Circle(diameter=float(modifiers[0][0]))
|
||||
elif shape == "R":
|
||||
aperture = Rect(size=modifiers[0][0:2])
|
||||
|
||||
self.apertures[d] = aperture
|
||||
|
||||
def stroke(self, x, y):
|
||||
super(GerberSvgContext, self).stroke(x, y)
|
||||
|
||||
if self.interpolation == INTERPOLATION_LINEAR:
|
||||
self.line(x, y)
|
||||
elif self.interpolation == INTERPOLATION_ARC:
|
||||
self.arc(x, y)
|
||||
|
||||
def line(self, x, y):
|
||||
super(GerberSvgContext, self).line(x, y)
|
||||
|
||||
x, y = self.resolve(x, y)
|
||||
|
||||
ap = self.apertures.get(str(self.aperture), None)
|
||||
if ap is None:
|
||||
return
|
||||
|
||||
self.dwg.add(ap.draw(self, x, y))
|
||||
|
||||
self.move(x, y, resolve=False)
|
||||
|
||||
def arc(self, x, y):
|
||||
super(GerberSvgContext, self).arc(x, y)
|
||||
|
||||
def flash(self, x, y):
|
||||
super(GerberSvgContext, self).flash(x, y)
|
||||
|
||||
x, y = self.resolve(x, y)
|
||||
|
||||
ap = self.apertures.get(str(self.aperture), None)
|
||||
if ap is None:
|
||||
return
|
||||
|
||||
self.dwg.add(ap.flash(self, x, y))
|
||||
|
||||
self.move(x, y, resolve=False)
|
||||
|
||||
def drill(self, x, y, diameter):
|
||||
hit = self.dwg.circle(center=(x*300, y*300), r=300*(diameter/2.0), fill="gray")
|
||||
self.dwg.add(hit)
|
||||
|
||||
|
||||
def dump(self,filename='teste.svg'):
|
||||
self.dwg.saveas(filename)
|
||||
434
gerber/rs274x.py
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
#! /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 re
|
||||
from .gerber_statements import *
|
||||
from .primitives import *
|
||||
from .cam import CamFile, FileSettings
|
||||
|
||||
def read(filename):
|
||||
""" Read data from filename and return a GerberFile
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of file to parse
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.rs274x.GerberFile`
|
||||
A GerberFile created from the specified file.
|
||||
"""
|
||||
return GerberParser().parse(filename)
|
||||
|
||||
|
||||
class GerberFile(CamFile):
|
||||
""" A class representing a single gerber file
|
||||
|
||||
The GerberFile class represents a single gerber file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
statements : list
|
||||
list of gerber file statements
|
||||
|
||||
settings : dict
|
||||
Dictionary of gerber file settings
|
||||
|
||||
filename : string
|
||||
Filename of the source gerber file
|
||||
|
||||
Attributes
|
||||
----------
|
||||
comments: list of strings
|
||||
List of comments contained in the gerber file.
|
||||
|
||||
size : tuple, (<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, filename=None):
|
||||
super(GerberFile, self).__init__(statements, settings, primitives, filename)
|
||||
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return [comment.comment for comment in self.statements
|
||||
if isinstance(comment, CommentStmt)]
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
xbounds, ybounds = self.bounds
|
||||
return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0])
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
xbounds = [0.0, 0.0]
|
||||
ybounds = [0.0, 0.0]
|
||||
for stmt in [stmt for stmt in self.statements
|
||||
if isinstance(stmt, CoordStmt)]:
|
||||
if stmt.x is not None:
|
||||
if stmt.x < xbounds[0]:
|
||||
xbounds[0] = stmt.x
|
||||
elif stmt.x > xbounds[1]:
|
||||
xbounds[1] = stmt.x
|
||||
if stmt.y is not None:
|
||||
if stmt.y < ybounds[0]:
|
||||
ybounds[0] = stmt.y
|
||||
elif stmt.y > ybounds[1]:
|
||||
ybounds[1] = stmt.y
|
||||
return (xbounds, ybounds)
|
||||
|
||||
|
||||
def write(self, filename):
|
||||
""" Write data out to a gerber file
|
||||
"""
|
||||
with open(filename, 'w') as f:
|
||||
for statement in self.statements:
|
||||
f.write(statement.to_gerber())
|
||||
|
||||
|
||||
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]+"
|
||||
FUNCTION = r"G\d{2}"
|
||||
|
||||
COORD_OP = r"D[0]?[123]"
|
||||
|
||||
FS = r"(?P<param>FS)(?P<zero>(L|T))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])"
|
||||
MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
|
||||
IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
|
||||
LP = r"(?P<param>LP)(?P<lp>(D|C))"
|
||||
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,](?P<modifiers>[^,]*)"
|
||||
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,]?(?P<modifiers>[^,]+)?"
|
||||
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,]*)"
|
||||
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,]*)"
|
||||
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,]+)?".format(name=NAME)
|
||||
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>.*)".format(name=NAME)
|
||||
|
||||
# begin deprecated
|
||||
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
|
||||
IN = r"(?P<param>IN)(?P<name>.*)"
|
||||
LN = r"(?P<param>LN)(?P<name>.*)"
|
||||
# end deprecated
|
||||
|
||||
PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN)
|
||||
PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS]
|
||||
|
||||
COORD_STMT = re.compile((
|
||||
r"(?P<function>{function})?"
|
||||
r"(X(?P<x>{number}))?(Y(?P<y>{number}))?"
|
||||
r"(I(?P<i>{number}))?(J(?P<j>{number}))?"
|
||||
r"(?P<op>{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP)))
|
||||
|
||||
APERTURE_STMT = re.compile(r"(G54)?D(?P<d>\d+)\*")
|
||||
|
||||
COMMENT_STMT = re.compile(r"G04(?P<comment>[^*]*)(\*)?")
|
||||
|
||||
EOF_STMT = re.compile(r"(?P<eof>M02)\*")
|
||||
|
||||
REGION_MODE_STMT = re.compile(r'(?P<mode>G3[67])\*')
|
||||
QUAD_MODE_STMT = re.compile(r'(?P<mode>G7[45])\*')
|
||||
|
||||
def __init__(self):
|
||||
self.settings = FileSettings()
|
||||
self.statements = []
|
||||
self.primitives = []
|
||||
self.apertures = {}
|
||||
self.current_region = None
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
|
||||
self.aperture = 0
|
||||
self.interpolation = 'linear'
|
||||
self.direction = 'clockwise'
|
||||
self.image_polarity = 'positive'
|
||||
self.level_polarity = 'dark'
|
||||
self.region_mode = 'off'
|
||||
self.quadrant_mode = 'multi-quadrant'
|
||||
self.step_and_repeat = (1, 1, 0, 0)
|
||||
|
||||
|
||||
def parse(self, filename):
|
||||
fp = open(filename, "r")
|
||||
data = fp.readlines()
|
||||
|
||||
for stmt in self._parse(data):
|
||||
self.evaluate(stmt)
|
||||
self.statements.append(stmt)
|
||||
|
||||
return GerberFile(self.statements, self.settings, self.primitives, filename)
|
||||
|
||||
def dump_json(self):
|
||||
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
|
||||
return json.dumps(stmts)
|
||||
|
||||
def dump_str(self):
|
||||
s = ""
|
||||
for stmt in self.statements:
|
||||
s += str(stmt) + "\n"
|
||||
return s
|
||||
|
||||
def _parse(self, data):
|
||||
oldline = ''
|
||||
|
||||
for i, line in enumerate(data):
|
||||
line = oldline + line.strip()
|
||||
|
||||
# skip empty lines
|
||||
if not len(line):
|
||||
continue
|
||||
|
||||
# deal with multi-line parameters
|
||||
if line.startswith("%") and not line.endswith("%"):
|
||||
oldline = line
|
||||
continue
|
||||
|
||||
did_something = True # make sure we do at least one loop
|
||||
while did_something and len(line) > 0:
|
||||
did_something = False
|
||||
|
||||
# Region Mode
|
||||
(mode, r) = _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
|
||||
|
||||
# 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
|
||||
|
||||
# comment
|
||||
(comment, r) = _match_one(self.COMMENT_STMT, line)
|
||||
if comment:
|
||||
yield CommentStmt(comment["comment"])
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# parameter
|
||||
(param, r) = _match_one_from_many(self.PARAM_STMT, line)
|
||||
if param:
|
||||
if param["param"] == "FS":
|
||||
stmt = FSParamStmt.from_dict(param)
|
||||
self.settings.zero_suppression = stmt.zero_suppression
|
||||
self.settings.format = stmt.format
|
||||
self.settings.notation = stmt.notation
|
||||
yield stmt
|
||||
elif param["param"] == "MO":
|
||||
stmt = MOParamStmt.from_dict(param)
|
||||
self.settings.units = stmt.mode
|
||||
yield stmt
|
||||
elif param["param"] == "IP":
|
||||
yield IPParamStmt.from_dict(param)
|
||||
elif param["param"] == "LP":
|
||||
yield LPParamStmt.from_dict(param)
|
||||
elif param["param"] == "AD":
|
||||
yield ADParamStmt.from_dict(param)
|
||||
elif param["param"] == "AM":
|
||||
yield AMParamStmt.from_dict(param)
|
||||
elif param["param"] == "OF":
|
||||
yield OFParamStmt.from_dict(param)
|
||||
elif param["param"] == "IN":
|
||||
yield INParamStmt.from_dict(param)
|
||||
elif param["param"] == "LN":
|
||||
yield LNParamStmt.from_dict(param)
|
||||
else:
|
||||
yield UnknownStmt(line)
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# eof
|
||||
(eof, r) = _match_one(self.EOF_STMT, line)
|
||||
if eof:
|
||||
yield EofStmt()
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
if False:
|
||||
print self.COORD_STMT.pattern
|
||||
print self.APERTURE_STMT.pattern
|
||||
print self.COMMENT_STMT.pattern
|
||||
print self.EOF_STMT.pattern
|
||||
for i in self.PARAM_STMT:
|
||||
print i.pattern
|
||||
|
||||
if line.find('*') > 0:
|
||||
yield UnknownStmt(line)
|
||||
oldline = line
|
||||
|
||||
def 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, EofStmt)):
|
||||
return
|
||||
|
||||
else:
|
||||
raise Exception("Invalid statement to evaluate")
|
||||
|
||||
|
||||
def _define_aperture(self, d, shape, modifiers):
|
||||
aperture = None
|
||||
if shape == 'C':
|
||||
diameter = float(modifiers[0][0])
|
||||
aperture = Circle(position=None, diameter=diameter)
|
||||
elif shape == 'R':
|
||||
width = float(modifiers[0][0])
|
||||
height = float(modifiers[0][1])
|
||||
aperture = Rectangle(position=None, width=width, height=height)
|
||||
elif shape == 'O':
|
||||
width = float(modifiers[0][0])
|
||||
height = float(modifiers[0][1])
|
||||
aperture = Obround(position=None, width=width, height=height)
|
||||
self.apertures[d] = aperture
|
||||
|
||||
def _evaluate_mode(self, stmt):
|
||||
if stmt.type == 'RegionMode':
|
||||
if self.region_mode == 'on' and stmt.mode == 'off':
|
||||
self.primitives.append(Region(self.current_region, self.level_polarity))
|
||||
self.current_region = None
|
||||
self.region_mode = stmt.mode
|
||||
elif stmt.type == 'QuadrantMode':
|
||||
self.quadrant_mode = stmt.mode
|
||||
|
||||
def _evaluate_param(self, stmt):
|
||||
if stmt.param == "FS":
|
||||
self.settings.zero_suppression = stmt.zero_suppression
|
||||
self.settings.format = stmt.format
|
||||
self.settings.notation = stmt.notation
|
||||
elif stmt.param == "MO":
|
||||
self.settings.units = stmt.mode
|
||||
elif stmt.param == "IP":
|
||||
self.image_polarity = stmt.ip
|
||||
elif stmt.param == "LP":
|
||||
self.level_polarity = stmt.lp
|
||||
elif stmt.param == "AD":
|
||||
self._define_aperture(stmt.d, stmt.shape, stmt.modifiers)
|
||||
|
||||
def _evaluate_coord(self, stmt):
|
||||
x = self.x if stmt.x is None else stmt.x
|
||||
y = self.y if stmt.y is None else stmt.y
|
||||
if stmt.function in ("G01", "G1"):
|
||||
self.interpolation = 'linear'
|
||||
elif stmt.function in ('G02', 'G2', 'G03', 'G3'):
|
||||
self.interpolation = 'arc'
|
||||
self.direction = ('clockwise' if stmt.function in ('G02', 'G2')
|
||||
else 'counterclockwise')
|
||||
if stmt.op == "D01":
|
||||
if self.region_mode == 'on':
|
||||
if self.current_region is None:
|
||||
self.current_region = [(self.x, self.y), ]
|
||||
self.current_region.append((x, y,))
|
||||
else:
|
||||
start = (self.x, self.y)
|
||||
end = (x, y)
|
||||
width = self.apertures[self.aperture].stroke_width
|
||||
if self.interpolation == 'linear':
|
||||
self.primitives.append(Line(start, end, width, self.level_polarity))
|
||||
else:
|
||||
center = (start[0] + stmt.i, start[1] + stmt.j)
|
||||
self.primitives.append(Arc(start, end, center, self.direction, width, self.level_polarity))
|
||||
|
||||
elif stmt.op == "D02":
|
||||
pass
|
||||
|
||||
elif stmt.op == "D03":
|
||||
primitive = copy.deepcopy(self.apertures[self.aperture])
|
||||
primitive.position = (x, y)
|
||||
primitive.level_polarity = self.level_polarity
|
||||
self.primitives.append(primitive)
|
||||
self.x, self.y = x, y
|
||||
|
||||
def _evaluate_aperture(self, stmt):
|
||||
self.aperture = stmt.d
|
||||
|
||||
|
||||
def _match_one(expr, data):
|
||||
match = expr.match(data)
|
||||
if match is None:
|
||||
return ({}, None)
|
||||
else:
|
||||
return (match.groupdict(), data[match.end(0):])
|
||||
|
||||
|
||||
def _match_one_from_many(exprs, data):
|
||||
for expr in exprs:
|
||||
match = expr.match(data)
|
||||
if match:
|
||||
return (match.groupdict(), data[match.end(0):])
|
||||
|
||||
return ({}, None)
|
||||
0
gerber/tests/__init__.py
Normal file
503
gerber/tests/resources/board_outline.GKO
Normal file
|
|
@ -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*
|
||||
1811
gerber/tests/resources/bottom_copper.GBL
Normal file
66
gerber/tests/resources/bottom_mask.GBS
Normal file
|
|
@ -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*
|
||||
6007
gerber/tests/resources/bottom_silk.GBO
Normal file
51
gerber/tests/resources/ncdrill.DRD
Normal file
|
|
@ -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
|
||||
3457
gerber/tests/resources/top_copper.GTL
Normal file
162
gerber/tests/resources/top_mask.GTS
Normal file
|
|
@ -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*
|
||||
2099
gerber/tests/resources/top_silk.GTO
Normal file
68
gerber/tests/test_cam.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
from ..cam import CamFile, FileSettings
|
||||
from tests import *
|
||||
|
||||
|
||||
def test_filesettings_defaults():
|
||||
""" Test FileSettings default values
|
||||
"""
|
||||
fs = FileSettings()
|
||||
assert_equal(fs.format, (2, 5))
|
||||
assert_equal(fs.notation, 'absolute')
|
||||
assert_equal(fs.zero_suppression, 'trailing')
|
||||
assert_equal(fs.units, 'inch')
|
||||
|
||||
|
||||
def test_filesettings_dict():
|
||||
""" Test FileSettings Dict
|
||||
"""
|
||||
fs = FileSettings()
|
||||
assert_equal(fs['format'], (2, 5))
|
||||
assert_equal(fs['notation'], 'absolute')
|
||||
assert_equal(fs['zero_suppression'], 'trailing')
|
||||
assert_equal(fs['units'], 'inch')
|
||||
|
||||
|
||||
def test_filesettings_assign():
|
||||
""" Test FileSettings attribute assignment
|
||||
"""
|
||||
fs = FileSettings()
|
||||
fs.units = 'test1'
|
||||
fs.notation = 'test2'
|
||||
fs.zero_suppression = 'test3'
|
||||
fs.format = 'test4'
|
||||
assert_equal(fs.units, 'test1')
|
||||
assert_equal(fs.notation, 'test2')
|
||||
assert_equal(fs.zero_suppression, 'test3')
|
||||
assert_equal(fs.format, 'test4')
|
||||
|
||||
|
||||
def test_filesettings_dict_assign():
|
||||
""" Test FileSettings dict-style attribute assignment
|
||||
"""
|
||||
fs = FileSettings()
|
||||
fs['units'] = 'metric'
|
||||
fs['notation'] = 'incremental'
|
||||
fs['zero_suppression'] = 'leading'
|
||||
fs['format'] = (1, 2)
|
||||
assert_equal(fs.units, 'metric')
|
||||
assert_equal(fs.notation, 'incremental')
|
||||
assert_equal(fs.zero_suppression, 'leading')
|
||||
assert_equal(fs.format, (1, 2))
|
||||
|
||||
def test_camfile_init():
|
||||
""" Smoke test CamFile test
|
||||
"""
|
||||
cf = CamFile()
|
||||
|
||||
def test_camfile_settings():
|
||||
""" Test CamFile Default Settings
|
||||
"""
|
||||
cf = CamFile()
|
||||
assert_equal(cf.settings, FileSettings())
|
||||
|
||||
|
||||
24
gerber/tests/test_common.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
from ..common import read
|
||||
from ..excellon import ExcellonFile
|
||||
from ..rs274x import GerberFile
|
||||
from tests import *
|
||||
|
||||
import os
|
||||
|
||||
|
||||
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/ncdrill.DRD')
|
||||
TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/top_copper.GTL')
|
||||
|
||||
def test_file_type_detection():
|
||||
""" Test file type detection
|
||||
"""
|
||||
ncdrill = read(NCDRILL_FILE)
|
||||
top_copper = read(TOP_COPPER_FILE)
|
||||
assert(isinstance(ncdrill, ExcellonFile))
|
||||
assert(isinstance(top_copper, GerberFile))
|
||||
32
gerber/tests/test_excellon.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
from ..excellon import read, detect_excellon_format, ExcellonFile
|
||||
from tests import *
|
||||
|
||||
import os
|
||||
|
||||
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/ncdrill.DRD')
|
||||
|
||||
def test_format_detection():
|
||||
""" Test file type detection
|
||||
"""
|
||||
settings = detect_excellon_format(NCDRILL_FILE)
|
||||
assert_equal(settings['format'], (2, 4))
|
||||
assert_equal(settings['zero_suppression'], 'leading')
|
||||
|
||||
def test_read():
|
||||
ncdrill = read(NCDRILL_FILE)
|
||||
assert(isinstance(ncdrill, ExcellonFile))
|
||||
|
||||
def test_read_settings():
|
||||
ncdrill = read(NCDRILL_FILE)
|
||||
assert_equal(ncdrill.settings.format, (2, 4))
|
||||
assert_equal(ncdrill.settings.zero_suppression, 'leading')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
270
gerber/tests/test_excellon_statements.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
from .tests import assert_equal, assert_raises
|
||||
from ..excellon_statements import *
|
||||
from ..cam import FileSettings
|
||||
|
||||
|
||||
def test_excellontool_factory():
|
||||
""" Test ExcellonTool factory method
|
||||
"""
|
||||
exc_line = 'T8F00S00C0.12500'
|
||||
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
|
||||
units='inch', notation='absolute')
|
||||
tool = ExcellonTool.from_excellon(exc_line, settings)
|
||||
assert_equal(tool.diameter, 0.125)
|
||||
assert_equal(tool.feed_rate, 0)
|
||||
assert_equal(tool.rpm, 0)
|
||||
|
||||
|
||||
def test_excellontool_dump():
|
||||
""" Test ExcellonTool to_excellon()
|
||||
"""
|
||||
exc_lines = ['T1F00S00C0.01200', 'T2F00S00C0.01500', 'T3F00S00C0.01968',
|
||||
'T4F00S00C0.02800', 'T5F00S00C0.03300', 'T6F00S00C0.03800',
|
||||
'T7F00S00C0.04300', 'T8F00S00C0.12500', 'T9F00S00C0.13000', ]
|
||||
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
|
||||
units='inch', notation='absolute')
|
||||
for line in exc_lines:
|
||||
tool = ExcellonTool.from_excellon(line, settings)
|
||||
assert_equal(tool.to_excellon(), line)
|
||||
|
||||
|
||||
def test_excellontool_order():
|
||||
settings = 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_equal(tool1.diameter, tool2.diameter)
|
||||
assert_equal(tool1.feed_rate, tool2.feed_rate)
|
||||
assert_equal(tool1.rpm, tool2.rpm)
|
||||
|
||||
|
||||
def test_toolselection_factory():
|
||||
""" Test ToolSelectionStmt factory method
|
||||
"""
|
||||
stmt = ToolSelectionStmt.from_excellon('T01')
|
||||
assert_equal(stmt.tool, 1)
|
||||
assert_equal(stmt.compensation_index, None)
|
||||
stmt = ToolSelectionStmt.from_excellon('T0223')
|
||||
assert_equal(stmt.tool, 2)
|
||||
assert_equal(stmt.compensation_index, 23)
|
||||
|
||||
|
||||
def test_toolselection_dump():
|
||||
""" Test ToolSelectionStmt to_excellon()
|
||||
"""
|
||||
lines = ['T01', 'T0223', 'T10', 'T09', 'T0000']
|
||||
for line in lines:
|
||||
stmt = ToolSelectionStmt.from_excellon(line)
|
||||
assert_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_coordinatestmt_factory():
|
||||
""" Test CoordinateStmt factory method
|
||||
"""
|
||||
line = 'X0278207Y0065293'
|
||||
stmt = CoordinateStmt.from_excellon(line)
|
||||
assert_equal(stmt.x, 2.78207)
|
||||
assert_equal(stmt.y, 0.65293)
|
||||
|
||||
line = 'X02945'
|
||||
stmt = CoordinateStmt.from_excellon(line)
|
||||
assert_equal(stmt.x, 2.945)
|
||||
|
||||
line = 'Y00575'
|
||||
stmt = CoordinateStmt.from_excellon(line)
|
||||
assert_equal(stmt.y, 0.575)
|
||||
|
||||
|
||||
def test_coordinatestmt_dump():
|
||||
""" Test CoordinateStmt to_excellon()
|
||||
"""
|
||||
lines = ['X0278207Y0065293', 'X0243795', 'Y0082528', 'Y0086028',
|
||||
'X0251295Y0081528', 'X02525Y0078', 'X0255Y00575', 'Y0052',
|
||||
'X02675', 'Y00575', 'X02425', 'Y0052', 'X023', ]
|
||||
for line in lines:
|
||||
stmt = CoordinateStmt.from_excellon(line)
|
||||
assert_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_commentstmt_factory():
|
||||
""" Test CommentStmt factory method
|
||||
"""
|
||||
line = ';Layer_Color=9474304'
|
||||
stmt = CommentStmt.from_excellon(line)
|
||||
assert_equal(stmt.comment, line[1:])
|
||||
|
||||
line = ';FILE_FORMAT=2:5'
|
||||
stmt = CommentStmt.from_excellon(line)
|
||||
assert_equal(stmt.comment, line[1:])
|
||||
|
||||
line = ';TYPE=PLATED'
|
||||
stmt = CommentStmt.from_excellon(line)
|
||||
assert_equal(stmt.comment, line[1:])
|
||||
|
||||
|
||||
def test_commentstmt_dump():
|
||||
""" Test CommentStmt to_excellon()
|
||||
"""
|
||||
lines = [';Layer_Color=9474304', ';FILE_FORMAT=2:5', ';TYPE=PLATED', ]
|
||||
for line in lines:
|
||||
stmt = CommentStmt.from_excellon(line)
|
||||
assert_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_unitstmt_factory():
|
||||
""" Test UnitStmt factory method
|
||||
"""
|
||||
line = 'INCH,LZ'
|
||||
stmt = UnitStmt.from_excellon(line)
|
||||
assert_equal(stmt.units, 'inch')
|
||||
assert_equal(stmt.zero_suppression, 'trailing')
|
||||
|
||||
line = 'METRIC,TZ'
|
||||
stmt = UnitStmt.from_excellon(line)
|
||||
assert_equal(stmt.units, 'metric')
|
||||
assert_equal(stmt.zero_suppression, 'leading')
|
||||
|
||||
|
||||
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_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_incrementalmode_factory():
|
||||
""" Test IncrementalModeStmt factory method
|
||||
"""
|
||||
line = 'ICI,ON'
|
||||
stmt = IncrementalModeStmt.from_excellon(line)
|
||||
assert_equal(stmt.mode, 'on')
|
||||
|
||||
line = 'ICI,OFF'
|
||||
stmt = IncrementalModeStmt.from_excellon(line)
|
||||
assert_equal(stmt.mode, 'off')
|
||||
|
||||
|
||||
def test_incrementalmode_dump():
|
||||
""" Test IncrementalModeStmt to_excellon()
|
||||
"""
|
||||
lines = ['ICI,ON', 'ICI,OFF', ]
|
||||
for line in lines:
|
||||
stmt = IncrementalModeStmt.from_excellon(line)
|
||||
assert_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_incrementalmode_validation():
|
||||
""" Test IncrementalModeStmt input validation
|
||||
"""
|
||||
assert_raises(ValueError, IncrementalModeStmt, 'OFF-ISH')
|
||||
|
||||
|
||||
def test_versionstmt_factory():
|
||||
""" Test VersionStmt factory method
|
||||
"""
|
||||
line = 'VER,1'
|
||||
stmt = VersionStmt.from_excellon(line)
|
||||
assert_equal(stmt.version, 1)
|
||||
|
||||
line = 'VER,2'
|
||||
stmt = VersionStmt.from_excellon(line)
|
||||
assert_equal(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_equal(stmt.to_excellon(), line)
|
||||
|
||||
def test_versionstmt_validation():
|
||||
""" Test VersionStmt input validation
|
||||
"""
|
||||
assert_raises(ValueError, VersionStmt, 3)
|
||||
|
||||
|
||||
def test_formatstmt_factory():
|
||||
""" Test FormatStmt factory method
|
||||
"""
|
||||
line = 'FMAT,1'
|
||||
stmt = FormatStmt.from_excellon(line)
|
||||
assert_equal(stmt.format, 1)
|
||||
|
||||
line = 'FMAT,2'
|
||||
stmt = FormatStmt.from_excellon(line)
|
||||
assert_equal(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_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_formatstmt_validation():
|
||||
""" Test FormatStmt input validation
|
||||
"""
|
||||
assert_raises(ValueError, FormatStmt, 3)
|
||||
|
||||
|
||||
def test_linktoolstmt_factory():
|
||||
""" Test LinkToolStmt factory method
|
||||
"""
|
||||
line = '1/2/3/4'
|
||||
stmt = LinkToolStmt.from_excellon(line)
|
||||
assert_equal(stmt.linked_tools, [1, 2, 3, 4])
|
||||
|
||||
line = '01/02/03/04'
|
||||
stmt = LinkToolStmt.from_excellon(line)
|
||||
assert_equal(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_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_measmodestmt_factory():
|
||||
""" Test MeasuringModeStmt factory method
|
||||
"""
|
||||
line = 'M72'
|
||||
stmt = MeasuringModeStmt.from_excellon(line)
|
||||
assert_equal(stmt.units, 'inch')
|
||||
|
||||
line = 'M71'
|
||||
stmt = MeasuringModeStmt.from_excellon(line)
|
||||
assert_equal(stmt.units, 'metric')
|
||||
|
||||
|
||||
def test_measmodestmt_dump():
|
||||
""" Test MeasuringModeStmt to_excellon()
|
||||
"""
|
||||
lines = ['M71', 'M72', ]
|
||||
for line in lines:
|
||||
stmt = MeasuringModeStmt.from_excellon(line)
|
||||
assert_equal(stmt.to_excellon(), line)
|
||||
|
||||
|
||||
def test_measmodestmt_validation():
|
||||
""" Test MeasuringModeStmt input validation
|
||||
"""
|
||||
assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70')
|
||||
assert_raises(ValueError, MeasuringModeStmt, 'millimeters')
|
||||
303
gerber/tests/test_gerber_statements.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
from .tests import *
|
||||
from ..gerber_statements import *
|
||||
|
||||
|
||||
def test_FSParamStmt_factory():
|
||||
""" Test FSParamStruct factory
|
||||
"""
|
||||
stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'}
|
||||
fs = FSParamStmt.from_dict(stmt)
|
||||
assert_equal(fs.param, 'FS')
|
||||
assert_equal(fs.zero_suppression, 'leading')
|
||||
assert_equal(fs.notation, 'absolute')
|
||||
assert_equal(fs.format, (2, 7))
|
||||
|
||||
stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '27'}
|
||||
fs = FSParamStmt.from_dict(stmt)
|
||||
assert_equal(fs.param, 'FS')
|
||||
assert_equal(fs.zero_suppression, 'trailing')
|
||||
assert_equal(fs.notation, 'incremental')
|
||||
assert_equal(fs.format, (2, 7))
|
||||
|
||||
def test_FSParamStmt():
|
||||
""" Test FSParamStmt initialization
|
||||
"""
|
||||
param = 'FS'
|
||||
zeros = 'trailing'
|
||||
notation = 'absolute'
|
||||
fmt = (2, 5)
|
||||
stmt = FSParamStmt(param, zeros, notation, fmt)
|
||||
assert_equal(stmt.param, param)
|
||||
assert_equal(stmt.zero_suppression, zeros)
|
||||
assert_equal(stmt.notation, notation)
|
||||
assert_equal(stmt.format, fmt)
|
||||
|
||||
def test_FSParamStmt_dump():
|
||||
""" Test FSParamStmt to_gerber()
|
||||
"""
|
||||
stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'}
|
||||
fs = FSParamStmt.from_dict(stmt)
|
||||
assert_equal(fs.to_gerber(), '%FSLAX27Y27*%')
|
||||
|
||||
stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'}
|
||||
fs = FSParamStmt.from_dict(stmt)
|
||||
assert_equal(fs.to_gerber(), '%FSTIX25Y25*%')
|
||||
|
||||
|
||||
def test_MOParamStmt_factory():
|
||||
""" Test MOParamStruct factory
|
||||
"""
|
||||
stmts = [{'param': 'MO', 'mo': 'IN'}, {'param': 'MO', 'mo': 'in'}, ]
|
||||
for stmt in stmts:
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.param, 'MO')
|
||||
assert_equal(mo.mode, 'inch')
|
||||
|
||||
stmts = [{'param': 'MO', 'mo': 'MM'}, {'param': 'MO', 'mo': 'mm'}, ]
|
||||
for stmt in stmts:
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.param, 'MO')
|
||||
assert_equal(mo.mode, 'metric')
|
||||
|
||||
def test_MOParamStmt():
|
||||
""" Test MOParamStmt initialization
|
||||
"""
|
||||
param = 'MO'
|
||||
mode = 'inch'
|
||||
stmt = MOParamStmt(param, mode)
|
||||
assert_equal(stmt.param, param)
|
||||
|
||||
for mode in ['inch', 'metric']:
|
||||
stmt = MOParamStmt(param, mode)
|
||||
assert_equal(stmt.mode, mode)
|
||||
|
||||
|
||||
def test_MOParamStmt_dump():
|
||||
""" Test MOParamStmt to_gerber()
|
||||
"""
|
||||
stmt = {'param': 'MO', 'mo': 'IN'}
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.to_gerber(), '%MOIN*%')
|
||||
|
||||
stmt = {'param': 'MO', 'mo': 'MM'}
|
||||
mo = MOParamStmt.from_dict(stmt)
|
||||
assert_equal(mo.to_gerber(), '%MOMM*%')
|
||||
|
||||
|
||||
def test_IPParamStmt_factory():
|
||||
""" Test IPParamStruct factory
|
||||
"""
|
||||
stmt = {'param': 'IP', 'ip': 'POS'}
|
||||
ip = IPParamStmt.from_dict(stmt)
|
||||
assert_equal(ip.ip, 'positive')
|
||||
|
||||
stmt = {'param': 'IP', 'ip': 'NEG'}
|
||||
ip = IPParamStmt.from_dict(stmt)
|
||||
assert_equal(ip.ip, 'negative')
|
||||
|
||||
def test_IPParamStmt():
|
||||
""" Test IPParamStmt initialization
|
||||
"""
|
||||
param = 'IP'
|
||||
for ip in ['positive', 'negative']:
|
||||
stmt = IPParamStmt(param, ip)
|
||||
assert_equal(stmt.param, param)
|
||||
assert_equal(stmt.ip, ip)
|
||||
|
||||
|
||||
def test_IPParamStmt_dump():
|
||||
""" Test IPParamStmt to_gerber()
|
||||
"""
|
||||
stmt = {'param': 'IP', 'ip': 'POS'}
|
||||
ip = IPParamStmt.from_dict(stmt)
|
||||
assert_equal(ip.to_gerber(), '%IPPOS*%')
|
||||
|
||||
stmt = {'param': 'IP', 'ip': 'NEG'}
|
||||
ip = IPParamStmt.from_dict(stmt)
|
||||
assert_equal(ip.to_gerber(), '%IPNEG*%')
|
||||
|
||||
|
||||
def test_OFParamStmt_factory():
|
||||
""" Test OFParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'}
|
||||
of = OFParamStmt.from_dict(stmt)
|
||||
assert_equal(of.a, 0.1234567)
|
||||
assert_equal(of.b, 0.1234567)
|
||||
|
||||
def test_OFParamStmt():
|
||||
""" Test IPParamStmt initialization
|
||||
"""
|
||||
param = 'OF'
|
||||
for val in [0.0, -3.4567]:
|
||||
stmt = OFParamStmt(param, val, val)
|
||||
assert_equal(stmt.param, param)
|
||||
assert_equal(stmt.a, val)
|
||||
assert_equal(stmt.b, val)
|
||||
|
||||
def test_OFParamStmt_dump():
|
||||
""" Test OFParamStmt to_gerber()
|
||||
"""
|
||||
stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'}
|
||||
of = OFParamStmt.from_dict(stmt)
|
||||
assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%')
|
||||
|
||||
|
||||
def test_LPParamStmt_factory():
|
||||
""" Test LPParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'LP', 'lp': 'C'}
|
||||
lp = LPParamStmt.from_dict(stmt)
|
||||
assert_equal(lp.lp, 'clear')
|
||||
|
||||
stmt = {'param': 'LP', 'lp': 'D'}
|
||||
lp = LPParamStmt.from_dict(stmt)
|
||||
assert_equal(lp.lp, 'dark')
|
||||
|
||||
def test_LPParamStmt_dump():
|
||||
""" Test LPParamStmt to_gerber()
|
||||
"""
|
||||
stmt = {'param': 'LP', 'lp': 'C'}
|
||||
lp = LPParamStmt.from_dict(stmt)
|
||||
assert_equal(lp.to_gerber(), '%LPC*%')
|
||||
|
||||
stmt = {'param': 'LP', 'lp': 'D'}
|
||||
lp = LPParamStmt.from_dict(stmt)
|
||||
assert_equal(lp.to_gerber(), '%LPD*%')
|
||||
|
||||
|
||||
def test_INParamStmt_factory():
|
||||
""" Test INParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'IN', 'name': 'test'}
|
||||
inp = INParamStmt.from_dict(stmt)
|
||||
assert_equal(inp.name, 'test')
|
||||
|
||||
def test_INParamStmt_dump():
|
||||
""" Test INParamStmt to_gerber()
|
||||
"""
|
||||
stmt = {'param': 'IN', 'name': 'test'}
|
||||
inp = INParamStmt.from_dict(stmt)
|
||||
assert_equal(inp.to_gerber(), '%INtest*%')
|
||||
|
||||
|
||||
def test_LNParamStmt_factory():
|
||||
""" Test LNParamStmt factory
|
||||
"""
|
||||
stmt = {'param': 'LN', 'name': 'test'}
|
||||
lnp = LNParamStmt.from_dict(stmt)
|
||||
assert_equal(lnp.name, 'test')
|
||||
|
||||
def test_LNParamStmt_dump():
|
||||
""" Test LNParamStmt to_gerber()
|
||||
"""
|
||||
stmt = {'param': 'LN', 'name': 'test'}
|
||||
lnp = LNParamStmt.from_dict(stmt)
|
||||
assert_equal(lnp.to_gerber(), '%LNtest*%')
|
||||
|
||||
def test_comment_stmt():
|
||||
""" Test comment statement
|
||||
"""
|
||||
stmt = CommentStmt('A comment')
|
||||
assert_equal(stmt.type, 'COMMENT')
|
||||
assert_equal(stmt.comment, 'A comment')
|
||||
|
||||
def test_comment_stmt_dump():
|
||||
""" Test CommentStmt to_gerber()
|
||||
"""
|
||||
stmt = CommentStmt('A comment')
|
||||
assert_equal(stmt.to_gerber(), 'G04A comment*')
|
||||
|
||||
|
||||
def test_eofstmt():
|
||||
""" Test EofStmt
|
||||
"""
|
||||
stmt = EofStmt()
|
||||
assert_equal(stmt.type, 'EOF')
|
||||
|
||||
def test_eofstmt_dump():
|
||||
""" Test EofStmt to_gerber()
|
||||
"""
|
||||
stmt = EofStmt()
|
||||
assert_equal(stmt.to_gerber(), 'M02*')
|
||||
|
||||
|
||||
def test_quadmodestmt_factory():
|
||||
""" Test QuadrantModeStmt.from_gerber()
|
||||
"""
|
||||
line = 'G74*'
|
||||
stmt = QuadrantModeStmt.from_gerber(line)
|
||||
assert_equal(stmt.type, 'QuadrantMode')
|
||||
assert_equal(stmt.mode, 'single-quadrant')
|
||||
|
||||
line = 'G75*'
|
||||
stmt = QuadrantModeStmt.from_gerber(line)
|
||||
assert_equal(stmt.mode, 'multi-quadrant')
|
||||
|
||||
def test_quadmodestmt_validation():
|
||||
""" Test QuadrantModeStmt input validation
|
||||
"""
|
||||
line = 'G76*'
|
||||
assert_raises(ValueError, QuadrantModeStmt.from_gerber, line)
|
||||
assert_raises(ValueError, QuadrantModeStmt, 'quadrant-ful')
|
||||
|
||||
|
||||
def test_quadmodestmt_dump():
|
||||
""" Test QuadrantModeStmt.to_gerber()
|
||||
"""
|
||||
for line in ('G74*', 'G75*',):
|
||||
stmt = QuadrantModeStmt.from_gerber(line)
|
||||
assert_equal(stmt.to_gerber(), line)
|
||||
|
||||
|
||||
def test_regionmodestmt_factory():
|
||||
""" Test RegionModeStmt.from_gerber()
|
||||
"""
|
||||
line = 'G36*'
|
||||
stmt = RegionModeStmt.from_gerber(line)
|
||||
assert_equal(stmt.type, 'RegionMode')
|
||||
assert_equal(stmt.mode, 'on')
|
||||
|
||||
line = 'G37*'
|
||||
stmt = RegionModeStmt.from_gerber(line)
|
||||
assert_equal(stmt.mode, 'off')
|
||||
|
||||
|
||||
def test_regionmodestmt_validation():
|
||||
""" Test RegionModeStmt input validation
|
||||
"""
|
||||
line = 'G38*'
|
||||
assert_raises(ValueError, RegionModeStmt.from_gerber, line)
|
||||
assert_raises(ValueError, RegionModeStmt, 'off-ish')
|
||||
|
||||
|
||||
def test_regionmodestmt_dump():
|
||||
""" Test RegionModeStmt.to_gerber()
|
||||
"""
|
||||
for line in ('G36*', 'G37*',):
|
||||
stmt = RegionModeStmt.from_gerber(line)
|
||||
assert_equal(stmt.to_gerber(), line)
|
||||
|
||||
|
||||
def test_unknownstmt():
|
||||
""" Test UnknownStmt
|
||||
"""
|
||||
line = 'G696969*'
|
||||
stmt = UnknownStmt(line)
|
||||
assert_equal(stmt.type, 'UNKNOWN')
|
||||
assert_equal(stmt.line, line)
|
||||
|
||||
|
||||
def test_unknownstmt_dump():
|
||||
""" Test UnknownStmt.to_gerber()
|
||||
"""
|
||||
lines = ('G696969*', 'M03*',)
|
||||
for line in lines:
|
||||
stmt = UnknownStmt(line)
|
||||
assert_equal(stmt.to_gerber(), line)
|
||||
|
||||
16
gerber/tests/test_rs274x.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
from ..rs274x import read, GerberFile
|
||||
from tests import *
|
||||
|
||||
import os
|
||||
|
||||
TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/top_copper.GTL')
|
||||
|
||||
|
||||
def test_read():
|
||||
top_copper = read(TOP_COPPER_FILE)
|
||||
assert(isinstance(top_copper, GerberFile))
|
||||
83
gerber/tests/test_utils.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
from .tests import assert_equal
|
||||
from ..utils import decimal_string, parse_gerber_value, write_gerber_value
|
||||
|
||||
|
||||
def test_zero_suppression():
|
||||
""" Test gerber value parser and writer handle zero suppression correctly.
|
||||
"""
|
||||
# Default format
|
||||
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), ]
|
||||
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)]
|
||||
for 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_format():
|
||||
""" Test gerber value parser and writer handle format correctly
|
||||
"""
|
||||
zero_suppression = 'leading'
|
||||
test_cases = [((2, 7), '1', 0.0000001), ((2, 6), '1', 0.000001),
|
||||
((2, 5), '1', 0.00001), ((2, 4), '1', 0.0001),
|
||||
((2, 3), '1', 0.001), ((2, 2), '1', 0.01),
|
||||
((2, 1), '1', 0.1), ((2, 7), '-1', -0.0000001),
|
||||
((2, 6), '-1', -0.000001), ((2, 5), '-1', -0.00001),
|
||||
((2, 4), '-1', -0.0001), ((2, 3), '-1', -0.001),
|
||||
((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1), ]
|
||||
for fmt, string, value in test_cases:
|
||||
assert(value == parse_gerber_value(string, fmt, zero_suppression))
|
||||
assert(string == write_gerber_value(value, fmt, zero_suppression))
|
||||
|
||||
zero_suppression = 'trailing'
|
||||
test_cases = [((6, 5), '1', 100000.0), ((5, 5), '1', 10000.0),
|
||||
((4, 5), '1', 1000.0), ((3, 5), '1', 100.0),
|
||||
((2, 5), '1', 10.0), ((1, 5), '1', 1.0),
|
||||
((6, 5), '-1', -100000.0), ((5, 5), '-1', -10000.0),
|
||||
((4, 5), '-1', -1000.0), ((3, 5), '-1', -100.0),
|
||||
((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0), ]
|
||||
for 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_equal(decimal_string(value, precision=3, padding=True), '1.123')
|
||||
assert_equal(decimal_string(value, precision=4, padding=True), '1.1230')
|
||||
assert_equal(decimal_string(value, precision=5, padding=True), '1.12300')
|
||||
assert_equal(decimal_string(value, precision=6, padding=True), '1.123000')
|
||||
18
gerber/tests/tests.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
from nose.tools import assert_in
|
||||
from nose.tools import assert_not_in
|
||||
from nose.tools import assert_equal
|
||||
from nose.tools import assert_not_equal
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_false
|
||||
from nose.tools import assert_raises
|
||||
from nose.tools import raises
|
||||
from nose import with_setup
|
||||
|
||||
__all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal',
|
||||
'assert_true', 'assert_false', 'assert_raises', 'raises',
|
||||
'with_setup' ]
|
||||
136
gerber/utils.py
|
|
@ -23,27 +23,30 @@ This module provides utility functions for working with Gerber and Excellon
|
|||
files.
|
||||
"""
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# License:
|
||||
|
||||
|
||||
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
|
||||
""" Convert gerber/excellon formatted string to floating-point number
|
||||
|
||||
|
||||
.. note::
|
||||
Format and zero suppression are configurable. Note that the Excellon
|
||||
and Gerber formats use opposite terminology with respect to leading
|
||||
and trailing zeros. The Gerber format specifies which zeros are
|
||||
suppressed, while the Excellon format specifies which zeros are
|
||||
included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use
|
||||
`zero_suppression='trailing'`
|
||||
|
||||
|
||||
Format and zero suppression are configurable. Note that the Excellon
|
||||
and Gerber formats use opposite terminology with respect to leading
|
||||
and trailing zeros. The Gerber format specifies which zeros are
|
||||
suppressed, while the Excellon format specifies which zeros are
|
||||
included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use
|
||||
`zero_suppression='trailing'`
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : string
|
||||
A Gerber/Excellon-formatted string representing a numerical value.
|
||||
|
||||
format : tuple (int,int)
|
||||
Gerber/Excellon precision format expressed as a tuple containing:
|
||||
Gerber/Excellon precision format expressed as a tuple containing:
|
||||
(number of integer-part digits, number of decimal-part digits)
|
||||
|
||||
zero_suppression : string
|
||||
|
|
@ -53,12 +56,16 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
|
|||
-------
|
||||
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
|
||||
|
|
@ -66,45 +73,41 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
|
|||
raise ValueError('Parser only supports precision up to 6:7 format')
|
||||
|
||||
# Remove extraneous information
|
||||
value = value.strip()
|
||||
value = value.strip(' +')
|
||||
#value = value.strip()
|
||||
value = value.lstrip('+')
|
||||
negative = '-' in value
|
||||
if negative:
|
||||
value = value.strip(' -')
|
||||
|
||||
# Handle excellon edge case with explicit decimal. "That was easy!"
|
||||
if '.' in value:
|
||||
return float(value)
|
||||
|
||||
digits = [digit for digit in '0' * MAX_DIGITS]
|
||||
value = value.lstrip('-')
|
||||
|
||||
|
||||
digits = list('0' * MAX_DIGITS)
|
||||
offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value))
|
||||
for i, digit in enumerate(value):
|
||||
digits[i + offset] = digit
|
||||
|
||||
|
||||
|
||||
result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:]))
|
||||
return -1.0 * result if negative else result
|
||||
|
||||
|
||||
return -result if negative else result
|
||||
|
||||
|
||||
def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
|
||||
""" Convert a floating point number to a Gerber/Excellon-formatted string.
|
||||
|
||||
|
||||
.. note::
|
||||
Format and zero suppression are configurable. Note that the Excellon
|
||||
and Gerber formats use opposite terminology with respect to leading
|
||||
and trailing zeros. The Gerber format specifies which zeros are
|
||||
suppressed, while the Excellon format specifies which zeros are
|
||||
included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use
|
||||
Format and zero suppression are configurable. Note that the Excellon
|
||||
and Gerber formats use opposite terminology with respect to leading
|
||||
and trailing zeros. The Gerber format specifies which zeros are
|
||||
suppressed, while the Excellon format specifies which zeros are
|
||||
included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use
|
||||
`zero_suppression='trailing'`
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
A floating point value.
|
||||
|
||||
format : tuple (n=2)
|
||||
Gerber/Excellon precision format expressed as a tuple containing:
|
||||
Gerber/Excellon precision format expressed as a tuple containing:
|
||||
(number of integer-part digits, number of decimal-part digits)
|
||||
|
||||
zero_suppression : string
|
||||
|
|
@ -118,61 +121,90 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
|
|||
# Format precision
|
||||
integer_digits, decimal_digits = format
|
||||
MAX_DIGITS = integer_digits + decimal_digits
|
||||
|
||||
|
||||
if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7:
|
||||
raise ValueError('Parser only supports precision up to 6:7 format')
|
||||
|
||||
|
||||
# Edge case...
|
||||
if value == 0:
|
||||
return '00'
|
||||
|
||||
# negative sign affects padding, so deal with it at the end...
|
||||
negative = value < 0.0
|
||||
negative = value < 0.0
|
||||
if negative:
|
||||
value = -1.0 * value
|
||||
|
||||
# Format string for padding out in both directions
|
||||
fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits)
|
||||
|
||||
digits = [val for val in fmtstring % value if val != '.']
|
||||
|
||||
# Suppression...
|
||||
|
||||
# Suppression...
|
||||
if zero_suppression == 'trailing':
|
||||
while digits[-1] == '0':
|
||||
digits.pop()
|
||||
else:
|
||||
while digits[0] == '0':
|
||||
digits.pop(0)
|
||||
|
||||
|
||||
return ''.join(digits) if not negative else ''.join(['-'] + digits)
|
||||
|
||||
|
||||
|
||||
def decimal_string(value, precision=6):
|
||||
def decimal_string(value, precision=6, padding=False):
|
||||
""" Convert float to string with limited precision
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
A floating point value.
|
||||
|
||||
precision :
|
||||
precision :
|
||||
Maximum number of decimal places to print
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : string
|
||||
The specified value as a string.
|
||||
|
||||
|
||||
"""
|
||||
floatstr = '%0.20g' % value
|
||||
floatstr = '%0.10g' % value
|
||||
integer = None
|
||||
decimal = None
|
||||
if '.' in floatstr:
|
||||
integer, decimal = floatstr.split('.')
|
||||
integer, decimal = floatstr.split('.')
|
||||
elif ',' in floatstr:
|
||||
integer, decimal = floatstr.split(',')
|
||||
integer, decimal = floatstr.split(',')
|
||||
if len(decimal) > precision:
|
||||
decimal = decimal[:precision]
|
||||
elif padding:
|
||||
decimal = decimal + (precision - len(decimal)) * '0'
|
||||
if integer or decimal:
|
||||
return ''.join([integer, '.', decimal])
|
||||
else:
|
||||
return int(floatstr)
|
||||
|
||||
|
||||
|
||||
def detect_file_format(filename):
|
||||
""" Determine format of a file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of the file to read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
format : string
|
||||
File format. either 'excellon' or 'rs274x'
|
||||
"""
|
||||
|
||||
# Read the first 20 lines
|
||||
with open(filename, 'r') as f:
|
||||
lines = [next(f) for x in xrange(20)]
|
||||
|
||||
# Look for
|
||||
for line in lines:
|
||||
if 'M48' in line:
|
||||
return 'excellon'
|
||||
elif '%FS' in line:
|
||||
return'rs274x'
|
||||
return 'unknown'
|
||||
|
|
|
|||
12
requirements.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
## The following requirements were added by pip --freeze:
|
||||
Jinja2==2.7.3
|
||||
MarkupSafe==0.23
|
||||
Pygments==1.6
|
||||
Sphinx==1.2.3
|
||||
coverage==3.7.1
|
||||
docutils==0.12
|
||||
nose==1.3.4
|
||||
numpydoc==0.5
|
||||
pyparsing==2.0.2
|
||||
svgwrite==1.1.6
|
||||
wsgiref==0.1.2
|
||||
66
setup.py
|
|
@ -1,23 +1,61 @@
|
|||
#!/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
|
||||
from setuptools import setup
|
||||
|
||||
def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
setup(
|
||||
name = "gerber-tools",
|
||||
version = "0.1",
|
||||
author = "Paulo Henrique Silva",
|
||||
author_email = "ph.silva@gmail.com",
|
||||
description = ("Utilities to handle Gerber (RS-274X) files."),
|
||||
license = "Apache",
|
||||
keywords = "gerber tools",
|
||||
url = "http://github.com/curtacircuitos/gerber-tools",
|
||||
packages=['gerber'],
|
||||
long_description=read('README.md'),
|
||||
classifiers=[
|
||||
|
||||
METADATA = {
|
||||
'name': 'gerber-tools',
|
||||
'version': 0.1,
|
||||
'author': 'Paulo Henrique Silva <ph.silva@gmail.com>, Hamilton Kibbe <ham@hamiltonkib.be>',
|
||||
'author_email': "ph.silva@gmail.com, ham@hamiltonkib.be",
|
||||
'description': ("Utilities to handle Gerber (RS-274X) files."),
|
||||
'license': "Apache",
|
||||
'keywords': "gerber tools",
|
||||
'url': "http://github.com/curtacircuitos/gerber-tools",
|
||||
'packages': ['gerber'],
|
||||
'long_description': read('README.md'),
|
||||
'classifiers':[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Topic :: Utilities",
|
||||
"License :: OSI Approved :: Apple Public Source License",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
SETUPTOOLS_METADATA = {
|
||||
'install_requires': ['svgwrite'],
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||