Graft pcb-tools upstream onto gerbonara tree
|
@ -0,0 +1,8 @@
|
|||
[run]
|
||||
branch = True
|
||||
source = gerber
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
omit =
|
||||
gerber/tests/*
|
|
@ -0,0 +1,45 @@
|
|||
name: pcb-tools
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
- name: Run coverage
|
||||
run: |
|
||||
make test-coverage
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
flags: unittest
|
4
LICENSE
|
@ -178,7 +178,7 @@
|
|||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
|
@ -186,7 +186,7 @@
|
|||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021 pygerber
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
41
Makefile
|
@ -1,16 +1,27 @@
|
|||
PROJECT = "desec_dns_api"
|
||||
|
||||
.PHONY: all clean sdist bdist_wheel test coverage html upload testupload
|
||||
PYTHON ?= python
|
||||
PYTEST ?= pytest
|
||||
|
||||
all: sdist bdist_wheel
|
||||
.PHONY: clean
|
||||
clean: doc-clean
|
||||
find . -name '*.pyc' -delete
|
||||
rm -rf *.egg-info
|
||||
rm -f .coverage
|
||||
rm -f coverage.xml
|
||||
|
||||
clean:
|
||||
rm -rf .pytest_cache/
|
||||
rm -rf build/
|
||||
rm -rf ${PROJECT}.egg-info/
|
||||
rm -rf dist/
|
||||
rm -rf htmlcov/
|
||||
rm -rf .coverage
|
||||
.PHONY: test
|
||||
test:
|
||||
$(PYTEST)
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage:
|
||||
rm -f .coverage
|
||||
rm -f coverage.xml
|
||||
$(PYTEST) --cov=./ --cov-report=xml
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
PYTHONPATH=. $(PYTHON) setup.py install
|
||||
|
||||
sdist:
|
||||
python3 setup.py sdist
|
||||
|
@ -18,17 +29,9 @@ sdist:
|
|||
bdist_wheel:
|
||||
python3 setup.py bdist_wheel
|
||||
|
||||
test:
|
||||
pytest --flake8 tests/
|
||||
|
||||
coverage:
|
||||
pytest --cov=${PROJECT} tests/
|
||||
|
||||
html:
|
||||
pytest --cov-report html:htmlcov --cov=${PROJECT} tests/
|
||||
|
||||
upload: sdist bdist_wheel
|
||||
twine upload -s -i contact@gerbonara.io --config-file ~/.pypirc --skip-existing --repository pypi dist/*
|
||||
|
||||
testupload: sdist bdist_wheel
|
||||
twine upload --config-file ~/.pypirc --skip-existing --repository testpypi dist/*
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
|
||||
Tools to handle Gerber and Excellon files in Python.
|
||||
|
||||
This repository is a friendly fork of [phsilva's pcb-tools](https://github.com/curtacircuitos/pcb-tools) with
|
||||
[extensions from opiopan](https://github.com/opiopan/pcb-tools-extension) integrated. We decided to fork pcb-tools since
|
||||
we need it as a dependency for [gerbolyze](https://gitlab.com/gerbolyze/gerbolyze) and pcb-tools was sometimes very
|
||||
behind on bug fixes.
|
||||
|
||||
# Installation
|
||||
|
||||
Arch Linux:
|
||||
|
@ -44,3 +49,5 @@ nc_drill.render(ctx, 'composite.svg')
|
|||
---
|
||||
|
||||
Made with ❤️ and 🐍.
|
||||
=======
|
||||
pcb-tools
|
||||
|
|
|
@ -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."
|
|
@ -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
|
|
@ -0,0 +1,40 @@
|
|||
About PCB Tools
|
||||
===============
|
||||
|
||||
|
||||
PCB Tools provides a set of utilities for visualizing and working with PCB
|
||||
design files in a variety of formats. The design files are generally referred
|
||||
to as Gerber files. This is a generic term that may refer to
|
||||
`RS-274X (Gerber) <http://en.wikipedia.org/wiki/Gerber_format>`_,
|
||||
`ODB++ <http://en.wikipedia.org/wiki/ODB%2B%2B>`_ ,
|
||||
or `Excellon <http://en.wikipedia.org/wiki/Excellon_format>`_ files. These
|
||||
file formats are used by the CNC equipment used to manufacutre PCBs.
|
||||
|
||||
PCB Tools currently supports the following file formats:
|
||||
|
||||
- Gerber (RS-274X)
|
||||
- Excellon
|
||||
|
||||
with planned support for IPC-2581, ODB++ and more.
|
||||
|
||||
Image Rendering
|
||||
~~~~~~~~~~~~~~~
|
||||
.. image:: ../../examples/cairo_example.png
|
||||
:alt: Rendering Example
|
||||
|
||||
The PCB Tools module provides tools to visualize PCBs and export images in a
|
||||
variety of formats, including SVG and PNG.
|
||||
|
||||
|
||||
|
||||
|
||||
Future Plans
|
||||
~~~~~~~~~~~~
|
||||
We are working on adding the following features to PCB Tools:
|
||||
|
||||
- Design Rules Checking
|
||||
- Editing
|
||||
- Panelization
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Gerber Tools documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Sep 28 18:16:46 2014.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('../../'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'numpydoc',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'PCB Tools'
|
||||
copyright = u'2014 Paulo Henrique Silva <ph.silva@gmail.com>, Hamilton Kibbe <ham@hamiltonkib.be>'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
add_module_names = False
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'PCBToolsdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'PCBTools.tex', u'PCB Tools Documentation',
|
||||
u'Hamilton Kibbe', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'pcbtools', u'PCB Tools Documentation',
|
||||
[u'Hamilton Kibbe'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'PCBTools', u'PCB Tools Documentation',
|
||||
u'Hamilton Kibbe', 'PCBTools', 'Tools for working with PCB CAM files.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
|
@ -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:
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
PCB Tools Reference
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
Gerber (RS-274X) Files <rs274x>
|
||||
Excellon Files <excellon>
|
||||
Operations <operations>
|
||||
Rendering <render>
|
|
@ -0,0 +1,24 @@
|
|||
:mod:`operations` --- Cam File operations
|
||||
=========================================
|
||||
|
||||
.. module:: operations
|
||||
:synopsis: Functions for modifying CAM files
|
||||
.. sectionauthor:: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
|
||||
The :mod:`operations` module provides functions which modify
|
||||
:class:`gerber.cam.CamFile` objects. All of the functions in this module
|
||||
return a modified copy of the supplied file.
|
||||
|
||||
.. _operations-contents:
|
||||
|
||||
Functions
|
||||
---------
|
||||
The :mod:`operations` module defines the following functions:
|
||||
|
||||
.. autofunction:: gerber.operations.to_inch
|
||||
.. autofunction:: gerber.operations.to_metric
|
||||
.. autofunction:: gerber.operations.offset
|
||||
|
||||
|
||||
|
|
@ -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:
|
|
@ -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:
|
|
@ -0,0 +1,14 @@
|
|||
Feature Suppport
|
||||
================
|
||||
|
||||
Currently supported features are as follows:
|
||||
|
||||
============ ======== =========== ================ ====== ======= =======
|
||||
File Format Parsing Rendering Unit Conversion Scale Offset Rotate
|
||||
============ ======== =========== ================ ====== ======= =======
|
||||
RS274-X Yes Yes Yes No Yes No
|
||||
Excellon Yes Yes Yes No Yes No
|
||||
ODB++ No No No No No No
|
||||
============ ======== =========== ================ ====== ======= =======
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.. PCB-tools documentation master file, created by
|
||||
sphinx-quickstart on Sun Sep 28 18:16:46 2014.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
PCB-Tools
|
||||
========================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
about
|
||||
features
|
||||
documentation/index
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
Po Szerokość: | Wysokość: | Rozmiar: 41 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 98 KiB |
|
@ -0,0 +1,78 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
"""
|
||||
This example demonstrates the use of pcb-tools with cairo to render a composite
|
||||
image from a set of gerber files. Each layer is loaded and drawn using a
|
||||
GerberCairoContext. The color and opacity of each layer can be set individually.
|
||||
Once all thedesired layers are drawn on the context, the context is written to
|
||||
a .png file.
|
||||
"""
|
||||
|
||||
import os
|
||||
from gerber import load_layer
|
||||
from gerber.render import RenderSettings, theme
|
||||
from gerber.render.cairo_backend import GerberCairoContext
|
||||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
|
||||
|
||||
|
||||
# Open the gerber files
|
||||
copper = load_layer(os.path.join(GERBER_FOLDER, 'copper.GTL'))
|
||||
mask = load_layer(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
|
||||
silk = load_layer(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
|
||||
drill = load_layer(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
# Draw the copper layer. render_layer() uses the default color scheme for the
|
||||
# layer, based on the layer type. Copper layers are rendered as
|
||||
ctx.render_layer(copper)
|
||||
|
||||
# Draw the soldermask layer
|
||||
ctx.render_layer(mask)
|
||||
|
||||
|
||||
# The default style can be overridden by passing a RenderSettings instance to
|
||||
# render_layer().
|
||||
# First, create a settings object:
|
||||
our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85)
|
||||
|
||||
# Draw the silkscreen layer, and specify the rendering settings to use
|
||||
ctx.render_layer(silk, settings=our_settings)
|
||||
|
||||
# Draw the drill layer
|
||||
ctx.render_layer(drill)
|
||||
|
||||
# Write output to png file
|
||||
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_example.png'))
|
||||
|
||||
# Load the bottom layers
|
||||
copper = load_layer(os.path.join(GERBER_FOLDER, 'bottom_copper.GBL'))
|
||||
mask = load_layer(os.path.join(GERBER_FOLDER, 'bottom_mask.GBS'))
|
||||
|
||||
# Clear the drawing
|
||||
ctx.clear()
|
||||
|
||||
# Render bottom layers
|
||||
ctx.render_layer(copper)
|
||||
ctx.render_layer(mask)
|
||||
ctx.render_layer(drill)
|
||||
|
||||
# Write png file
|
||||
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_bottom.png'))
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Example using pcb-tools with tsp-solver (github.com/dmishin/tsp-solver) to
|
||||
# optimize tool paths in an Excellon file.
|
||||
#
|
||||
#
|
||||
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Based on a script by https://github.com/koppi
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
import sys
|
||||
import math
|
||||
import gerber
|
||||
from operator import sub
|
||||
from gerber.excellon import DrillHit
|
||||
|
||||
try:
|
||||
from tsp_solver.greedy import solve_tsp
|
||||
except ImportError:
|
||||
print('\n=================================================================\n'
|
||||
'This example requires tsp-solver be installed in order to run.\n\n'
|
||||
'tsp-solver can be downloaded from:\n'
|
||||
' http://github.com/dmishin/tsp-solver.\n'
|
||||
'=================================================================')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# Get file name to open
|
||||
if len(sys.argv) < 2:
|
||||
fname = 'gerbers/shld.drd'
|
||||
else:
|
||||
fname = sys.argv[1]
|
||||
|
||||
# Read the excellon file
|
||||
f = gerber.read(fname)
|
||||
|
||||
positions = {}
|
||||
tools = {}
|
||||
hit_counts = f.hit_count()
|
||||
oldpath = sum(f.path_length().values())
|
||||
|
||||
#Get hit positions
|
||||
for hit in f.hits:
|
||||
tool_num = hit.tool.number
|
||||
if tool_num not in positions.keys():
|
||||
positions[tool_num] = []
|
||||
positions[tool_num].append(hit.position)
|
||||
|
||||
hits = []
|
||||
|
||||
# Optimize tool path for each tool
|
||||
for tool, count in iter(hit_counts.items()):
|
||||
|
||||
# Calculate distance matrix
|
||||
distance_matrix = [[math.hypot(*tuple(map(sub,
|
||||
positions[tool][i],
|
||||
positions[tool][j])))
|
||||
for j in iter(range(count))]
|
||||
for i in iter(range(count))]
|
||||
|
||||
# Calculate new path
|
||||
path = solve_tsp(distance_matrix, 50)
|
||||
|
||||
# Create new hits list
|
||||
hits += [DrillHit(f.tools[tool], positions[tool][p]) for p in path]
|
||||
|
||||
# Update the file
|
||||
f.hits = hits
|
||||
f.filename = f.filename + '.optimized'
|
||||
f.write()
|
||||
|
||||
# Print drill report
|
||||
print(f.report())
|
||||
print('Original path length: %1.4f' % oldpath)
|
||||
print('Optimized path length: %1.4f' % sum(f.path_length().values()))
|
||||
|
Po Szerokość: | Wysokość: | Rozmiar: 33 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 88 KiB |
|
@ -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*
|
|
@ -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
|
|
@ -0,0 +1,162 @@
|
|||
G75*
|
||||
%MOIN*%
|
||||
%OFA0B0*%
|
||||
%FSLAX24Y24*%
|
||||
%IPPOS*%
|
||||
%LPD*%
|
||||
%AMOC8*
|
||||
5,1,8,0,0,1.08239,22.5*
|
||||
%
|
||||
%ADD10R,0.0340X0.0880*%
|
||||
%ADD11R,0.0671X0.0237*%
|
||||
%ADD12R,0.4178X0.4332*%
|
||||
%ADD13R,0.0930X0.0500*%
|
||||
%ADD14R,0.0710X0.1655*%
|
||||
%ADD15R,0.0671X0.0592*%
|
||||
%ADD16R,0.0592X0.0671*%
|
||||
%ADD17R,0.0710X0.1615*%
|
||||
%ADD18R,0.1419X0.0828*%
|
||||
%ADD19C,0.0634*%
|
||||
%ADD20C,0.1360*%
|
||||
%ADD21R,0.0474X0.0580*%
|
||||
%ADD22C,0.0680*%
|
||||
%ADD23R,0.0552X0.0552*%
|
||||
%ADD24C,0.1340*%
|
||||
%ADD25C,0.0476*%
|
||||
D10*
|
||||
X005000Y010604D03*
|
||||
X005500Y010604D03*
|
||||
X006000Y010604D03*
|
||||
X006500Y010604D03*
|
||||
X006500Y013024D03*
|
||||
X006000Y013024D03*
|
||||
X005500Y013024D03*
|
||||
X005000Y013024D03*
|
||||
D11*
|
||||
X011423Y007128D03*
|
||||
X011423Y006872D03*
|
||||
X011423Y006616D03*
|
||||
X011423Y006360D03*
|
||||
X011423Y006104D03*
|
||||
X011423Y005848D03*
|
||||
X011423Y005592D03*
|
||||
X011423Y005336D03*
|
||||
X011423Y005080D03*
|
||||
X011423Y004825D03*
|
||||
X011423Y004569D03*
|
||||
X011423Y004313D03*
|
||||
X011423Y004057D03*
|
||||
X011423Y003801D03*
|
||||
X014277Y003801D03*
|
||||
X014277Y004057D03*
|
||||
X014277Y004313D03*
|
||||
X014277Y004569D03*
|
||||
X014277Y004825D03*
|
||||
X014277Y005080D03*
|
||||
X014277Y005336D03*
|
||||
X014277Y005592D03*
|
||||
X014277Y005848D03*
|
||||
X014277Y006104D03*
|
||||
X014277Y006360D03*
|
||||
X014277Y006616D03*
|
||||
X014277Y006872D03*
|
||||
X014277Y007128D03*
|
||||
D12*
|
||||
X009350Y010114D03*
|
||||
D13*
|
||||
X012630Y010114D03*
|
||||
X012630Y010784D03*
|
||||
X012630Y011454D03*
|
||||
X012630Y009444D03*
|
||||
X012630Y008774D03*
|
||||
D14*
|
||||
X010000Y013467D03*
|
||||
X010000Y016262D03*
|
||||
D15*
|
||||
X004150Y012988D03*
|
||||
X004150Y012240D03*
|
||||
X009900Y005688D03*
|
||||
X009900Y004940D03*
|
||||
X015000Y006240D03*
|
||||
X015000Y006988D03*
|
||||
D16*
|
||||
X014676Y008364D03*
|
||||
X015424Y008364D03*
|
||||
X017526Y004514D03*
|
||||
X018274Y004514D03*
|
||||
X010674Y004064D03*
|
||||
X009926Y004064D03*
|
||||
X004174Y009564D03*
|
||||
X003426Y009564D03*
|
||||
X005376Y014564D03*
|
||||
X006124Y014564D03*
|
||||
D17*
|
||||
X014250Y016088D03*
|
||||
X014250Y012741D03*
|
||||
D18*
|
||||
X014250Y010982D03*
|
||||
X014250Y009447D03*
|
||||
D19*
|
||||
X017200Y009464D03*
|
||||
X018200Y009964D03*
|
||||
X018200Y010964D03*
|
||||
X017200Y010464D03*
|
||||
X017200Y011464D03*
|
||||
X018200Y011964D03*
|
||||
D20*
|
||||
X020700Y012714D03*
|
||||
X020700Y008714D03*
|
||||
D21*
|
||||
X005004Y003814D03*
|
||||
X005004Y004864D03*
|
||||
X005004Y005864D03*
|
||||
X005004Y006914D03*
|
||||
X008696Y006914D03*
|
||||
X008696Y005864D03*
|
||||
X008696Y004864D03*
|
||||
X008696Y003814D03*
|
||||
D22*
|
||||
X001800Y008564D02*
|
||||
X001200Y008564D01*
|
||||
X001200Y009564D02*
|
||||
X001800Y009564D01*
|
||||
X001800Y010564D02*
|
||||
X001200Y010564D01*
|
||||
X001200Y011564D02*
|
||||
X001800Y011564D01*
|
||||
X001800Y012564D02*
|
||||
X001200Y012564D01*
|
||||
X005350Y016664D02*
|
||||
X005350Y017264D01*
|
||||
X006350Y017264D02*
|
||||
X006350Y016664D01*
|
||||
X007350Y016664D02*
|
||||
X007350Y017264D01*
|
||||
X017350Y017114D02*
|
||||
X017350Y016514D01*
|
||||
X018350Y016514D02*
|
||||
X018350Y017114D01*
|
||||
D23*
|
||||
X016613Y004514D03*
|
||||
X015787Y004514D03*
|
||||
D24*
|
||||
X020800Y005064D03*
|
||||
X020800Y016064D03*
|
||||
X002300Y016064D03*
|
||||
X002350Y005114D03*
|
||||
D25*
|
||||
X009250Y004064D03*
|
||||
X012100Y005314D03*
|
||||
X013500Y006864D03*
|
||||
X015650Y006264D03*
|
||||
X015200Y004514D03*
|
||||
X013550Y008764D03*
|
||||
X013350Y010114D03*
|
||||
X013300Y011464D03*
|
||||
X011650Y013164D03*
|
||||
X010000Y015114D03*
|
||||
X006500Y013714D03*
|
||||
X004150Y011564D03*
|
||||
X014250Y014964D03*
|
||||
X015850Y009914D03*
|
||||
M02*
|
|
@ -0,0 +1,7 @@
|
|||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
X0Y-1000D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
G04 Test drawing with circular apertures*
|
||||
G04 Hand coded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10C,0.050*%
|
||||
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
|
||||
G04 drawing with it (only should show in flashes)*
|
||||
%ADD11C,0.075X0.050*%
|
||||
|
||||
G04 No hole, centered at 0,0 *
|
||||
G54D10*
|
||||
G04 Recenter to 0,0
|
||||
G01X0Y0D02*
|
||||
G04 Draw a line segment*
|
||||
X00100Y0D01*
|
||||
G04 Turn off for a segment*
|
||||
X00200Y0D02*
|
||||
G04 Draw another line at angle*
|
||||
G54D11*
|
||||
X00300Y00100D01*
|
||||
G04 Turn off for a segment*
|
||||
X0Y00100D02*
|
||||
G54D10*
|
||||
G04 Turn on circular interpolation*
|
||||
G75*
|
||||
G03X0Y00300I0J00100D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00500Y00D02*
|
||||
G04 Draw a larger radius arc*
|
||||
G03X00350Y00150I-00250J-00050D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00250Y00200D02*
|
||||
G04 Draw a larger clockwise radius arc*
|
||||
G02X00350Y00350I00250J-00050D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,27 @@
|
|||
G04 Test flashing of circular apertures*
|
||||
G04 Four groups of circular apertures are arranged in a square*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10C,0.050*%
|
||||
%ADD11C,0.050X0.025*%
|
||||
%ADD12C,0.050X0.025X0.030*%
|
||||
|
||||
G04 No hole, centered at 0,0 *
|
||||
G54D10*
|
||||
X0Y0D03*
|
||||
|
||||
G04 Round hole, centered at 0.1,0 *
|
||||
G54D11*
|
||||
X00100Y0D03*
|
||||
|
||||
G04 Square hole, centered at 0,0.1 *
|
||||
G54D12*
|
||||
X0Y00100D03*
|
||||
|
||||
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
|
||||
G54D11*
|
||||
X00100Y00090D03*
|
||||
X00100Y00110D03*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,38 @@
|
|||
G04 Test drawing with rectangular apertures*
|
||||
G04 Hand coded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10O,0.050X0.025*%
|
||||
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
|
||||
G04 drawing with it (only should show in flashes)*
|
||||
%ADD11O,0.075X0.050X0.025*%
|
||||
|
||||
G04 No hole, centered at 0,0 *
|
||||
G54D10*
|
||||
G04 Recenter to 0,0
|
||||
G01X0Y0D02*
|
||||
G04 Draw a line segment*
|
||||
X00100Y0D01*
|
||||
G04 Turn off for a segment*
|
||||
X00200Y0D02*
|
||||
G04 Draw another line at angle*
|
||||
G54D11*
|
||||
X00300Y00100D01*
|
||||
G04 Turn off for a segment*
|
||||
X0Y00100D02*
|
||||
G54D10*
|
||||
G04 Turn on circular interpolation*
|
||||
G75*
|
||||
G03X0Y00300I0J00100D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00500Y00D02*
|
||||
G04 Draw a larger radius arc*
|
||||
G03X00350Y00150I-00250J-00050D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00250Y00200D02*
|
||||
G04 Draw a larger clockwise radius arc*
|
||||
G02X00350Y00350I00250J-00050D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,27 @@
|
|||
G04 Test flashing of obround apertures*
|
||||
G04 Four groups of obround apertures are arranged in a square*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10O,0.050X0.080*%
|
||||
%ADD11O,0.080X0.050X0.025*%
|
||||
%ADD12O,0.050X0.025X0.025X0.0150*%
|
||||
|
||||
G04 No hole, centered at 0,0 *
|
||||
G54D10*
|
||||
X0Y0D03*
|
||||
|
||||
G04 Round hole, centered at 0.1,0 *
|
||||
G54D11*
|
||||
X00100Y0D03*
|
||||
|
||||
G04 Square hole, centered at 0,0.1 *
|
||||
G54D12*
|
||||
X0Y00100D03*
|
||||
|
||||
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
|
||||
G54D11*
|
||||
X00100Y00090D03*
|
||||
X00100Y00110D03*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,38 @@
|
|||
G04 Test drawing with polygon apertures*
|
||||
G04 Hand coded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10O,0.050X0.025*%
|
||||
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
|
||||
G04 drawing with it (only should show in flashes)*
|
||||
%ADD11O,0.075X0.050X0.025*%
|
||||
|
||||
G04 No hole, centered at 0,0 *
|
||||
G54D10*
|
||||
G04 Recenter to 0,0
|
||||
G01X0Y0D02*
|
||||
G04 Draw a line segment*
|
||||
X00100Y0D01*
|
||||
G04 Turn off for a segment*
|
||||
X00200Y0D02*
|
||||
G04 Draw another line at angle*
|
||||
G54D11*
|
||||
X00300Y00100D01*
|
||||
G04 Turn off for a segment*
|
||||
X0Y00100D02*
|
||||
G54D10*
|
||||
G04 Turn on circular interpolation*
|
||||
G75*
|
||||
G03X0Y00300I0J00100D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00500Y00D02*
|
||||
G04 Draw a larger radius arc*
|
||||
G03X00350Y00150I-00250J-00050D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00250Y00200D02*
|
||||
G04 Draw a larger clockwise radius arc*
|
||||
G02X00350Y00350I00250J-00050D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,27 @@
|
|||
G04 Test flashing of polygon apertures*
|
||||
G04 Four groups of polygon apertures are arranged in a square*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10P,0.050X3*%
|
||||
%ADD11P,0.050X6X-45X0.035*%
|
||||
%ADD12P,0.040X10X25X0.025X0.025X0.0150*%
|
||||
|
||||
G04 Triangle, centered at 0,0 *
|
||||
G54D10*
|
||||
X0Y0D03*
|
||||
|
||||
G04 Hexagon with round hole rotate 45 degreed ccwise, centered at 0.1,0 *
|
||||
G54D11*
|
||||
X00100Y0D03*
|
||||
|
||||
G04 10-sided with square hole rotated 25 degrees, centered at 0,0.1 *
|
||||
G54D12*
|
||||
X0Y00100D03*
|
||||
|
||||
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
|
||||
G54D11*
|
||||
X00100Y00090D03*
|
||||
X00100Y00110D03*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,38 @@
|
|||
G04 Test drawing with rectangular apertures*
|
||||
G04 Hand coded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10R,0.050X0.025*%
|
||||
G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when*
|
||||
G04 drawing with it (only should show in flashes)*
|
||||
%ADD11R,0.075X0.050X0.025*%
|
||||
|
||||
G04 No hole, centered at 0,0 *
|
||||
G54D10*
|
||||
G04 Recenter to 0,0
|
||||
G01X0Y0D02*
|
||||
G04 Draw a line segment*
|
||||
X00100Y0D01*
|
||||
G04 Turn off for a segment*
|
||||
X00200Y0D02*
|
||||
G04 Draw another line at angle*
|
||||
G54D11*
|
||||
X00300Y00100D01*
|
||||
G04 Turn off for a segment*
|
||||
X0Y00100D02*
|
||||
G54D10*
|
||||
G04 Turn on circular interpolation*
|
||||
G75*
|
||||
G03X0Y00300I0J00100D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00500Y00D02*
|
||||
G04 Draw a larger radius arc*
|
||||
G03X00350Y00150I-00250J-00050D01*
|
||||
|
||||
G04 Turn off for a segment*
|
||||
X00250Y00200D02*
|
||||
G04 Draw a larger clockwise radius arc*
|
||||
G02X00350Y00350I00250J-00050D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,27 @@
|
|||
G04 Test flashing of rectangular apertures*
|
||||
G04 Four groups of rectangular apertures are arranged in a square*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10R,0.050X0.080*%
|
||||
%ADD11R,0.080X0.050X0.025*%
|
||||
%ADD12R,0.050X0.025X0.025X0.0150*%
|
||||
|
||||
G04 No hole, centered at 0,0 *
|
||||
G54D10*
|
||||
X0Y0D03*
|
||||
|
||||
G04 Round hole, centered at 0.1,0 *
|
||||
G54D11*
|
||||
X00100Y0D03*
|
||||
|
||||
G04 Square hole, centered at 0,0.1 *
|
||||
G54D12*
|
||||
X0Y00100D03*
|
||||
|
||||
G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 *
|
||||
G54D11*
|
||||
X00100Y00090D03*
|
||||
X00100Y00110D03*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,35 @@
|
|||
G04 Test circular interpolation*
|
||||
G04 Hand coded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10C,0.050*%
|
||||
G54D10*
|
||||
|
||||
G04 Recenter to 0,0*
|
||||
G01X0Y0D02*
|
||||
|
||||
G04 Turn on multi-quadrant mode*
|
||||
G75*
|
||||
G03X0Y00200I0J00100D01*
|
||||
|
||||
G04 Switch to quadrant mode, draw ccwise*
|
||||
G74*
|
||||
G01X00400Y0D02*
|
||||
G03X00470Y00080I0J00100D01*
|
||||
|
||||
G04 Draw things clockwise on the top two objects*
|
||||
|
||||
G04 Turn on multi-quadrant mode*
|
||||
G75*
|
||||
G01X00100Y00300D02*
|
||||
G02X00100Y00500I0J00100D01*
|
||||
|
||||
G04 Switch to quadrant mode, draw clockwise*
|
||||
G04 Note: since this is single quadrant mode, I and J must be*
|
||||
G04 positive, and the parser should automatically negate the J value*
|
||||
G04 to make the curve travel in the clockwise direction*
|
||||
G74*
|
||||
G01X00400Y00300D02*
|
||||
G02X00500Y00300I00150J00300D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,8 @@
|
|||
M48
|
||||
INCH,LZ
|
||||
T13C0.05
|
||||
%
|
||||
T13
|
||||
X-001000Y030000
|
||||
X00000Y03000
|
||||
X001Y03
M30
|
|
@ -0,0 +1,19 @@
|
|||
M48
|
||||
INCH,TZ
|
||||
T01C0.050
|
||||
%
|
||||
T01
|
||||
X0000Y0000
|
||||
X10000Y10000
|
||||
R5X1000
|
||||
X20000Y10000
|
||||
R5Y1000
|
||||
X30000Y10000
|
||||
R5X1000Y1500
|
||||
X10000Y00000
|
||||
R5X-1000
|
||||
X20000Y00000
|
||||
R5Y-1000
|
||||
X30000Y00000
|
||||
R5X-1000Y-1500
|
||||
M30
|
|
@ -0,0 +1,8 @@
|
|||
M48
|
||||
INCH,TZ
|
||||
T13C0.05
|
||||
%
|
||||
T13
|
||||
X-001000Y030000
|
||||
X0Y030000
|
||||
X01000Y30000
M30
|
|
@ -0,0 +1,19 @@
|
|||
G04 Test image justify 1*
|
||||
G04 Crosshairs should be justified to the X axis *
|
||||
G04 and 0.5 inches offset from Y axis *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%IJB.5*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Crosshairs *
|
||||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
|
||||
X0Y-1000D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,19 @@
|
|||
G04 Test image justify 2*
|
||||
G04 Crosshairs should be centered in X and Y (platen size *
|
||||
G04 is assumed to be 2x the overall size of the image) *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%IJACBC*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Crosshairs *
|
||||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
|
||||
X0Y-1000D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,18 @@
|
|||
G04 Test image polarity *
|
||||
G04 Crosshairs should be centered on 0,0 in final rendering*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%IOA-2.0B-1.0*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Crosshairs to be on 0,0 *
|
||||
X1000Y1000D02*
|
||||
G54D10*
|
||||
X3000Y1000D01*
|
||||
|
||||
X2000Y0D02*
|
||||
G54D10*
|
||||
X2000Y2000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,19 @@
|
|||
G04 Test image offset uses current units *
|
||||
G04 Crosshairs should be centered on 0,0 in final rendering*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOMM*%
|
||||
%FSLAX23Y23*%
|
||||
%IOB-25.4*%
|
||||
%MOIN*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Crosshairs to be on 0,0 *
|
||||
X-1000Y1000D02*
|
||||
G54D10*
|
||||
X1000Y1000D01*
|
||||
|
||||
X0Y0D02*
|
||||
G54D10*
|
||||
X0Y2000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,17 @@
|
|||
G04 Test image polarity *
|
||||
G04 Crosshairs should be cut out of a positive background*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%IPNEG*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Draw crosshairs *
|
||||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
X0Y-1000D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,21 @@
|
|||
G04 Test image rotation *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%IR270*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Quarter star *
|
||||
X1000Y0D02*
|
||||
G54D10*
|
||||
X2000Y0D01*
|
||||
|
||||
X1000Y0D02*
|
||||
G54D10*
|
||||
X2000Y1000D01*
|
||||
|
||||
X1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,11 @@
|
|||
G04 Test include file 1 *
|
||||
G04 Crosshairs should be drawn at 0,0 in final rendering*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%IOA-2.0B-1.0*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Crosshairs to be on 0,0 *
|
||||
%IFinclude-file-1.gbx*%
|
||||
M02*
|
|
@ -0,0 +1,15 @@
|
|||
G04 Test layer axis select *
|
||||
G04 Line is drawn along A axis, then axis select switches it and renders *
|
||||
G04 line along y axis *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ASAYBX*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Draw line *
|
||||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,28 @@
|
|||
G04 Test layer knockout 1*
|
||||
G04 A cleared 3x3 square should surround the crosshairs *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Create a large dark area *
|
||||
G36*
|
||||
X-2000Y-2000D02*
|
||||
X2000Y-2000D01*
|
||||
X2000Y2000D01*
|
||||
X-2000Y2000D01*
|
||||
X-2000Y-2000D01*
|
||||
G37*
|
||||
|
||||
G04 Create the knockout region *
|
||||
%KOCX-1.5Y-1.5I3J3*%
|
||||
|
||||
G04 Draw crosshairs *
|
||||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
X0Y-1000D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,28 @@
|
|||
G04 Test layer knockout 2*
|
||||
G04 A cleared 0.5 inch border should surround the crosshairs *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Create a large dark area *
|
||||
G36*
|
||||
X-2000Y-2000D02*
|
||||
X2000Y-2000D01*
|
||||
X2000Y2000D01*
|
||||
X-2000Y2000D01*
|
||||
X-2000Y-2000D01*
|
||||
G37*
|
||||
|
||||
G04 Create the knockout region *
|
||||
%KOCK0.5*%
|
||||
|
||||
G04 Draw crosshairs *
|
||||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
X0Y-1000D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,23 @@
|
|||
G04 Test layer mirror image 1 *
|
||||
G04 Quarter star is drawn pointing towards +X, +Y. Mirror
|
||||
G04 flips around the Y axis and the star should point towards -X, -Y *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%MIA1B1*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Draw quarter star *
|
||||
X0Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
|
||||
X0Y0D02*
|
||||
G54D10*
|
||||
X1000Y1000D01*
|
||||
|
||||
X0Y0D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,68 @@
|
|||
G04 Test handling of unit changes within a RS274X file *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
G04 Aperture 10 should be in Inches *
|
||||
%ADD10C,0.050*%
|
||||
%MOMM*%
|
||||
G04 Aperture 11 should be in MMs *
|
||||
%ADD11C,1.250*%
|
||||
G04 Aperture 12 should be in MMs *
|
||||
%AMTHERMAL*
|
||||
7,0,0,25.4,12.7,2.54,0*%
|
||||
%MOIN*%
|
||||
G04 Aperture 13 is in inches *
|
||||
%AMTHERMALTWO*
|
||||
7,0,0,1,0.5,0.1,0*%
|
||||
%MOMM*%
|
||||
%ADD12THERMAL*%
|
||||
%MOIN*%
|
||||
%ADD13THERMALTWO*%
|
||||
|
||||
%MOIN*%
|
||||
G04 Box 1, using aperture 10*
|
||||
X0Y0D02*
|
||||
G54D10*
|
||||
X0Y0D01*
|
||||
X1000D01*
|
||||
Y1000D01*
|
||||
X0D01*
|
||||
Y0D01*
|
||||
|
||||
G04 Box 2, using aperture 11*
|
||||
X2000Y0D02*
|
||||
G54D11*
|
||||
X2000Y0D01*
|
||||
X3000D01*
|
||||
Y1000D01*
|
||||
X2000D01*
|
||||
Y0D01*
|
||||
|
||||
%MOMM*%
|
||||
G04 Box 3, using aperture 10*
|
||||
X100000Y0D02*
|
||||
G54D10*
|
||||
X100000Y0D01*
|
||||
X125000D01*
|
||||
Y25000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
|
||||
G04 Draw Thermal in box 1*
|
||||
G54D12*
|
||||
Y12000X12700D03*
|
||||
|
||||
G04 Draw Thermal in box 2*
|
||||
G04 ..switch to inches for coordinates*
|
||||
G70*
|
||||
Y500X2500D02*
|
||||
G54D12*
|
||||
Y500X2500D03*
|
||||
|
||||
G04 ..switch to mms for coordinates*
|
||||
G71*
|
||||
G04 Draw Thermal in box 3*
|
||||
G54D13*
|
||||
Y12000X112000D03*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,18 @@
|
|||
G04 Test layer offset 1 *
|
||||
G04 Crosshairs should be centered on 0,0*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%OFA-2.0B-1.0*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Crosshairs to be on 0,0 *
|
||||
X1000Y1000D02*
|
||||
G54D10*
|
||||
X3000Y1000D01*
|
||||
|
||||
X2000Y0D02*
|
||||
G54D10*
|
||||
X2000Y2000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,23 @@
|
|||
G04 Test layer rotation 1 *
|
||||
G04 Quarter star should be rotated 45 degrees counterclockwise, pointing*
|
||||
G04 the center line straight up *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%RO45*%
|
||||
%ADD10C,0.025*%
|
||||
|
||||
G04 Quarter star *
|
||||
X1000Y0D02*
|
||||
G54D10*
|
||||
X2000Y0D01*
|
||||
|
||||
X1000Y0D02*
|
||||
G54D10*
|
||||
X2000Y1000D01*
|
||||
|
||||
X1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,17 @@
|
|||
G04 Test layer scale factor 1 *
|
||||
G04 Crosshairs should be centered on 0,0 and 2 inches wide and 1 inch tall*
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%SFA2B1*%
|
||||
%ADD10C,0.025*%
|
||||
|
||||
G04 Crosshairs to be on 0,0 *
|
||||
X-500Y0D02*
|
||||
G54D10*
|
||||
X500Y0D01*
|
||||
X0Y-500D02*
|
||||
G54D10*
|
||||
X0Y500D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,17 @@
|
|||
G04 Test step and repeat 1*
|
||||
G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%SRX3Y2I5.0J2*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Draw crosshairs *
|
||||
X-1000Y0D02*
|
||||
G54D10*
|
||||
X1000Y0D01*
|
||||
X0Y-1000D02*
|
||||
G54D10*
|
||||
X0Y1000D01*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,18 @@
|
|||
G04 Test step and repeat 1*
|
||||
G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y *
|
||||
G04 Handcoded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%SRX3Y2I1J1*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Draw a simple square*
|
||||
G36*
|
||||
G01X00400Y0D02*
|
||||
X00600Y0D01*
|
||||
X00600Y00200D01*
|
||||
X00400Y00200D01*
|
||||
X00400Y0D01*
|
||||
G37*
|
||||
|
||||
M02*
|
|
@ -0,0 +1,45 @@
|
|||
G04 Test drawing with polygon apertures*
|
||||
G04 Four small polygon fills aranged in a square
|
||||
G04 Hand coded by Julian Lamb *
|
||||
%MOIN*%
|
||||
%FSLAX23Y23*%
|
||||
%ADD10C,0.050*%
|
||||
|
||||
G04 Draw a rectangle with a rounded right side*
|
||||
G36*
|
||||
G01X0Y0D02*
|
||||
X00200Y0D01*
|
||||
G75*
|
||||
G03X00200Y00200I0J00100D01*
|
||||
X0Y00200D01*
|
||||
G04 Do not close with a final line, so let gerbv automatically close*
|
||||
G37*
|
||||
|
||||
G04 Draw a simple square*
|
||||
G36*
|
||||
G01X00400Y0D02*
|
||||
X00600Y0D01*
|
||||
X00600Y00200D01*
|
||||
X00400Y00200D01*
|
||||
X00400Y0D01*
|
||||
G37*
|
||||
|
||||
G04 Draw a small diamond*
|
||||
G36*
|
||||
G01X00100Y00300D02*
|
||||
X00200Y00400D01*
|
||||
X00100Y00500D01*
|
||||
X0Y00400D01*
|
||||
X00100Y00300D01*
|
||||
G37*
|
||||
|
||||
G04 Draw a very-narrow slit*
|
||||
G36*
|
||||
G01X00500Y00300D02*
|
||||
X00510Y00300D01*
|
||||
X00510Y00500D01*
|
||||
X00500Y00500D01*
|
||||
X00500Y00300D01*
|
||||
G37*
|
||||
|
||||
M02*
|
Po Szerokość: | Wysokość: | Rozmiar: 49 KiB |
|
@ -0,0 +1,53 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
"""
|
||||
This example demonstrates the use of pcb-tools with cairo to render composite
|
||||
images using the PCB interface
|
||||
"""
|
||||
|
||||
import os
|
||||
from gerber import PCB
|
||||
from gerber.render import theme
|
||||
from gerber.render.cairo_backend import GerberCairoContext
|
||||
|
||||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
|
||||
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
# Create a new PCB instance
|
||||
pcb = PCB.from_directory(GERBER_FOLDER)
|
||||
|
||||
# Render PCB top view
|
||||
ctx.render_layers(pcb.top_layers,
|
||||
os.path.join(os.path.dirname(__file__), 'pcb_top.png',),
|
||||
theme.THEMES['OSH Park'], max_width=800, max_height=600)
|
||||
|
||||
# Render PCB bottom view
|
||||
ctx.render_layers(pcb.bottom_layers,
|
||||
os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'),
|
||||
theme.THEMES['OSH Park'], max_width=800, max_height=600)
|
||||
|
||||
# Render copper layers only
|
||||
ctx.render_layers(pcb.copper_layers + pcb.drill_layers,
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
'pcb_transparent_copper.png'),
|
||||
theme.THEMES['Transparent Copper'], max_width=800, max_height=600)
|
||||
|
Po Szerokość: | Wysokość: | Rozmiar: 102 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 108 KiB |
|
@ -0,0 +1,59 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
"""
|
||||
This example renders the gerber files from the gerbv test suite
|
||||
"""
|
||||
|
||||
import os
|
||||
from gerber.rs274x import read as gerber_read
|
||||
from gerber.excellon import read as excellon_read
|
||||
from gerber.render import GerberCairoContext
|
||||
from gerber.utils import listdir
|
||||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbv_test_files'))
|
||||
|
||||
if not os.path.isdir(os.path.join(os.path.dirname(__file__), 'outputs')):
|
||||
os.mkdir(os.path.join(os.path.dirname(__file__), 'outputs'))
|
||||
|
||||
for infile in listdir(GERBER_FOLDER):
|
||||
if infile.startswith('test'):
|
||||
try:
|
||||
outfile = os.path.splitext(infile)[0] + '.png'
|
||||
if infile.endswith('gbx'):
|
||||
layer = gerber_read(os.path.join(GERBER_FOLDER, infile))
|
||||
print("Loaded Gerber file: {}".format(infile))
|
||||
elif infile.endswith('exc'):
|
||||
layer = excellon_read(os.path.join(GERBER_FOLDER, infile))
|
||||
print("Loaded Excellon file: {}".format(infile))
|
||||
else:
|
||||
continue
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext(1200)
|
||||
ctx.color = (80./255, 80/255., 154/255.)
|
||||
ctx.drill_color = ctx.color
|
||||
|
||||
# Draw the layer, and specify the rendering settings to use
|
||||
layer.render(ctx)
|
||||
|
||||
# Write output to png file
|
||||
print("Writing output to: {}".format(outfile))
|
||||
ctx.dump(os.path.join(os.path.dirname(__file__), 'outputs', outfile))
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
traceback.print_exc()
|
|
@ -0,0 +1,28 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Gerber Tools
|
||||
============
|
||||
**Gerber Tools**
|
||||
|
||||
gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon
|
||||
files in python.
|
||||
"""
|
||||
|
||||
from .common import read, loads
|
||||
from .layers import load_layer, load_layer_data
|
||||
from .pcb import PCB
|
|
@ -0,0 +1,122 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from .render import available_renderers
|
||||
from .render import theme
|
||||
from .pcb import PCB
|
||||
from . import load_layer
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Render gerber files to image',
|
||||
prog='gerber-render'
|
||||
)
|
||||
parser.add_argument(
|
||||
'filenames', metavar='FILENAME', type=str, nargs='+',
|
||||
help='Gerber files to render. If a directory is provided, it should '
|
||||
'be provided alone and should contain the gerber files for a '
|
||||
'single PCB.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--outfile', '-o', type=str, nargs='?', default='out',
|
||||
help="Output Filename (extension will be added automatically)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--backend', '-b', choices=available_renderers.keys(), default='cairo',
|
||||
help='Choose the backend to use to generate the output.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--theme', '-t', choices=theme.THEMES.keys(), default='default',
|
||||
help='Select render theme.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--width', type=int, default=1920, help='Maximum width.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--height', type=int, default=1080, help='Maximum height.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose', '-v', action='store_true', default=False,
|
||||
help='Increase verbosity of the output.'
|
||||
)
|
||||
# parser.add_argument(
|
||||
# '--quick', '-q', action='store_true', default=False,
|
||||
# help='Skip longer running rendering steps to produce lower quality'
|
||||
# ' output faster. This only has an effect for the freecad backend.'
|
||||
# )
|
||||
# parser.add_argument(
|
||||
# '--nox', action='store_true', default=False,
|
||||
# help='Run without using any GUI elements. This may produce suboptimal'
|
||||
# 'output. For the freecad backend, colors, transparancy, and '
|
||||
# 'visibility cannot be set without a GUI instance.'
|
||||
# )
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
renderer = available_renderers[args.backend]()
|
||||
|
||||
if args.backend in ['cairo', ]:
|
||||
outext = 'png'
|
||||
else:
|
||||
outext = None
|
||||
|
||||
if os.path.exists(args.filenames[0]) and os.path.isdir(args.filenames[0]):
|
||||
directory = args.filenames[0]
|
||||
pcb = PCB.from_directory(directory)
|
||||
|
||||
if args.backend in ['cairo', ]:
|
||||
top = pcb.top_layers
|
||||
bottom = pcb.bottom_layers
|
||||
copper = pcb.copper_layers
|
||||
|
||||
outline = pcb.outline_layer
|
||||
if outline:
|
||||
top = [outline] + top
|
||||
bottom = [outline] + bottom
|
||||
copper = [outline] + copper + pcb.drill_layers
|
||||
|
||||
renderer.render_layers(
|
||||
layers=top, theme=theme.THEMES[args.theme],
|
||||
max_height=args.height, max_width=args.width,
|
||||
filename='{0}.top.{1}'.format(args.outfile, outext)
|
||||
)
|
||||
renderer.render_layers(
|
||||
layers=bottom, theme=theme.THEMES[args.theme],
|
||||
max_height=args.height, max_width=args.width,
|
||||
filename='{0}.bottom.{1}'.format(args.outfile, outext)
|
||||
)
|
||||
renderer.render_layers(
|
||||
layers=copper, theme=theme.THEMES['Transparent Multilayer'],
|
||||
max_height=args.height, max_width=args.width,
|
||||
filename='{0}.copper.{1}'.format(args.outfile, outext))
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
filenames = args.filenames
|
||||
for filename in filenames:
|
||||
layer = load_layer(filename)
|
||||
settings = theme.THEMES[args.theme].get(layer.layer_class, None)
|
||||
renderer.render_layer(layer, settings=settings)
|
||||
renderer.dump(filename='{0}.{1}'.format(args.outfile, outext))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
""" This module provides RS-274-X AM macro evaluation.
|
||||
"""
|
||||
|
||||
|
||||
class OpCode:
|
||||
PUSH = 1
|
||||
LOAD = 2
|
||||
STORE = 3
|
||||
ADD = 4
|
||||
SUB = 5
|
||||
MUL = 6
|
||||
DIV = 7
|
||||
PRIM = 8
|
||||
|
||||
@staticmethod
|
||||
def str(opcode):
|
||||
if opcode == OpCode.PUSH:
|
||||
return "OPCODE_PUSH"
|
||||
elif opcode == OpCode.LOAD:
|
||||
return "OPCODE_LOAD"
|
||||
elif opcode == OpCode.STORE:
|
||||
return "OPCODE_STORE"
|
||||
elif opcode == OpCode.ADD:
|
||||
return "OPCODE_ADD"
|
||||
elif opcode == OpCode.SUB:
|
||||
return "OPCODE_SUB"
|
||||
elif opcode == OpCode.MUL:
|
||||
return "OPCODE_MUL"
|
||||
elif opcode == OpCode.DIV:
|
||||
return "OPCODE_DIV"
|
||||
elif opcode == OpCode.PRIM:
|
||||
return "OPCODE_PRIM"
|
||||
else:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def eval_macro(instructions, parameters={}):
|
||||
|
||||
if not isinstance(parameters, type({})):
|
||||
p = {}
|
||||
for i, val in enumerate(parameters):
|
||||
p[i + 1] = val
|
||||
|
||||
parameters = p
|
||||
|
||||
stack = []
|
||||
|
||||
def pop():
|
||||
return stack.pop()
|
||||
|
||||
def push(op):
|
||||
stack.append(op)
|
||||
|
||||
def top():
|
||||
return stack[-1]
|
||||
|
||||
def empty():
|
||||
return len(stack) == 0
|
||||
|
||||
for opcode, argument in instructions:
|
||||
if opcode == OpCode.PUSH:
|
||||
push(argument)
|
||||
|
||||
elif opcode == OpCode.LOAD:
|
||||
push(parameters.get(argument, 0))
|
||||
|
||||
elif opcode == OpCode.STORE:
|
||||
parameters[argument] = pop()
|
||||
|
||||
elif opcode == OpCode.ADD:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(op2 + op1)
|
||||
|
||||
elif opcode == OpCode.SUB:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(op2 - op2)
|
||||
|
||||
elif opcode == OpCode.MUL:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(op2 * op1)
|
||||
|
||||
elif opcode == OpCode.DIV:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(op2 / op1)
|
||||
|
||||
elif opcode == OpCode.PRIM:
|
||||
yield "%d,%s" % (argument, ",".join([str(x) for x in stack]))
|
||||
stack = []
|
|
@ -0,0 +1,255 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
""" This module provides RS-274-X AM macro modifiers parsing.
|
||||
"""
|
||||
|
||||
from .am_eval import OpCode, eval_macro
|
||||
|
||||
import string
|
||||
|
||||
|
||||
class Token:
|
||||
ADD = "+"
|
||||
SUB = "-"
|
||||
# compatibility as many gerber writes do use non compliant X
|
||||
MULT = ("x", "X")
|
||||
DIV = "/"
|
||||
OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV)
|
||||
LEFT_PARENS = "("
|
||||
RIGHT_PARENS = ")"
|
||||
EQUALS = "="
|
||||
EOF = "EOF"
|
||||
|
||||
|
||||
def token_to_opcode(token):
|
||||
if token == Token.ADD:
|
||||
return OpCode.ADD
|
||||
elif token == Token.SUB:
|
||||
return OpCode.SUB
|
||||
elif token in Token.MULT:
|
||||
return OpCode.MUL
|
||||
elif token == Token.DIV:
|
||||
return OpCode.DIV
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def precedence(token):
|
||||
if token == Token.ADD or token == Token.SUB:
|
||||
return 1
|
||||
elif token in Token.MULT or token == Token.DIV:
|
||||
return 2
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def is_op(token):
|
||||
return token in Token.OPERATORS
|
||||
|
||||
|
||||
class Scanner:
|
||||
|
||||
def __init__(self, s):
|
||||
self.buff = s
|
||||
self.n = 0
|
||||
|
||||
def eof(self):
|
||||
return self.n == len(self.buff)
|
||||
|
||||
def peek(self):
|
||||
if not self.eof():
|
||||
return self.buff[self.n]
|
||||
|
||||
return Token.EOF
|
||||
|
||||
def ungetc(self):
|
||||
if self.n > 0:
|
||||
self.n -= 1
|
||||
|
||||
def getc(self):
|
||||
if self.eof():
|
||||
return ""
|
||||
|
||||
c = self.buff[self.n]
|
||||
self.n += 1
|
||||
return c
|
||||
|
||||
def readint(self):
|
||||
n = ""
|
||||
while not self.eof() and (self.peek() in string.digits):
|
||||
n += self.getc()
|
||||
return int(n)
|
||||
|
||||
def readfloat(self):
|
||||
n = ""
|
||||
while not self.eof() and (self.peek() in string.digits or self.peek() == "."):
|
||||
n += self.getc()
|
||||
# weird case where zero is ommited inthe last modifider, like in ',0.'
|
||||
if n == ".":
|
||||
return 0
|
||||
return float(n)
|
||||
|
||||
def readstr(self, end="*"):
|
||||
s = ""
|
||||
while not self.eof() and self.peek() != end:
|
||||
s += self.getc()
|
||||
return s.strip()
|
||||
|
||||
|
||||
def print_instructions(instructions):
|
||||
for opcode, argument in instructions:
|
||||
print("%s %s" % (OpCode.str(opcode),
|
||||
str(argument) if argument is not None else ""))
|
||||
|
||||
|
||||
def read_macro(macro):
|
||||
instructions = []
|
||||
|
||||
for block in macro.split("*"):
|
||||
|
||||
is_primitive = False
|
||||
is_equation = False
|
||||
|
||||
found_equation_left_side = False
|
||||
found_primitive_code = False
|
||||
|
||||
equation_left_side = 0
|
||||
primitive_code = 0
|
||||
|
||||
unary_minus_allowed = False
|
||||
unary_minus = False
|
||||
|
||||
if Token.EQUALS in block:
|
||||
is_equation = True
|
||||
else:
|
||||
is_primitive = True
|
||||
|
||||
scanner = Scanner(block)
|
||||
|
||||
# inlined here for compactness and convenience
|
||||
op_stack = []
|
||||
|
||||
def pop():
|
||||
return op_stack.pop()
|
||||
|
||||
def push(op):
|
||||
op_stack.append(op)
|
||||
|
||||
def top():
|
||||
return op_stack[-1]
|
||||
|
||||
def empty():
|
||||
return len(op_stack) == 0
|
||||
|
||||
while not scanner.eof():
|
||||
|
||||
c = scanner.getc()
|
||||
|
||||
if c == ",":
|
||||
found_primitive_code = True
|
||||
|
||||
# add all instructions on the stack to finish last modifier
|
||||
while not empty():
|
||||
instructions.append((token_to_opcode(pop()), None))
|
||||
|
||||
unary_minus_allowed = True
|
||||
|
||||
elif c in Token.OPERATORS:
|
||||
if c == Token.SUB and unary_minus_allowed:
|
||||
unary_minus = True
|
||||
unary_minus_allowed = False
|
||||
continue
|
||||
|
||||
while not empty() and is_op(top()) and precedence(top()) >= precedence(c):
|
||||
instructions.append((token_to_opcode(pop()), None))
|
||||
|
||||
push(c)
|
||||
|
||||
elif c == Token.LEFT_PARENS:
|
||||
push(c)
|
||||
|
||||
elif c == Token.RIGHT_PARENS:
|
||||
while not empty() and top() != Token.LEFT_PARENS:
|
||||
instructions.append((token_to_opcode(pop()), None))
|
||||
|
||||
if empty():
|
||||
raise ValueError("unbalanced parentheses")
|
||||
|
||||
# discard "("
|
||||
pop()
|
||||
|
||||
elif c.startswith("$"):
|
||||
n = scanner.readint()
|
||||
|
||||
if is_equation and not found_equation_left_side:
|
||||
equation_left_side = n
|
||||
else:
|
||||
instructions.append((OpCode.LOAD, n))
|
||||
|
||||
elif c == Token.EQUALS:
|
||||
found_equation_left_side = True
|
||||
|
||||
elif c == "0":
|
||||
if is_primitive and not found_primitive_code:
|
||||
instructions.append((OpCode.PUSH, scanner.readstr("*")))
|
||||
found_primitive_code = True
|
||||
else:
|
||||
# decimal or integer disambiguation
|
||||
if scanner.peek() not in '.' or scanner.peek() == Token.EOF:
|
||||
instructions.append((OpCode.PUSH, 0))
|
||||
|
||||
elif c in "123456789.":
|
||||
scanner.ungetc()
|
||||
|
||||
if is_primitive and not found_primitive_code:
|
||||
primitive_code = scanner.readint()
|
||||
else:
|
||||
n = scanner.readfloat()
|
||||
if unary_minus:
|
||||
unary_minus = False
|
||||
n *= -1
|
||||
|
||||
instructions.append((OpCode.PUSH, n))
|
||||
else:
|
||||
# whitespace or unknown char
|
||||
pass
|
||||
|
||||
# add all instructions on the stack to finish last modifier (if any)
|
||||
while not empty():
|
||||
instructions.append((token_to_opcode(pop()), None))
|
||||
|
||||
# at end, we either have a primitive or a equation
|
||||
if is_primitive and found_primitive_code:
|
||||
instructions.append((OpCode.PRIM, primitive_code))
|
||||
|
||||
if is_equation:
|
||||
instructions.append((OpCode.STORE, equation_left_side))
|
||||
|
||||
return instructions
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
instructions = read_macro(sys.argv[1])
|
||||
|
||||
print("insructions:")
|
||||
print_instructions(instructions)
|
||||
|
||||
print("eval:")
|
||||
for primitive in eval_macro(instructions):
|
||||
print(primitive)
|
|
@ -0,0 +1,286 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
CAM File
|
||||
============
|
||||
**AM file classes**
|
||||
|
||||
This module provides common base classes for Excellon/Gerber CNC files
|
||||
"""
|
||||
|
||||
|
||||
class FileSettings(object):
|
||||
""" CAM File Settings
|
||||
|
||||
Provides a common representation of gerber/excellon file settings
|
||||
|
||||
Parameters
|
||||
----------
|
||||
notation: string
|
||||
notation format. either 'absolute' or 'incremental'
|
||||
|
||||
units : string
|
||||
Measurement units. 'inch' or 'metric'
|
||||
|
||||
zero_suppression: string
|
||||
'leading' to suppress leading zeros, 'trailing' to suppress trailing zeros.
|
||||
This is the convention used in Gerber files.
|
||||
|
||||
format : tuple (int, int)
|
||||
Decimal format
|
||||
|
||||
zeros : string
|
||||
'leading' to include leading zeros, 'trailing to include trailing zeros.
|
||||
This is the convention used in Excellon files
|
||||
|
||||
Notes
|
||||
-----
|
||||
Either `zeros` or `zero_suppression` should be specified, there is no need to
|
||||
specify both. `zero_suppression` will take on the opposite value of `zeros`
|
||||
and vice versa
|
||||
"""
|
||||
|
||||
def __init__(self, notation='absolute', units='inch',
|
||||
zero_suppression=None, format=(2, 5), zeros=None,
|
||||
angle_units='degrees'):
|
||||
if notation not in ['absolute', 'incremental']:
|
||||
raise ValueError('Notation must be either absolute or incremental')
|
||||
self.notation = notation
|
||||
|
||||
if units not in ['inch', 'metric']:
|
||||
raise ValueError('Units must be either inch or metric')
|
||||
self.units = units
|
||||
|
||||
if zero_suppression is None and zeros is None:
|
||||
self.zero_suppression = 'trailing'
|
||||
|
||||
elif zero_suppression == zeros:
|
||||
raise ValueError('Zeros and Zero Suppression must be different. \
|
||||
Best practice is to specify only one.')
|
||||
|
||||
elif zero_suppression is not None:
|
||||
if zero_suppression not in ['leading', 'trailing']:
|
||||
# This is a common problem in Eagle files, so just suppress it
|
||||
self.zero_suppression = 'leading'
|
||||
else:
|
||||
self.zero_suppression = zero_suppression
|
||||
|
||||
elif zeros is not None:
|
||||
if zeros not in ['leading', 'trailing']:
|
||||
raise ValueError('Zeros must be either leading or trailling')
|
||||
self.zeros = zeros
|
||||
|
||||
if len(format) != 2:
|
||||
raise ValueError('Format must be a tuple(n=2) of integers')
|
||||
self.format = format
|
||||
|
||||
if angle_units not in ('degrees', 'radians'):
|
||||
raise ValueError('Angle units may be degrees or radians')
|
||||
self.angle_units = angle_units
|
||||
|
||||
@property
|
||||
def zero_suppression(self):
|
||||
return self._zero_suppression
|
||||
|
||||
@zero_suppression.setter
|
||||
def zero_suppression(self, value):
|
||||
self._zero_suppression = value
|
||||
self._zeros = 'leading' if value == 'trailing' else 'trailing'
|
||||
|
||||
@property
|
||||
def zeros(self):
|
||||
return self._zeros
|
||||
|
||||
@zeros.setter
|
||||
def zeros(self, value):
|
||||
|
||||
self._zeros = value
|
||||
self._zero_suppression = 'leading' if value == 'trailing' else 'trailing'
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == 'notation':
|
||||
return self.notation
|
||||
elif key == 'units':
|
||||
return self.units
|
||||
elif key == 'zero_suppression':
|
||||
return self.zero_suppression
|
||||
elif key == 'zeros':
|
||||
return self.zeros
|
||||
elif key == 'format':
|
||||
return self.format
|
||||
elif key == 'angle_units':
|
||||
return self.angle_units
|
||||
else:
|
||||
raise KeyError()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key == 'notation':
|
||||
if value not in ['absolute', 'incremental']:
|
||||
raise ValueError('Notation must be either \
|
||||
absolute or incremental')
|
||||
self.notation = value
|
||||
elif key == 'units':
|
||||
if value not in ['inch', 'metric']:
|
||||
raise ValueError('Units must be either inch or metric')
|
||||
self.units = value
|
||||
|
||||
elif key == 'zero_suppression':
|
||||
if value not in ['leading', 'trailing']:
|
||||
raise ValueError('Zero suppression must be either leading or \
|
||||
trailling')
|
||||
self.zero_suppression = value
|
||||
|
||||
elif key == 'zeros':
|
||||
if value not in ['leading', 'trailing']:
|
||||
raise ValueError('Zeros must be either leading or trailling')
|
||||
self.zeros = value
|
||||
|
||||
elif key == 'format':
|
||||
if len(value) != 2:
|
||||
raise ValueError('Format must be a tuple(n=2) of integers')
|
||||
self.format = value
|
||||
|
||||
elif key == 'angle_units':
|
||||
if value not in ('degrees', 'radians'):
|
||||
raise ValueError('Angle units may be degrees or radians')
|
||||
self.angle_units = value
|
||||
|
||||
else:
|
||||
raise KeyError('%s is not a valid key' % key)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.notation == other.notation and
|
||||
self.units == other.units and
|
||||
self.zero_suppression == other.zero_suppression and
|
||||
self.format == other.format and
|
||||
self.angle_units == other.angle_units)
|
||||
|
||||
def __str__(self):
|
||||
return ('<Settings: %s %s %s %s %s>' %
|
||||
(self.units, self.notation, self.zero_suppression, self.format, self.angle_units))
|
||||
|
||||
|
||||
class CamFile(object):
|
||||
""" Base class for Gerber/Excellon files.
|
||||
|
||||
Provides a common set of settings parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
settings : FileSettings
|
||||
The current file configuration.
|
||||
|
||||
primitives : iterable
|
||||
List of primitives in the file.
|
||||
|
||||
filename : string
|
||||
Name of the file that this CamFile represents.
|
||||
|
||||
layer_name : string
|
||||
Name of the PCB layer that the file represents
|
||||
|
||||
Attributes
|
||||
----------
|
||||
settings : FileSettings
|
||||
File settings as a FileSettings object
|
||||
|
||||
notation : string
|
||||
File notation setting. May be either 'absolute' or 'incremental'
|
||||
|
||||
units : string
|
||||
File units setting. May be 'inch' or 'metric'
|
||||
|
||||
zero_suppression : string
|
||||
File zero-suppression setting. May be either 'leading' or 'trailling'
|
||||
|
||||
format : tuple (<int>, <int>)
|
||||
File decimal representation format as a tuple of (integer digits,
|
||||
decimal digits)
|
||||
"""
|
||||
|
||||
def __init__(self, statements=None, settings=None, primitives=None,
|
||||
filename=None, layer_name=None):
|
||||
if settings is not None:
|
||||
self.notation = settings['notation']
|
||||
self.units = settings['units']
|
||||
self.zero_suppression = settings['zero_suppression']
|
||||
self.zeros = settings['zeros']
|
||||
self.format = settings['format']
|
||||
else:
|
||||
self.notation = 'absolute'
|
||||
self.units = 'inch'
|
||||
self.zero_suppression = 'trailing'
|
||||
self.zeros = 'leading'
|
||||
self.format = (2, 5)
|
||||
self.statements = statements if statements is not None else []
|
||||
if primitives is not None:
|
||||
self.primitives = primitives
|
||||
self.filename = filename
|
||||
self.layer_name = layer_name
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
""" File settings
|
||||
|
||||
Returns
|
||||
-------
|
||||
settings : FileSettings (dict-like)
|
||||
A FileSettings object with the specified configuration.
|
||||
"""
|
||||
return FileSettings(self.notation, self.units, self.zero_suppression,
|
||||
self.format)
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
""" File boundaries
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
pass
|
||||
|
||||
def to_inch(self):
|
||||
pass
|
||||
|
||||
def to_metric(self):
|
||||
pass
|
||||
|
||||
def render(self, ctx=None, invert=False, filename=None):
|
||||
""" Generate image of layer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : :class:`GerberContext`
|
||||
GerberContext subclass used for rendering the image
|
||||
|
||||
filename : string <optional>
|
||||
If provided, save the rendered image to `filename`
|
||||
"""
|
||||
if ctx is None:
|
||||
from .render import GerberCairoContext
|
||||
ctx = GerberCairoContext()
|
||||
ctx.set_bounds(self.bounding_box)
|
||||
ctx.paint_background()
|
||||
ctx.invert = invert
|
||||
ctx.new_render_layer()
|
||||
for p in self.primitives:
|
||||
ctx.render(p)
|
||||
ctx.flatten()
|
||||
|
||||
if filename is not None:
|
||||
ctx.dump(filename)
|
|
@ -0,0 +1,71 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import rs274x
|
||||
from . import excellon
|
||||
from . import ipc356
|
||||
from .exceptions import ParseError
|
||||
from .utils import detect_file_format
|
||||
|
||||
|
||||
def read(filename):
|
||||
""" Read a gerber or excellon file and return a representative object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of the file to read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : CncFile subclass
|
||||
CncFile object representing the file, either GerberFile, ExcellonFile,
|
||||
or IPCNetlist. Returns None if file is not of the proper type.
|
||||
"""
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
return loads(data, filename)
|
||||
|
||||
|
||||
def loads(data, filename=None):
|
||||
""" Read gerber or excellon file contents from a string and return a
|
||||
representative object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
Source file contents as a string.
|
||||
|
||||
filename : string, optional
|
||||
String containing the filename of the data source.
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : CncFile subclass
|
||||
CncFile object representing the data, either GerberFile, ExcellonFile,
|
||||
or IPCNetlist. Returns None if data is not of the proper type.
|
||||
"""
|
||||
|
||||
fmt = detect_file_format(data)
|
||||
if fmt == 'rs274x':
|
||||
return rs274x.loads(data, filename=filename)
|
||||
elif fmt == 'excellon':
|
||||
return excellon.loads(data, filename=filename)
|
||||
elif fmt == 'ipc_d_356':
|
||||
return ipc356.loads(data, filename=filename)
|
||||
else:
|
||||
raise ParseError('Unable to detect file format')
|
|
@ -0,0 +1,904 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
This module provides Excellon file classes and parsing utilities
|
||||
"""
|
||||
|
||||
import math
|
||||
import operator
|
||||
|
||||
from .cam import CamFile, FileSettings
|
||||
from .excellon_statements import *
|
||||
from .excellon_tool import ExcellonToolDefinitionParser
|
||||
from .primitives import Drill, Slot
|
||||
from .utils import inch, metric
|
||||
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
|
||||
|
||||
def read(filename):
|
||||
""" Read data from filename and return an ExcellonFile
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of file to parse
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.excellon.ExcellonFile`
|
||||
An ExcellonFile created from the specified file.
|
||||
|
||||
"""
|
||||
# File object should use settings from source file by default.
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
settings = FileSettings(**detect_excellon_format(data))
|
||||
return ExcellonParser(settings).parse(filename)
|
||||
|
||||
def loads(data, filename=None, settings=None, tools=None):
|
||||
""" Read data from string and return an ExcellonFile
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon file contents
|
||||
|
||||
filename : string, optional
|
||||
string containing the filename of the data source
|
||||
|
||||
tools: dict (optional)
|
||||
externally defined tools
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.excellon.ExcellonFile`
|
||||
An ExcellonFile created from the specified file.
|
||||
|
||||
"""
|
||||
# File object should use settings from source file by default.
|
||||
if not settings:
|
||||
settings = FileSettings(**detect_excellon_format(data))
|
||||
return ExcellonParser(settings, tools).parse_raw(data, filename)
|
||||
|
||||
|
||||
class DrillHit(object):
|
||||
"""Drill feature that is a single drill hole.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
tool : ExcellonTool
|
||||
Tool to drill the hole. Defines the size of the hole that is generated.
|
||||
position : tuple(float, float)
|
||||
Center position of the drill.
|
||||
|
||||
"""
|
||||
def __init__(self, tool, position):
|
||||
self.tool = tool
|
||||
self.position = position
|
||||
|
||||
def to_inch(self):
|
||||
if self.tool.settings.units == 'metric':
|
||||
self.tool.to_inch()
|
||||
self.position = tuple(map(inch, self.position))
|
||||
|
||||
def to_metric(self):
|
||||
if self.tool.settings.units == 'inch':
|
||||
self.tool.to_metric()
|
||||
self.position = tuple(map(metric, self.position))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
position = self.position
|
||||
radius = self.tool.diameter / 2.
|
||||
|
||||
min_x = position[0] - radius
|
||||
max_x = position[0] + radius
|
||||
min_y = position[1] - radius
|
||||
max_y = position[1] + radius
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self.position = tuple(map(operator.add, self.position, (x_offset, y_offset)))
|
||||
|
||||
def __str__(self):
|
||||
return 'Hit (%f, %f) {%s}' % (self.position[0], self.position[1], self.tool)
|
||||
|
||||
class DrillSlot(object):
|
||||
"""
|
||||
A slot is created between two points. The way the slot is created depends on the statement used to create it
|
||||
"""
|
||||
|
||||
TYPE_ROUT = 1
|
||||
TYPE_G85 = 2
|
||||
|
||||
def __init__(self, tool, start, end, slot_type):
|
||||
self.tool = tool
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.slot_type = slot_type
|
||||
|
||||
def to_inch(self):
|
||||
if self.tool.settings.units == 'metric':
|
||||
self.tool.to_inch()
|
||||
self.start = tuple(map(inch, self.start))
|
||||
self.end = tuple(map(inch, self.end))
|
||||
|
||||
def to_metric(self):
|
||||
if self.tool.settings.units == 'inch':
|
||||
self.tool.to_metric()
|
||||
self.start = tuple(map(metric, self.start))
|
||||
self.end = tuple(map(metric, self.end))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
start = self.start
|
||||
end = self.end
|
||||
radius = self.tool.diameter / 2.
|
||||
min_x = min(start[0], end[0]) - radius
|
||||
max_x = max(start[0], end[0]) + radius
|
||||
min_y = min(start[1], end[1]) - radius
|
||||
max_y = max(start[1], end[1]) + radius
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self.start = tuple(map(operator.add, self.start, (x_offset, y_offset)))
|
||||
self.end = tuple(map(operator.add, self.end, (x_offset, y_offset)))
|
||||
|
||||
|
||||
class ExcellonFile(CamFile):
|
||||
""" A class representing a single excellon file
|
||||
|
||||
The ExcellonFile class represents a single excellon file.
|
||||
|
||||
http://www.excellon.com/manuals/program.htm
|
||||
(archived version at https://web.archive.org/web/20150920001043/http://www.excellon.com/manuals/program.htm)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tools : list
|
||||
list of gerber file statements
|
||||
|
||||
hits : list of tuples
|
||||
list of drill hits as (<Tool>, (x, y))
|
||||
|
||||
settings : dict
|
||||
Dictionary of gerber file settings
|
||||
|
||||
filename : string
|
||||
Filename of the source gerber file
|
||||
|
||||
Attributes
|
||||
----------
|
||||
units : string
|
||||
either 'inch' or 'metric'.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, statements, tools, hits, settings, filename=None):
|
||||
super(ExcellonFile, self).__init__(statements=statements,
|
||||
settings=settings,
|
||||
filename=filename)
|
||||
self.tools = tools
|
||||
self.hits = hits
|
||||
|
||||
@property
|
||||
def primitives(self):
|
||||
"""
|
||||
Gets the primitives. Note that unlike Gerber, this generates new objects
|
||||
"""
|
||||
primitives = []
|
||||
for hit in self.hits:
|
||||
if isinstance(hit, DrillHit):
|
||||
primitives.append(Drill(hit.position, hit.tool.diameter,
|
||||
units=self.settings.units))
|
||||
elif isinstance(hit, DrillSlot):
|
||||
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter,
|
||||
units=self.settings.units))
|
||||
else:
|
||||
raise ValueError('Unknown hit type')
|
||||
return primitives
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
xmin = ymin = 100000000000
|
||||
xmax = ymax = -100000000000
|
||||
for hit in self.hits:
|
||||
bbox = hit.bounding_box
|
||||
xmin = min(bbox[0][0], xmin)
|
||||
xmax = max(bbox[0][1], xmax)
|
||||
ymin = min(bbox[1][0], ymin)
|
||||
ymax = max(bbox[1][1], ymax)
|
||||
return ((xmin, xmax), (ymin, ymax))
|
||||
|
||||
def report(self, filename=None):
|
||||
""" Print or save drill report
|
||||
"""
|
||||
if self.settings.units == 'inch':
|
||||
toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}in.\n' % self.settings.format
|
||||
else:
|
||||
toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}mm\n' % self.settings.format
|
||||
rprt = '=====================\nExcellon Drill Report\n=====================\n'
|
||||
if self.filename is not None:
|
||||
rprt += 'NC Drill File: %s\n\n' % self.filename
|
||||
rprt += 'Drill File Info:\n----------------\n'
|
||||
rprt += (' Data Mode %s\n' % 'Absolute'
|
||||
if self.settings.notation == 'absolute' else 'Incremental')
|
||||
rprt += (' Units %s\n' % 'Inches'
|
||||
if self.settings.units == 'inch' else 'Millimeters')
|
||||
rprt += '\nTool List:\n----------\n\n'
|
||||
rprt += ' Code Size Hits Path Length\n'
|
||||
rprt += ' --------------------------------------\n'
|
||||
for tool in iter(self.tools.values()):
|
||||
rprt += toolfmt.format(tool.number, tool.diameter,
|
||||
tool.hit_count, self.path_length(tool.number))
|
||||
if filename is not None:
|
||||
with open(filename, 'w') as f:
|
||||
f.write(rprt)
|
||||
return rprt
|
||||
|
||||
def write(self, filename=None):
|
||||
filename = filename if filename is not None else self.filename
|
||||
with open(filename, 'w') as f:
|
||||
|
||||
# Copy the header verbatim
|
||||
for statement in self.statements:
|
||||
if not isinstance(statement, ToolSelectionStmt):
|
||||
f.write(statement.to_excellon(self.settings) + '\n')
|
||||
else:
|
||||
break
|
||||
|
||||
# Write out coordinates for drill hits by tool
|
||||
for tool in iter(self.tools.values()):
|
||||
f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n')
|
||||
for hit in self.hits:
|
||||
if hit.tool.number == tool.number:
|
||||
f.write(CoordinateStmt(
|
||||
*hit.position).to_excellon(self.settings) + '\n')
|
||||
f.write(EndOfProgramStmt().to_excellon() + '\n')
|
||||
|
||||
def to_inch(self):
|
||||
"""
|
||||
Convert units to inches
|
||||
"""
|
||||
if self.units != 'inch':
|
||||
for statement in self.statements:
|
||||
statement.to_inch()
|
||||
for tool in iter(self.tools.values()):
|
||||
tool.to_inch()
|
||||
#for primitive in self.primitives:
|
||||
# primitive.to_inch()
|
||||
#for hit in self.hits:
|
||||
# hit.to_inch()
|
||||
self.units = 'inch'
|
||||
|
||||
def to_metric(self):
|
||||
""" Convert units to metric
|
||||
"""
|
||||
if self.units != 'metric':
|
||||
for statement in self.statements:
|
||||
statement.to_metric()
|
||||
for tool in iter(self.tools.values()):
|
||||
tool.to_metric()
|
||||
#for primitive in self.primitives:
|
||||
# print("Converting to metric: {}".format(primitive))
|
||||
# primitive.to_metric()
|
||||
# print(primitive)
|
||||
for hit in self.hits:
|
||||
hit.to_metric()
|
||||
self.units = 'metric'
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
for statement in self.statements:
|
||||
statement.offset(x_offset, y_offset)
|
||||
for primitive in self.primitives:
|
||||
primitive.offset(x_offset, y_offset)
|
||||
for hit in self. hits:
|
||||
hit.offset(x_offset, y_offset)
|
||||
|
||||
def path_length(self, tool_number=None):
|
||||
""" Return the path length for a given tool
|
||||
"""
|
||||
lengths = {}
|
||||
positions = {}
|
||||
for hit in self.hits:
|
||||
tool = hit.tool
|
||||
num = tool.number
|
||||
positions[num] = ((0, 0) if positions.get(num) is None
|
||||
else positions[num])
|
||||
lengths[num] = 0.0 if lengths.get(num) is None else lengths[num]
|
||||
lengths[num] = lengths[
|
||||
num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
|
||||
positions[num] = hit.position
|
||||
|
||||
if tool_number is None:
|
||||
return lengths
|
||||
else:
|
||||
return lengths.get(tool_number)
|
||||
|
||||
def hit_count(self, tool_number=None):
|
||||
counts = {}
|
||||
for tool in iter(self.tools.values()):
|
||||
counts[tool.number] = tool.hit_count
|
||||
if tool_number is None:
|
||||
return counts
|
||||
else:
|
||||
return counts.get(tool_number)
|
||||
|
||||
def update_tool(self, tool_number, **kwargs):
|
||||
""" Change parameters of a tool
|
||||
"""
|
||||
if kwargs.get('feed_rate') is not None:
|
||||
self.tools[tool_number].feed_rate = kwargs.get('feed_rate')
|
||||
if kwargs.get('retract_rate') is not None:
|
||||
self.tools[tool_number].retract_rate = kwargs.get('retract_rate')
|
||||
if kwargs.get('rpm') is not None:
|
||||
self.tools[tool_number].rpm = kwargs.get('rpm')
|
||||
if kwargs.get('diameter') is not None:
|
||||
self.tools[tool_number].diameter = kwargs.get('diameter')
|
||||
if kwargs.get('max_hit_count') is not None:
|
||||
self.tools[tool_number].max_hit_count = kwargs.get('max_hit_count')
|
||||
if kwargs.get('depth_offset') is not None:
|
||||
self.tools[tool_number].depth_offset = kwargs.get('depth_offset')
|
||||
# Update drill hits
|
||||
newtool = self.tools[tool_number]
|
||||
for hit in self.hits:
|
||||
if hit.tool.number == newtool.number:
|
||||
hit.tool = newtool
|
||||
|
||||
|
||||
class ExcellonParser(object):
|
||||
""" Excellon File Parser
|
||||
|
||||
Parameters
|
||||
----------
|
||||
settings : FileSettings or dict-like
|
||||
Excellon file settings to use when interpreting the excellon file.
|
||||
"""
|
||||
def __init__(self, settings=None, ext_tools=None):
|
||||
self.notation = 'absolute'
|
||||
self.units = 'inch'
|
||||
self.zeros = 'leading'
|
||||
self.format = (2, 4)
|
||||
self.state = 'INIT'
|
||||
self.statements = []
|
||||
self.tools = {}
|
||||
self.ext_tools = ext_tools or {}
|
||||
self.comment_tools = {}
|
||||
self.hits = []
|
||||
self.active_tool = None
|
||||
self.pos = [0., 0.]
|
||||
self.drill_down = False
|
||||
self._previous_line = ''
|
||||
# Default for plated is None, which means we don't know
|
||||
self.plated = ExcellonTool.PLATED_UNKNOWN
|
||||
if settings is not None:
|
||||
self.units = settings.units
|
||||
self.zeros = settings.zeros
|
||||
self.notation = settings.notation
|
||||
self.format = settings.format
|
||||
|
||||
@property
|
||||
def coordinates(self):
|
||||
return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
xmin = ymin = 100000000000
|
||||
xmax = ymax = -100000000000
|
||||
for x, y in self.coordinates:
|
||||
if x is not None:
|
||||
xmin = x if x < xmin else xmin
|
||||
xmax = x if x > xmax else xmax
|
||||
if y is not None:
|
||||
ymin = y if y < ymin else ymin
|
||||
ymax = y if y > ymax else ymax
|
||||
return ((xmin, xmax), (ymin, ymax))
|
||||
|
||||
@property
|
||||
def hole_sizes(self):
|
||||
return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)]
|
||||
|
||||
@property
|
||||
def hole_count(self):
|
||||
return len(self.hits)
|
||||
|
||||
def parse(self, filename):
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
return self.parse_raw(data, filename)
|
||||
|
||||
def parse_raw(self, data, filename=None):
|
||||
for line in StringIO(data):
|
||||
self._parse_line(line.strip())
|
||||
for stmt in self.statements:
|
||||
stmt.units = self.units
|
||||
return ExcellonFile(self.statements, self.tools, self.hits,
|
||||
self._settings(), filename)
|
||||
|
||||
def _parse_line(self, line):
|
||||
# skip empty lines
|
||||
# Prepend previous line's data...
|
||||
line = '{}{}'.format(self._previous_line, line)
|
||||
self._previous_line = ''
|
||||
|
||||
# Skip empty lines
|
||||
if not line.strip():
|
||||
return
|
||||
|
||||
if line[0] == ';':
|
||||
comment_stmt = CommentStmt.from_excellon(line)
|
||||
self.statements.append(comment_stmt)
|
||||
|
||||
# get format from altium comment
|
||||
if "FILE_FORMAT" in comment_stmt.comment:
|
||||
detected_format = tuple(
|
||||
[int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
|
||||
if detected_format:
|
||||
self.format = detected_format
|
||||
|
||||
if "TYPE=PLATED" in comment_stmt.comment:
|
||||
self.plated = ExcellonTool.PLATED_YES
|
||||
|
||||
if "TYPE=NON_PLATED" in comment_stmt.comment:
|
||||
self.plated = ExcellonTool.PLATED_NO
|
||||
|
||||
if "HEADER:" in comment_stmt.comment:
|
||||
self.state = "HEADER"
|
||||
|
||||
if " Holesize " in comment_stmt.comment:
|
||||
self.state = "HEADER"
|
||||
|
||||
# Parse this as a hole definition
|
||||
tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment)
|
||||
if len(tools) == 1:
|
||||
tool = tools[tools.keys()[0]]
|
||||
self._add_comment_tool(tool)
|
||||
|
||||
elif line[:3] == 'M48':
|
||||
self.statements.append(HeaderBeginStmt())
|
||||
self.state = 'HEADER'
|
||||
|
||||
elif line[0] == '%':
|
||||
self.statements.append(RewindStopStmt())
|
||||
if self.state == 'HEADER':
|
||||
self.state = 'DRILL'
|
||||
elif self.state == 'INIT':
|
||||
self.state = 'HEADER'
|
||||
|
||||
elif line[:3] == 'M00' and self.state == 'DRILL':
|
||||
if self.active_tool:
|
||||
cur_tool_number = self.active_tool.number
|
||||
next_tool = self._get_tool(cur_tool_number + 1)
|
||||
|
||||
self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool))
|
||||
self.active_tool = next_tool
|
||||
else:
|
||||
raise Exception('Invalid state exception')
|
||||
|
||||
elif line[:3] == 'M95':
|
||||
self.statements.append(HeaderEndStmt())
|
||||
if self.state == 'HEADER':
|
||||
self.state = 'DRILL'
|
||||
|
||||
elif line[:3] == 'M15':
|
||||
self.statements.append(ZAxisRoutPositionStmt())
|
||||
self.drill_down = True
|
||||
|
||||
elif line[:3] == 'M16':
|
||||
self.statements.append(RetractWithClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
elif line[:3] == 'M17':
|
||||
self.statements.append(RetractWithoutClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
elif line[:3] == 'M30':
|
||||
stmt = EndOfProgramStmt.from_excellon(line, self._settings())
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'G00':
|
||||
# Coordinates may be on the next line
|
||||
if line.strip() == 'G00':
|
||||
self._previous_line = line
|
||||
return
|
||||
|
||||
self.statements.append(RouteModeStmt())
|
||||
self.state = 'ROUT'
|
||||
|
||||
stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
|
||||
stmt.mode = self.state
|
||||
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
if y is not None:
|
||||
self.pos[1] = y
|
||||
else:
|
||||
if x is not None:
|
||||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
|
||||
elif line[:3] == 'G01':
|
||||
|
||||
# Coordinates might be on the next line...
|
||||
if line.strip() == 'G01':
|
||||
self._previous_line = line
|
||||
return
|
||||
|
||||
self.statements.append(RouteModeStmt())
|
||||
self.state = 'LINEAR'
|
||||
|
||||
stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
|
||||
stmt.mode = self.state
|
||||
|
||||
# The start position is where we were before the rout command
|
||||
start = (self.pos[0], self.pos[1])
|
||||
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
if y is not None:
|
||||
self.pos[1] = y
|
||||
else:
|
||||
if x is not None:
|
||||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
|
||||
# Our ending position
|
||||
end = (self.pos[0], self.pos[1])
|
||||
|
||||
if self.drill_down:
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT))
|
||||
self.active_tool._hit()
|
||||
|
||||
elif line[:3] == 'G05':
|
||||
self.statements.append(DrillModeStmt())
|
||||
self.drill_down = False
|
||||
self.state = 'DRILL'
|
||||
|
||||
elif 'INCH' in line or 'METRIC' in line:
|
||||
stmt = UnitStmt.from_excellon(line)
|
||||
self.units = stmt.units
|
||||
self.zeros = stmt.zeros
|
||||
if stmt.format:
|
||||
self.format = stmt.format
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'M71' or line[:3] == 'M72':
|
||||
stmt = MeasuringModeStmt.from_excellon(line)
|
||||
self.units = stmt.units
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'ICI':
|
||||
stmt = IncrementalModeStmt.from_excellon(line)
|
||||
self.notation = 'incremental' if stmt.mode == 'on' else 'absolute'
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'VER':
|
||||
stmt = VersionStmt.from_excellon(line)
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:4] == 'FMAT':
|
||||
stmt = FormatStmt.from_excellon(line)
|
||||
self.statements.append(stmt)
|
||||
self.format = stmt.format_tuple
|
||||
|
||||
elif line[:3] == 'G40':
|
||||
self.statements.append(CutterCompensationOffStmt())
|
||||
|
||||
elif line[:3] == 'G41':
|
||||
self.statements.append(CutterCompensationLeftStmt())
|
||||
|
||||
elif line[:3] == 'G42':
|
||||
self.statements.append(CutterCompensationRightStmt())
|
||||
|
||||
elif line[:3] == 'G90':
|
||||
self.statements.append(AbsoluteModeStmt())
|
||||
self.notation = 'absolute'
|
||||
|
||||
elif line[0] == 'F':
|
||||
infeed_rate_stmt = ZAxisInfeedRateStmt.from_excellon(line)
|
||||
self.statements.append(infeed_rate_stmt)
|
||||
|
||||
elif line[0] == 'T' and self.state == 'HEADER':
|
||||
if not ',OFF' in line and not ',ON' in line:
|
||||
tool = ExcellonTool.from_excellon(line, self._settings(), None, self.plated)
|
||||
self._merge_properties(tool)
|
||||
self.tools[tool.number] = tool
|
||||
self.statements.append(tool)
|
||||
else:
|
||||
self.statements.append(UnknownStmt.from_excellon(line))
|
||||
|
||||
elif line[0] == 'T' and self.state != 'HEADER':
|
||||
stmt = ToolSelectionStmt.from_excellon(line)
|
||||
self.statements.append(stmt)
|
||||
|
||||
# T0 is used as END marker, just ignore
|
||||
if stmt.tool != 0:
|
||||
tool = self._get_tool(stmt.tool)
|
||||
|
||||
if not tool:
|
||||
# FIXME: for weird files with no tools defined, original calc from gerb
|
||||
if self._settings().units == "inch":
|
||||
diameter = (16 + 8 * stmt.tool) / 1000.0
|
||||
else:
|
||||
diameter = metric((16 + 8 * stmt.tool) / 1000.0)
|
||||
|
||||
tool = ExcellonTool(
|
||||
self._settings(), number=stmt.tool, diameter=diameter)
|
||||
self.tools[tool.number] = tool
|
||||
|
||||
# FIXME: need to add this tool definition inside header to
|
||||
# make sure it is properly written
|
||||
for i, s in enumerate(self.statements):
|
||||
if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool):
|
||||
self.statements.insert(i, tool)
|
||||
break
|
||||
|
||||
self.active_tool = tool
|
||||
|
||||
elif line[0] == 'R' and self.state != 'HEADER':
|
||||
stmt = RepeatHoleStmt.from_excellon(line, self._settings())
|
||||
self.statements.append(stmt)
|
||||
for i in range(stmt.count):
|
||||
self.pos[0] += stmt.xdelta if stmt.xdelta is not None else 0
|
||||
self.pos[1] += stmt.ydelta if stmt.ydelta is not None else 0
|
||||
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
|
||||
self.active_tool._hit()
|
||||
|
||||
elif line[0] in ['X', 'Y']:
|
||||
if 'G85' in line:
|
||||
stmt = SlotStmt.from_excellon(line, self._settings())
|
||||
|
||||
# I don't know if this is actually correct, but it makes sense
|
||||
# that this is where the tool would end
|
||||
x = stmt.x_end
|
||||
y = stmt.y_end
|
||||
|
||||
self.statements.append(stmt)
|
||||
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
if y is not None:
|
||||
self.pos[1] = y
|
||||
else:
|
||||
if x is not None:
|
||||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
|
||||
if self.state == 'DRILL' or self.state == 'HEADER':
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end), DrillSlot.TYPE_G85))
|
||||
self.active_tool._hit()
|
||||
else:
|
||||
stmt = CoordinateStmt.from_excellon(line, self._settings())
|
||||
|
||||
# We need this in case we are in rout mode
|
||||
start = (self.pos[0], self.pos[1])
|
||||
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
if y is not None:
|
||||
self.pos[1] = y
|
||||
else:
|
||||
if x is not None:
|
||||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
|
||||
if self.state == 'LINEAR' and self.drill_down:
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT))
|
||||
|
||||
elif self.state == 'DRILL' or self.state == 'HEADER':
|
||||
# Yes, drills in the header doesn't follow the specification, but it there are many
|
||||
# files like this
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
|
||||
self.active_tool._hit()
|
||||
|
||||
else:
|
||||
self.statements.append(UnknownStmt.from_excellon(line))
|
||||
|
||||
def _settings(self):
|
||||
return FileSettings(units=self.units, format=self.format,
|
||||
zeros=self.zeros, notation=self.notation)
|
||||
|
||||
def _add_comment_tool(self, tool):
|
||||
"""
|
||||
Add a tool that was defined in the comments to this file.
|
||||
|
||||
If we have already found this tool, then we will merge this comment tool definition into
|
||||
the information for the tool
|
||||
"""
|
||||
|
||||
existing = self.tools.get(tool.number)
|
||||
if existing and existing.plated == None:
|
||||
existing.plated = tool.plated
|
||||
|
||||
self.comment_tools[tool.number] = tool
|
||||
|
||||
def _merge_properties(self, tool):
|
||||
"""
|
||||
When we have externally defined tools, merge the properties of that tool into this one
|
||||
|
||||
For now, this is only plated
|
||||
"""
|
||||
|
||||
if tool.plated == ExcellonTool.PLATED_UNKNOWN:
|
||||
ext_tool = self.ext_tools.get(tool.number)
|
||||
|
||||
if ext_tool:
|
||||
tool.plated = ext_tool.plated
|
||||
|
||||
def _get_tool(self, toolid):
|
||||
|
||||
tool = self.tools.get(toolid)
|
||||
if not tool:
|
||||
tool = self.comment_tools.get(toolid)
|
||||
if tool:
|
||||
tool.settings = self._settings()
|
||||
self.tools[toolid] = tool
|
||||
|
||||
if not tool:
|
||||
tool = self.ext_tools.get(toolid)
|
||||
if tool:
|
||||
tool.settings = self._settings()
|
||||
self.tools[toolid] = tool
|
||||
|
||||
return tool
|
||||
|
||||
def detect_excellon_format(data=None, filename=None):
|
||||
""" Detect excellon file decimal format and zero-suppression settings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
String containing contents of Excellon file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
settings : dict
|
||||
Detected excellon file settings. Keys are
|
||||
- `format`: decimal format as tuple (<int part>, <decimal part>)
|
||||
- `zero_suppression`: zero suppression, 'leading' or 'trailing'
|
||||
"""
|
||||
results = {}
|
||||
detected_zeros = None
|
||||
detected_format = None
|
||||
zeros_options = ('leading', 'trailing', )
|
||||
format_options = ((2, 4), (2, 5), (3, 3),)
|
||||
|
||||
if data is None and filename is None:
|
||||
raise ValueError('Either data or filename arguments must be provided')
|
||||
if data is None:
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
|
||||
# Check for obvious clues:
|
||||
p = ExcellonParser()
|
||||
p.parse_raw(data)
|
||||
|
||||
# Get zero_suppression from a unit statement
|
||||
zero_statements = [stmt.zeros for stmt in p.statements
|
||||
if isinstance(stmt, UnitStmt)]
|
||||
|
||||
# get format from altium comment
|
||||
format_comment = [stmt.comment for stmt in p.statements
|
||||
if isinstance(stmt, CommentStmt)
|
||||
and 'FILE_FORMAT' in stmt.comment]
|
||||
|
||||
detected_format = (tuple([int(val) for val in
|
||||
format_comment[0].split('=')[1].split(':')])
|
||||
if len(format_comment) == 1 else None)
|
||||
detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None
|
||||
|
||||
# Bail out here if possible
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zeros': detected_zeros}
|
||||
|
||||
# Only look at remaining options
|
||||
if detected_format is not None:
|
||||
format_options = (detected_format,)
|
||||
if detected_zeros is not None:
|
||||
zeros_options = (detected_zeros,)
|
||||
|
||||
# Brute force all remaining options, and pick the best looking one...
|
||||
for zeros in zeros_options:
|
||||
for fmt in format_options:
|
||||
key = (fmt, zeros)
|
||||
settings = FileSettings(zeros=zeros, format=fmt)
|
||||
try:
|
||||
p = ExcellonParser(settings)
|
||||
ef = p.parse_raw(data)
|
||||
size = tuple([t[0] - t[1] for t in ef.bounding_box])
|
||||
hole_area = 0.0
|
||||
for hit in p.hits:
|
||||
tool = hit.tool
|
||||
hole_area += math.pow(math.pi * tool.diameter / 2., 2)
|
||||
results[key] = (size, p.hole_count, hole_area)
|
||||
except:
|
||||
pass
|
||||
|
||||
# See if any of the dimensions are left with only a single option
|
||||
formats = set(key[0] for key in iter(results.keys()))
|
||||
zeros = set(key[1] for key in iter(results.keys()))
|
||||
if len(formats) == 1:
|
||||
detected_format = formats.pop()
|
||||
if len(zeros) == 1:
|
||||
detected_zeros = zeros.pop()
|
||||
|
||||
# Bail out here if we got everything....
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zeros': detected_zeros}
|
||||
|
||||
# Otherwise score each option and pick the best candidate
|
||||
else:
|
||||
scores = {}
|
||||
for key in results.keys():
|
||||
size, count, diameter = results[key]
|
||||
scores[key] = _layer_size_score(size, count, diameter)
|
||||
minscore = min(scores.values())
|
||||
for key in iter(scores.keys()):
|
||||
if scores[key] == minscore:
|
||||
return {'format': key[0], 'zeros': key[1]}
|
||||
|
||||
|
||||
def _layer_size_score(size, hole_count, hole_area):
|
||||
""" Heuristic used for determining the correct file number interpretation.
|
||||
Lower is better.
|
||||
"""
|
||||
board_area = size[0] * size[1]
|
||||
if board_area == 0:
|
||||
return 0
|
||||
|
||||
hole_percentage = hole_area / board_area
|
||||
hole_score = (hole_percentage - 0.25) ** 2
|
||||
size_score = (board_area - 8) ** 2
|
||||
return hole_score * size_score
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon DRR File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
|
||||
"""
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from argparse import PARSER
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon Settings Definition File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
This module provides Excellon file classes and parsing utilities
|
||||
"""
|
||||
|
||||
import re
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .cam import FileSettings
|
||||
|
||||
def loads(data):
|
||||
""" Read settings file information and return an FileSettings
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon settings file contents
|
||||
|
||||
Returns
|
||||
-------
|
||||
file settings: FileSettings
|
||||
|
||||
"""
|
||||
|
||||
return ExcellonSettingsParser().parse_raw(data)
|
||||
|
||||
def map_coordinates(value):
|
||||
if value == 'ABSOLUTE':
|
||||
return 'absolute'
|
||||
return 'relative'
|
||||
|
||||
def map_units(value):
|
||||
if value == 'ENGLISH':
|
||||
return 'inch'
|
||||
return 'metric'
|
||||
|
||||
def map_boolean(value):
|
||||
return value == 'YES'
|
||||
|
||||
SETTINGS_KEYS = {
|
||||
'INTEGER-PLACES': (int, 'format-int'),
|
||||
'DECIMAL-PLACES': (int, 'format-dec'),
|
||||
'COORDINATES': (map_coordinates, 'notation'),
|
||||
'OUTPUT-UNITS': (map_units, 'units'),
|
||||
}
|
||||
|
||||
class ExcellonSettingsParser(object):
|
||||
"""Excellon Settings PARSER
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.values = {}
|
||||
self.settings = None
|
||||
|
||||
def parse_raw(self, data):
|
||||
for line in StringIO(data):
|
||||
self._parse(line.strip())
|
||||
|
||||
# Create the FileSettings object
|
||||
self.settings = FileSettings(
|
||||
notation=self.values['notation'],
|
||||
units=self.values['units'],
|
||||
format=(self.values['format-int'], self.values['format-dec'])
|
||||
)
|
||||
|
||||
return self.settings
|
||||
|
||||
def _parse(self, line):
|
||||
|
||||
line_items = line.split()
|
||||
if len(line_items) == 2:
|
||||
|
||||
item_type_info = SETTINGS_KEYS.get(line_items[0])
|
||||
if item_type_info:
|
||||
# Convert the value to the expected type
|
||||
item_value = item_type_info[0](line_items[1])
|
||||
|
||||
self.values[item_type_info[1]] = item_value
|
|
@ -0,0 +1,979 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Excellon Statements
|
||||
====================
|
||||
**Excellon file statement classes**
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
import itertools
|
||||
from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
|
||||
inch, metric)
|
||||
|
||||
|
||||
__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
|
||||
'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt',
|
||||
'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt',
|
||||
'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt',
|
||||
'MeasuringModeStmt', 'RouteModeStmt', 'LinearModeStmt', 'DrillModeStmt',
|
||||
'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt',
|
||||
'ExcellonStatement', 'ZAxisRoutPositionStmt',
|
||||
'RetractWithClampingStmt', 'RetractWithoutClampingStmt',
|
||||
'CutterCompensationOffStmt', 'CutterCompensationLeftStmt',
|
||||
'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt',
|
||||
'NextToolSelectionStmt', 'SlotStmt']
|
||||
|
||||
|
||||
class ExcellonStatement(object):
|
||||
""" Excellon Statement abstract base class
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
raise NotImplementedError('from_excellon must be implemented in a '
|
||||
'subclass')
|
||||
|
||||
def __init__(self, unit='inch', id=None):
|
||||
self.units = unit
|
||||
self.id = uuid.uuid4().int if id is None else id
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
raise NotImplementedError('to_excellon must be implemented in a '
|
||||
'subclass')
|
||||
|
||||
def to_inch(self):
|
||||
self.units = 'inch'
|
||||
|
||||
def to_metric(self):
|
||||
self.units = 'metric'
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
pass
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
class ExcellonTool(ExcellonStatement):
|
||||
""" Excellon Tool class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
settings : FileSettings (dict-like)
|
||||
File-wide settings.
|
||||
|
||||
kwargs : dict-like
|
||||
Tool settings from the excellon statement. Valid keys are:
|
||||
- `diameter` : Tool diameter [expressed in file units]
|
||||
- `rpm` : Tool RPM
|
||||
- `feed_rate` : Z-axis tool feed rate
|
||||
- `retract_rate` : Z-axis tool retraction rate
|
||||
- `max_hit_count` : Number of hits allowed before a tool change
|
||||
- `depth_offset` : Offset of tool depth from tip of tool.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
number : integer
|
||||
Tool number from the excellon file
|
||||
|
||||
diameter : float
|
||||
Tool diameter in file units
|
||||
|
||||
rpm : float
|
||||
Tool RPM
|
||||
|
||||
feed_rate : float
|
||||
Tool Z-axis feed rate.
|
||||
|
||||
retract_rate : float
|
||||
Tool Z-axis retract rate
|
||||
|
||||
depth_offset : float
|
||||
Offset of depth measurement from tip of tool
|
||||
|
||||
max_hit_count : integer
|
||||
Maximum number of tool hits allowed before a tool change
|
||||
|
||||
hit_count : integer
|
||||
Number of tool hits in excellon file.
|
||||
"""
|
||||
|
||||
PLATED_UNKNOWN = None
|
||||
PLATED_YES = 'plated'
|
||||
PLATED_NO = 'nonplated'
|
||||
PLATED_OPTIONAL = 'optional'
|
||||
|
||||
@classmethod
|
||||
def from_tool(cls, tool):
|
||||
args = {}
|
||||
|
||||
args['depth_offset'] = tool.depth_offset
|
||||
args['diameter'] = tool.diameter
|
||||
args['feed_rate'] = tool.feed_rate
|
||||
args['max_hit_count'] = tool.max_hit_count
|
||||
args['number'] = tool.number
|
||||
args['plated'] = tool.plated
|
||||
args['retract_rate'] = tool.retract_rate
|
||||
args['rpm'] = tool.rpm
|
||||
|
||||
return cls(None, **args)
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, id=None, plated=None):
|
||||
""" Create a Tool from an excellon file tool definition line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : string
|
||||
Tool definition line from an excellon file.
|
||||
|
||||
settings : FileSettings (dict-like)
|
||||
Excellon file-wide settings
|
||||
|
||||
Returns
|
||||
-------
|
||||
tool : Tool
|
||||
An ExcellonTool representing the tool defined in `line`
|
||||
"""
|
||||
commands = pairwise(re.split('([BCFHSTZ])', line)[1:])
|
||||
args = {}
|
||||
args['id'] = id
|
||||
nformat = settings.format
|
||||
zero_suppression = settings.zero_suppression
|
||||
for cmd, val in commands:
|
||||
if cmd == 'B':
|
||||
args['retract_rate'] = parse_gerber_value(val, nformat, zero_suppression)
|
||||
elif cmd == 'C':
|
||||
args['diameter'] = parse_gerber_value(val, nformat, zero_suppression)
|
||||
elif cmd == 'F':
|
||||
args['feed_rate'] = parse_gerber_value(val, nformat, zero_suppression)
|
||||
elif cmd == 'H':
|
||||
args['max_hit_count'] = parse_gerber_value(val, nformat, zero_suppression)
|
||||
elif cmd == 'S':
|
||||
args['rpm'] = 1000 * parse_gerber_value(val, nformat, zero_suppression)
|
||||
elif cmd == 'T':
|
||||
args['number'] = int(val)
|
||||
elif cmd == 'Z':
|
||||
args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression)
|
||||
|
||||
if plated != ExcellonTool.PLATED_UNKNOWN:
|
||||
# Sometimees we can can parse the plating status
|
||||
args['plated'] = plated
|
||||
return cls(settings, **args)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, settings, tool_dict):
|
||||
""" Create an ExcellonTool from a dict.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
settings : FileSettings (dict-like)
|
||||
Excellon File-wide settings
|
||||
|
||||
tool_dict : dict
|
||||
Excellon tool parameters as a dict
|
||||
|
||||
Returns
|
||||
-------
|
||||
tool : ExcellonTool
|
||||
An ExcellonTool initialized with the parameters in tool_dict.
|
||||
"""
|
||||
return cls(settings, **tool_dict)
|
||||
|
||||
def __init__(self, settings, **kwargs):
|
||||
if kwargs.get('id') is not None:
|
||||
super(ExcellonTool, self).__init__(id=kwargs.get('id'))
|
||||
self.settings = settings
|
||||
self.number = kwargs.get('number')
|
||||
self.feed_rate = kwargs.get('feed_rate')
|
||||
self.retract_rate = kwargs.get('retract_rate')
|
||||
self.rpm = kwargs.get('rpm')
|
||||
self.diameter = kwargs.get('diameter')
|
||||
self.max_hit_count = kwargs.get('max_hit_count')
|
||||
self.depth_offset = kwargs.get('depth_offset')
|
||||
self.plated = kwargs.get('plated')
|
||||
|
||||
self.hit_count = 0
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
if self.settings and not settings:
|
||||
settings = self.settings
|
||||
fmt = settings.format
|
||||
zs = settings.zero_suppression
|
||||
stmt = 'T%02d' % self.number
|
||||
if self.retract_rate is not None:
|
||||
stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs)
|
||||
if self.feed_rate is not None:
|
||||
stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs)
|
||||
if self.max_hit_count is not None:
|
||||
stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs)
|
||||
if self.rpm is not None:
|
||||
if self.rpm < 100000.:
|
||||
stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs)
|
||||
else:
|
||||
stmt += 'S%g' % (self.rpm / 1000.)
|
||||
if self.diameter is not None:
|
||||
stmt += 'C%s' % decimal_string(self.diameter, fmt[1], True)
|
||||
if self.depth_offset is not None:
|
||||
stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs)
|
||||
return stmt
|
||||
|
||||
def to_inch(self):
|
||||
if self.settings.units != 'inch':
|
||||
self.settings.units = 'inch'
|
||||
if self.diameter is not None:
|
||||
self.diameter = inch(self.diameter)
|
||||
|
||||
def to_metric(self):
|
||||
if self.settings.units != 'metric':
|
||||
self.settings.units = 'metric'
|
||||
if self.diameter is not None:
|
||||
self.diameter = metric(self.diameter)
|
||||
|
||||
def _hit(self):
|
||||
self.hit_count += 1
|
||||
|
||||
def equivalent(self, other):
|
||||
"""
|
||||
Is the other tool equal to this, ignoring the tool number, and other file specified properties
|
||||
"""
|
||||
|
||||
if type(self) != type(other):
|
||||
return False
|
||||
|
||||
return (self.diameter == other.diameter
|
||||
and self.feed_rate == other.feed_rate
|
||||
and self.retract_rate == other.retract_rate
|
||||
and self.rpm == other.rpm
|
||||
and self.depth_offset == other.depth_offset
|
||||
and self.max_hit_count == other.max_hit_count
|
||||
and self.plated == other.plated
|
||||
and self.settings.units == other.settings.units)
|
||||
|
||||
def __repr__(self):
|
||||
unit = 'in.' if self.settings.units == 'inch' else 'mm'
|
||||
fmtstr = '<ExcellonTool %%02d: %%%d.%dg%%s dia.>' % self.settings.format
|
||||
return fmtstr % (self.number, self.diameter, unit)
|
||||
|
||||
|
||||
class ToolSelectionStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
""" Create a ToolSelectionStmt from an excellon file line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : string
|
||||
Line from an Excellon file
|
||||
|
||||
Returns
|
||||
-------
|
||||
tool_statement : ToolSelectionStmt
|
||||
ToolSelectionStmt representation of `line.`
|
||||
"""
|
||||
line = line[1:]
|
||||
compensation_index = None
|
||||
|
||||
# up to 3 characters for tool number (Frizting uses that)
|
||||
if len(line) <= 3:
|
||||
tool = int(line)
|
||||
else:
|
||||
tool = int(line[:2])
|
||||
compensation_index = int(line[2:])
|
||||
|
||||
return cls(tool, compensation_index, **kwargs)
|
||||
|
||||
def __init__(self, tool, compensation_index=None, **kwargs):
|
||||
super(ToolSelectionStmt, self).__init__(**kwargs)
|
||||
tool = int(tool)
|
||||
compensation_index = (int(compensation_index) if compensation_index
|
||||
is not None else None)
|
||||
self.tool = tool
|
||||
self.compensation_index = compensation_index
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
stmt = 'T%02d' % self.tool
|
||||
if self.compensation_index is not None:
|
||||
stmt += '%02d' % self.compensation_index
|
||||
return stmt
|
||||
|
||||
class NextToolSelectionStmt(ExcellonStatement):
|
||||
|
||||
# TODO the statement exists outside of the context of the file,
|
||||
# so it is imposible to know that it is really the next tool
|
||||
|
||||
def __init__(self, cur_tool, next_tool, **kwargs):
|
||||
"""
|
||||
Select the next tool in the wheel.
|
||||
Parameters
|
||||
----------
|
||||
cur_tool : the tool that is currently selected
|
||||
next_tool : the that that is now selected
|
||||
"""
|
||||
super(NextToolSelectionStmt, self).__init__(**kwargs)
|
||||
|
||||
self.cur_tool = cur_tool
|
||||
self.next_tool = next_tool
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
stmt = 'M00'
|
||||
return stmt
|
||||
|
||||
class ZAxisInfeedRateStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
""" Create a ZAxisInfeedRate from an excellon file line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : string
|
||||
Line from an Excellon file
|
||||
|
||||
Returns
|
||||
-------
|
||||
z_axis_infeed_rate : ToolSelectionStmt
|
||||
ToolSelectionStmt representation of `line.`
|
||||
"""
|
||||
rate = int(line[1:])
|
||||
|
||||
return cls(rate, **kwargs)
|
||||
|
||||
def __init__(self, rate, **kwargs):
|
||||
super(ZAxisInfeedRateStmt, self).__init__(**kwargs)
|
||||
self.rate = rate
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'F%02d' % self.rate
|
||||
|
||||
|
||||
class CoordinateStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_point(cls, point, mode=None):
|
||||
|
||||
stmt = cls(point[0], point[1])
|
||||
if mode:
|
||||
stmt.mode = mode
|
||||
return stmt
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, **kwargs):
|
||||
x_coord = None
|
||||
y_coord = None
|
||||
if line[0] == 'X':
|
||||
splitline = line.strip('X').split('Y')
|
||||
x_coord = parse_gerber_value(splitline[0], settings.format,
|
||||
settings.zero_suppression)
|
||||
if len(splitline) == 2:
|
||||
y_coord = parse_gerber_value(splitline[1], settings.format,
|
||||
settings.zero_suppression)
|
||||
else:
|
||||
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
|
||||
settings.zero_suppression)
|
||||
c = cls(x_coord, y_coord, **kwargs)
|
||||
c.units = settings.units
|
||||
return c
|
||||
|
||||
def __init__(self, x=None, y=None, **kwargs):
|
||||
super(CoordinateStmt, self).__init__(**kwargs)
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.mode = None
|
||||
|
||||
def to_excellon(self, settings):
|
||||
stmt = ''
|
||||
if self.mode == "ROUT":
|
||||
stmt += "G00"
|
||||
if self.mode == "LINEAR":
|
||||
stmt += "G01"
|
||||
if self.x is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x, settings.format,
|
||||
settings.zero_suppression)
|
||||
if self.y is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y, settings.format,
|
||||
settings.zero_suppression)
|
||||
return stmt
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
self.units = 'inch'
|
||||
if self.x is not None:
|
||||
self.x = inch(self.x)
|
||||
if self.y is not None:
|
||||
self.y = inch(self.y)
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
self.units = 'metric'
|
||||
if self.x is not None:
|
||||
self.x = metric(self.x)
|
||||
if self.y is not None:
|
||||
self.y = metric(self.y)
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
if self.x is not None:
|
||||
self.x += x_offset
|
||||
if self.y is not None:
|
||||
self.y += y_offset
|
||||
|
||||
def __str__(self):
|
||||
coord_str = ''
|
||||
if self.x is not None:
|
||||
coord_str += 'X: %g ' % self.x
|
||||
if self.y is not None:
|
||||
coord_str += 'Y: %g ' % self.y
|
||||
|
||||
return '<Coordinate Statement: %s>' % coord_str
|
||||
|
||||
|
||||
class RepeatHoleStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, **kwargs):
|
||||
match = re.compile(r'R(?P<rcount>[0-9]*)X?(?P<xdelta>[+\-]?\d*\.?\d*)?Y?'
|
||||
'(?P<ydelta>[+\-]?\d*\.?\d*)?').match(line)
|
||||
stmt = match.groupdict()
|
||||
count = int(stmt['rcount'])
|
||||
xdelta = (parse_gerber_value(stmt['xdelta'], settings.format,
|
||||
settings.zero_suppression)
|
||||
if stmt['xdelta'] is not '' else None)
|
||||
ydelta = (parse_gerber_value(stmt['ydelta'], settings.format,
|
||||
settings.zero_suppression)
|
||||
if stmt['ydelta'] is not '' else None)
|
||||
c = cls(count, xdelta, ydelta, **kwargs)
|
||||
c.units = settings.units
|
||||
return c
|
||||
|
||||
def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs):
|
||||
super(RepeatHoleStmt, self).__init__(**kwargs)
|
||||
self.count = count
|
||||
self.xdelta = xdelta
|
||||
self.ydelta = ydelta
|
||||
|
||||
def to_excellon(self, settings):
|
||||
stmt = 'R%d' % self.count
|
||||
if self.xdelta is not None and self.xdelta != 0.0:
|
||||
stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format,
|
||||
settings.zero_suppression)
|
||||
if self.ydelta is not None and self.ydelta != 0.0:
|
||||
stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format,
|
||||
settings.zero_suppression)
|
||||
return stmt
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
self.units = 'inch'
|
||||
if self.xdelta is not None:
|
||||
self.xdelta = inch(self.xdelta)
|
||||
if self.ydelta is not None:
|
||||
self.ydelta = inch(self.ydelta)
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
self.units = 'metric'
|
||||
if self.xdelta is not None:
|
||||
self.xdelta = metric(self.xdelta)
|
||||
if self.ydelta is not None:
|
||||
self.ydelta = metric(self.ydelta)
|
||||
|
||||
def __str__(self):
|
||||
return '<Repeat Hole: %d times, offset X: %g Y: %g>' % (
|
||||
self.count,
|
||||
self.xdelta if self.xdelta is not None else 0,
|
||||
self.ydelta if self.ydelta is not None else 0)
|
||||
|
||||
|
||||
class CommentStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
return cls(line.lstrip(';'))
|
||||
|
||||
def __init__(self, comment, **kwargs):
|
||||
super(CommentStmt, self).__init__(**kwargs)
|
||||
self.comment = comment
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return ';%s' % self.comment
|
||||
|
||||
|
||||
class HeaderBeginStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(HeaderBeginStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M48'
|
||||
|
||||
|
||||
class HeaderEndStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(HeaderEndStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M95'
|
||||
|
||||
|
||||
class RewindStopStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RewindStopStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return '%'
|
||||
|
||||
|
||||
class ZAxisRoutPositionStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ZAxisRoutPositionStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M15'
|
||||
|
||||
|
||||
class RetractWithClampingStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RetractWithClampingStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M16'
|
||||
|
||||
|
||||
class RetractWithoutClampingStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RetractWithoutClampingStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M17'
|
||||
|
||||
|
||||
class CutterCompensationOffStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CutterCompensationOffStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'G40'
|
||||
|
||||
|
||||
class CutterCompensationLeftStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CutterCompensationLeftStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'G41'
|
||||
|
||||
|
||||
class CutterCompensationRightStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CutterCompensationRightStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'G42'
|
||||
|
||||
|
||||
class EndOfProgramStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, **kwargs):
|
||||
match = re.compile(r'M30X?(?P<x>\d*\.?\d*)?Y?'
|
||||
'(?P<y>\d*\.?\d*)?').match(line)
|
||||
stmt = match.groupdict()
|
||||
x = (parse_gerber_value(stmt['x'], settings.format,
|
||||
settings.zero_suppression)
|
||||
if stmt['x'] is not '' else None)
|
||||
y = (parse_gerber_value(stmt['y'], settings.format,
|
||||
settings.zero_suppression)
|
||||
if stmt['y'] is not '' else None)
|
||||
c = cls(x, y, **kwargs)
|
||||
c.units = settings.units
|
||||
return c
|
||||
|
||||
def __init__(self, x=None, y=None, **kwargs):
|
||||
super(EndOfProgramStmt, self).__init__(**kwargs)
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
stmt = 'M30'
|
||||
if self.x is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x)
|
||||
if self.y is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y)
|
||||
return stmt
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
self.units = 'inch'
|
||||
if self.x is not None:
|
||||
self.x = inch(self.x)
|
||||
if self.y is not None:
|
||||
self.y = inch(self.y)
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
self.units = 'metric'
|
||||
if self.x is not None:
|
||||
self.x = metric(self.x)
|
||||
if self.y is not None:
|
||||
self.y = metric(self.y)
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
if self.x is not None:
|
||||
self.x += x_offset
|
||||
if self.y is not None:
|
||||
self.y += y_offset
|
||||
|
||||
|
||||
class UnitStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings):
|
||||
"""Create the unit statement from the FileSettings"""
|
||||
|
||||
return cls(settings.units, settings.zeros)
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
units = 'inch' if 'INCH' in line else 'metric'
|
||||
zeros = 'leading' if 'LZ' in line else 'trailing'
|
||||
if '0000.00' in line:
|
||||
format = (4, 2)
|
||||
elif '000.000' in line:
|
||||
format = (3, 3)
|
||||
elif '00.0000' in line:
|
||||
format = (2, 4)
|
||||
else:
|
||||
format = None
|
||||
return cls(units, zeros, format, **kwargs)
|
||||
|
||||
def __init__(self, units='inch', zeros='leading', format=None, **kwargs):
|
||||
super(UnitStmt, self).__init__(**kwargs)
|
||||
self.units = units.lower()
|
||||
self.zeros = zeros
|
||||
self.format = format
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
# TODO This won't export the invalid format statement if it exists
|
||||
stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
|
||||
'LZ' if self.zeros == 'leading'
|
||||
else 'TZ')
|
||||
return stmt
|
||||
|
||||
def to_inch(self):
|
||||
self.units = 'inch'
|
||||
|
||||
def to_metric(self):
|
||||
self.units = 'metric'
|
||||
|
||||
|
||||
class IncrementalModeStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs)
|
||||
|
||||
def __init__(self, mode='off', **kwargs):
|
||||
super(IncrementalModeStmt, self).__init__(**kwargs)
|
||||
if mode.lower() not in ['on', 'off']:
|
||||
raise ValueError('Mode may be "on" or "off"')
|
||||
self.mode = mode
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON')
|
||||
|
||||
|
||||
class VersionStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
version = int(line.split(',')[1])
|
||||
return cls(version, **kwargs)
|
||||
|
||||
def __init__(self, version=1, **kwargs):
|
||||
super(VersionStmt, self).__init__(**kwargs)
|
||||
version = int(version)
|
||||
if version not in [1, 2]:
|
||||
raise ValueError('Valid versions are 1 or 2')
|
||||
self.version = version
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'VER,%d' % self.version
|
||||
|
||||
|
||||
class FormatStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
fmt = int(line.split(',')[1])
|
||||
return cls(fmt, **kwargs)
|
||||
|
||||
def __init__(self, format=1, **kwargs):
|
||||
super(FormatStmt, self).__init__(**kwargs)
|
||||
format = int(format)
|
||||
if format not in [1, 2]:
|
||||
raise ValueError('Valid formats are 1 or 2')
|
||||
self.format = format
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'FMAT,%d' % self.format
|
||||
|
||||
@property
|
||||
def format_tuple(self):
|
||||
return (self.format, 6 - self.format)
|
||||
|
||||
|
||||
class LinkToolStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
linked = [int(tool) for tool in line.split('/')]
|
||||
return cls(linked, **kwargs)
|
||||
|
||||
def __init__(self, linked_tools, **kwargs):
|
||||
super(LinkToolStmt, self).__init__(**kwargs)
|
||||
self.linked_tools = [int(x) for x in linked_tools]
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return '/'.join([str(x) for x in self.linked_tools])
|
||||
|
||||
|
||||
class MeasuringModeStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
if not ('M71' in line or 'M72' in line):
|
||||
raise ValueError('Not a measuring mode statement')
|
||||
return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs)
|
||||
|
||||
def __init__(self, units='inch', **kwargs):
|
||||
super(MeasuringModeStmt, self).__init__(**kwargs)
|
||||
units = units.lower()
|
||||
if units not in ['inch', 'metric']:
|
||||
raise ValueError('units must be "inch" or "metric"')
|
||||
self.units = units
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M72' if self.units == 'inch' else 'M71'
|
||||
|
||||
def to_inch(self):
|
||||
self.units = 'inch'
|
||||
|
||||
def to_metric(self):
|
||||
self.units = 'metric'
|
||||
|
||||
|
||||
class RouteModeStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(RouteModeStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'G00'
|
||||
|
||||
|
||||
class LinearModeStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(LinearModeStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'G01'
|
||||
|
||||
|
||||
class DrillModeStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(DrillModeStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'G05'
|
||||
|
||||
|
||||
class AbsoluteModeStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AbsoluteModeStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'G90'
|
||||
|
||||
|
||||
class UnknownStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
return cls(line, **kwargs)
|
||||
|
||||
def __init__(self, stmt, **kwargs):
|
||||
super(UnknownStmt, self).__init__(**kwargs)
|
||||
self.stmt = stmt
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
return self.stmt
|
||||
|
||||
def __str__(self):
|
||||
return "<Unknown Statement: %s>" % self.stmt
|
||||
|
||||
|
||||
class SlotStmt(ExcellonStatement):
|
||||
"""
|
||||
G85 statement. Defines a slot created by multiple drills between two specified points.
|
||||
|
||||
Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_points(cls, start, end):
|
||||
|
||||
return cls(start[0], start[1], end[0], end[1])
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, **kwargs):
|
||||
# Split the line based on the G85 separator
|
||||
sub_coords = line.split('G85')
|
||||
(x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
|
||||
(x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
|
||||
|
||||
# Some files seem to specify only one of the coordinates
|
||||
if x_end_coord == None:
|
||||
x_end_coord = x_start_coord
|
||||
if y_end_coord == None:
|
||||
y_end_coord = y_start_coord
|
||||
|
||||
c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
|
||||
c.units = settings.units
|
||||
return c
|
||||
|
||||
@staticmethod
|
||||
def parse_sub_coords(line, settings):
|
||||
|
||||
x_coord = None
|
||||
y_coord = None
|
||||
|
||||
if line[0] == 'X':
|
||||
splitline = line.strip('X').split('Y')
|
||||
x_coord = parse_gerber_value(splitline[0], settings.format,
|
||||
settings.zero_suppression)
|
||||
if len(splitline) == 2:
|
||||
y_coord = parse_gerber_value(splitline[1], settings.format,
|
||||
settings.zero_suppression)
|
||||
else:
|
||||
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
|
||||
settings.zero_suppression)
|
||||
|
||||
return (x_coord, y_coord)
|
||||
|
||||
|
||||
def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs):
|
||||
super(SlotStmt, self).__init__(**kwargs)
|
||||
self.x_start = x_start
|
||||
self.y_start = y_start
|
||||
self.x_end = x_end
|
||||
self.y_end = y_end
|
||||
self.mode = None
|
||||
|
||||
def to_excellon(self, settings):
|
||||
stmt = ''
|
||||
|
||||
if self.x_start is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x_start, settings.format,
|
||||
settings.zero_suppression)
|
||||
if self.y_start is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
|
||||
settings.zero_suppression)
|
||||
|
||||
stmt += 'G85'
|
||||
|
||||
if self.x_end is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
|
||||
settings.zero_suppression)
|
||||
if self.y_end is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
|
||||
settings.zero_suppression)
|
||||
|
||||
return stmt
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
self.units = 'inch'
|
||||
if self.x_start is not None:
|
||||
self.x_start = inch(self.x_start)
|
||||
if self.y_start is not None:
|
||||
self.y_start = inch(self.y_start)
|
||||
if self.x_end is not None:
|
||||
self.x_end = inch(self.x_end)
|
||||
if self.y_end is not None:
|
||||
self.y_end = inch(self.y_end)
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
self.units = 'metric'
|
||||
if self.x_start is not None:
|
||||
self.x_start = metric(self.x_start)
|
||||
if self.y_start is not None:
|
||||
self.y_start = metric(self.y_start)
|
||||
if self.x_end is not None:
|
||||
self.x_end = metric(self.x_end)
|
||||
if self.y_end is not None:
|
||||
self.y_end = metric(self.y_end)
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
if self.x_start is not None:
|
||||
self.x_start += x_offset
|
||||
if self.y_start is not None:
|
||||
self.y_start += y_offset
|
||||
if self.x_end is not None:
|
||||
self.x_end += x_offset
|
||||
if self.y_end is not None:
|
||||
self.y_end += y_offset
|
||||
|
||||
def __str__(self):
|
||||
start_str = ''
|
||||
if self.x_start is not None:
|
||||
start_str += 'X: %g ' % self.x_start
|
||||
if self.y_start is not None:
|
||||
start_str += 'Y: %g ' % self.y_start
|
||||
|
||||
end_str = ''
|
||||
if self.x_end is not None:
|
||||
end_str += 'X: %g ' % self.x_end
|
||||
if self.y_end is not None:
|
||||
end_str += 'Y: %g ' % self.y_end
|
||||
|
||||
return '<Slot Statement: %s to %s>' % (start_str, end_str)
|
||||
|
||||
def pairwise(iterator):
|
||||
""" Iterate over list taking two elements at a time.
|
||||
|
||||
e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)]
|
||||
"""
|
||||
a, b = itertools.tee(iterator)
|
||||
itr = zip(itertools.islice(a, 0, None, 2), itertools.islice(b, 1, None, 2))
|
||||
for elem in itr:
|
||||
yield elem
|
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon Tool Definition File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
This module provides Excellon file classes and parsing utilities
|
||||
"""
|
||||
|
||||
import re
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .excellon_statements import ExcellonTool
|
||||
|
||||
def loads(data, settings=None):
|
||||
""" Read tool file information and return a map of tools
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon Tool Definition file contents
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict tool name: ExcellonTool
|
||||
|
||||
"""
|
||||
return ExcellonToolDefinitionParser(settings).parse_raw(data)
|
||||
|
||||
class ExcellonToolDefinitionParser(object):
|
||||
""" Excellon File Parser
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
"""
|
||||
|
||||
allegro_tool = re.compile(r'(?P<size>[0-9/.]+)\s+(?P<plated>P|N)\s+T(?P<toolid>[0-9]{2})\s+(?P<xtol>[0-9/.]+)\s+(?P<ytol>[0-9/.]+)')
|
||||
allegro_comment_mils = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
|
||||
allegro2_comment_mils = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
|
||||
allegro_comment_mm = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
|
||||
allegro2_comment_mm = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
|
||||
|
||||
matchers = [
|
||||
(allegro_tool, 'mils'),
|
||||
(allegro_comment_mils, 'mils'),
|
||||
(allegro2_comment_mils, 'mils'),
|
||||
(allegro_comment_mm, 'mm'),
|
||||
(allegro2_comment_mm, 'mm'),
|
||||
]
|
||||
|
||||
def __init__(self, settings=None):
|
||||
self.tools = {}
|
||||
self.settings = settings
|
||||
|
||||
def parse_raw(self, data):
|
||||
for line in StringIO(data):
|
||||
self._parse(line.strip())
|
||||
|
||||
return self.tools
|
||||
|
||||
def _parse(self, line):
|
||||
|
||||
for matcher in ExcellonToolDefinitionParser.matchers:
|
||||
m = matcher[0].match(line)
|
||||
if m:
|
||||
unit = matcher[1]
|
||||
|
||||
size = float(m.group('size'))
|
||||
platedstr = m.group('plated')
|
||||
toolid = int(m.group('toolid'))
|
||||
xtol = float(m.group('xtol'))
|
||||
ytol = float(m.group('ytol'))
|
||||
|
||||
size = self._convert_length(size, unit)
|
||||
xtol = self._convert_length(xtol, unit)
|
||||
ytol = self._convert_length(ytol, unit)
|
||||
|
||||
if platedstr == 'PLATED':
|
||||
plated = ExcellonTool.PLATED_YES
|
||||
elif platedstr == 'NON_PLATED':
|
||||
plated = ExcellonTool.PLATED_NO
|
||||
elif platedstr == 'OPTIONAL':
|
||||
plated = ExcellonTool.PLATED_OPTIONAL
|
||||
else:
|
||||
plated = ExcellonTool.PLATED_UNKNOWN
|
||||
|
||||
tool = ExcellonTool(None, number=toolid, diameter=size,
|
||||
plated=plated)
|
||||
|
||||
self.tools[tool.number] = tool
|
||||
|
||||
break
|
||||
|
||||
def _convert_length(self, value, unit):
|
||||
|
||||
# Convert the value to mm
|
||||
if unit == 'mils':
|
||||
value /= 39.3700787402
|
||||
|
||||
# Now convert to the settings unit
|
||||
if self.settings.units == 'inch':
|
||||
return value / 25.4
|
||||
else:
|
||||
# Already in mm
|
||||
return value
|
||||
|
||||
def loads_rep(data, settings=None):
|
||||
""" Read tool report information generated by PADS and return a map of tools
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon Report file contents
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict tool name: ExcellonTool
|
||||
|
||||
"""
|
||||
return ExcellonReportParser(settings).parse_raw(data)
|
||||
|
||||
class ExcellonReportParser(object):
|
||||
|
||||
# We sometimes get files with different encoding, so we can't actually
|
||||
# match the text - the best we can do it detect the table header
|
||||
header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===')
|
||||
|
||||
def __init__(self, settings=None):
|
||||
self.tools = {}
|
||||
self.settings = settings
|
||||
|
||||
self.found_header = False
|
||||
|
||||
def parse_raw(self, data):
|
||||
for line in StringIO(data):
|
||||
self._parse(line.strip())
|
||||
|
||||
return self.tools
|
||||
|
||||
def _parse(self, line):
|
||||
|
||||
# skip empty lines and "comments"
|
||||
if not line.strip():
|
||||
return
|
||||
|
||||
if not self.found_header:
|
||||
# Try to find the heaader, since we need that to be sure we
|
||||
# understand the contents correctly.
|
||||
if ExcellonReportParser.header.match(line):
|
||||
self.found_header = True
|
||||
|
||||
elif line[0] != '=':
|
||||
# Already found the header, so we know to to map the contents
|
||||
parts = line.split()
|
||||
if len(parts) == 6:
|
||||
toolid = int(parts[0])
|
||||
size = float(parts[1])
|
||||
if parts[2] == 'x':
|
||||
plated = ExcellonTool.PLATED_YES
|
||||
elif parts[2] == '-':
|
||||
plated = ExcellonTool.PLATED_NO
|
||||
else:
|
||||
plated = ExcellonTool.PLATED_UNKNOWN
|
||||
feedrate = int(parts[3])
|
||||
speed = int(parts[4])
|
||||
qty = int(parts[5])
|
||||
|
||||
tool = ExcellonTool(None, number=toolid, diameter=size,
|
||||
plated=plated, feed_rate=feedrate,
|
||||
rpm=speed)
|
||||
|
||||
self.tools[tool.number] = tool
|
|
@ -0,0 +1,36 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GerberParseError(ParseError):
|
||||
pass
|
||||
|
||||
|
||||
class ExcellonParseError(ParseError):
|
||||
pass
|
||||
|
||||
|
||||
class ExcellonFileError(IOError):
|
||||
pass
|
||||
|
||||
|
||||
class GerberFileError(IOError):
|
||||
pass
|
|
@ -0,0 +1,485 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import math
|
||||
import re
|
||||
from .cam import CamFile, FileSettings
|
||||
from .primitives import TestRecord
|
||||
|
||||
# Net Name Variables
|
||||
_NNAME = re.compile(r'^NNAME\d+$')
|
||||
|
||||
# Board Edge Coordinates
|
||||
_COORD = re.compile(r'X?(?P<x>[\d\s]*)?Y?(?P<y>[\d\s]*)?')
|
||||
|
||||
_SM_FIELD = {
|
||||
'0': 'none',
|
||||
'1': 'primary side',
|
||||
'2': 'secondary side',
|
||||
'3': 'both'}
|
||||
|
||||
|
||||
def read(filename):
|
||||
""" Read data from filename and return an IPCNetlist
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of file to parse
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.ipc356.IPCNetlist`
|
||||
An IPCNetlist object created from the specified file.
|
||||
|
||||
"""
|
||||
# File object should use settings from source file by default.
|
||||
return IPCNetlist.from_file(filename)
|
||||
|
||||
|
||||
def loads(data, filename=None):
|
||||
""" Generate an IPCNetlist object from IPC-D-356 data in memory
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing netlist file contents
|
||||
|
||||
filename : string, optional
|
||||
string containing the filename of the data source
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.ipc356.IPCNetlist`
|
||||
An IPCNetlist created from the specified file.
|
||||
"""
|
||||
return IPCNetlistParser().parse_raw(data, filename)
|
||||
|
||||
|
||||
class IPCNetlist(CamFile):
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filename):
|
||||
parser = IPCNetlistParser()
|
||||
return parser.parse(filename)
|
||||
|
||||
def __init__(self, statements, settings, primitives=None, filename=None):
|
||||
self.statements = statements
|
||||
self.units = settings.units
|
||||
self.angle_units = settings.angle_units
|
||||
self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name,
|
||||
rec.access) for rec in self.test_records]
|
||||
self.filename = filename
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return FileSettings(units=self.units, angle_units=self.angle_units)
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return [record for record in self.statements
|
||||
if isinstance(record, IPC356_Comment)]
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
return [record for record in self.statements
|
||||
if isinstance(record, IPC356_Parameter)]
|
||||
|
||||
@property
|
||||
def test_records(self):
|
||||
return [record for record in self.statements
|
||||
if isinstance(record, IPC356_TestRecord)]
|
||||
|
||||
@property
|
||||
def nets(self):
|
||||
nets = []
|
||||
for net in list(set([rec.net_name for rec in self.test_records
|
||||
if rec.net_name is not None])):
|
||||
adjacent_nets = set()
|
||||
for record in self.adjacency_records:
|
||||
if record.net == net:
|
||||
adjacent_nets = adjacent_nets.update(record.adjacent_nets)
|
||||
elif net in record.adjacent_nets:
|
||||
adjacent_nets.add(record.net)
|
||||
nets.append(IPC356_Net(net, adjacent_nets))
|
||||
return nets
|
||||
|
||||
@property
|
||||
def components(self):
|
||||
return list(set([rec.id for rec in self.test_records
|
||||
if rec.id is not None and rec.id != 'VIA']))
|
||||
|
||||
@property
|
||||
def vias(self):
|
||||
return [rec.id for rec in self.test_records if rec.id == 'VIA']
|
||||
|
||||
@property
|
||||
def outlines(self):
|
||||
return [stmt for stmt in self.statements
|
||||
if isinstance(stmt, IPC356_Outline)]
|
||||
|
||||
@property
|
||||
def adjacency_records(self):
|
||||
return [record for record in self.statements
|
||||
if isinstance(record, IPC356_Adjacency)]
|
||||
|
||||
def render(self, ctx, layer='both', filename=None):
|
||||
for p in self.primitives:
|
||||
if layer == 'both' and p.layer in ('top', 'bottom', 'both'):
|
||||
ctx.render(p)
|
||||
elif layer == 'top' and p.layer in ('top', 'both'):
|
||||
ctx.render(p)
|
||||
elif layer == 'bottom' and p.layer in ('bottom', 'both'):
|
||||
ctx.render(p)
|
||||
if filename is not None:
|
||||
ctx.dump(filename)
|
||||
|
||||
|
||||
class IPCNetlistParser(object):
|
||||
# TODO: Allow multi-line statements (e.g. Altium board edge)
|
||||
|
||||
def __init__(self):
|
||||
self.units = 'inch'
|
||||
self.angle_units = 'degrees'
|
||||
self.statements = []
|
||||
self.nnames = {}
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return FileSettings(units=self.units, angle_units=self.angle_units)
|
||||
|
||||
def parse(self, filename):
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
return self.parse_raw(data, filename)
|
||||
|
||||
def parse_raw(self, data, filename=None):
|
||||
oldline = ''
|
||||
for line in data.splitlines():
|
||||
# Check for existing multiline data...
|
||||
if oldline != '':
|
||||
if len(line) and line[0] == '0':
|
||||
oldline = oldline.rstrip('\r\n') + line[3:].rstrip()
|
||||
else:
|
||||
self._parse_line(oldline)
|
||||
oldline = line
|
||||
else:
|
||||
oldline = line
|
||||
self._parse_line(oldline)
|
||||
|
||||
return IPCNetlist(self.statements, self.settings, filename=filename)
|
||||
|
||||
def _parse_line(self, line):
|
||||
if not len(line):
|
||||
return
|
||||
if line[0] == 'C':
|
||||
# Comment
|
||||
self.statements.append(IPC356_Comment.from_line(line))
|
||||
|
||||
elif line[0] == 'P':
|
||||
# Parameter
|
||||
p = IPC356_Parameter.from_line(line)
|
||||
if p.parameter == 'UNITS':
|
||||
if p.value in ('CUST', 'CUST 0'):
|
||||
self.units = 'inch'
|
||||
self.angle_units = 'degrees'
|
||||
elif p.value == 'CUST 1':
|
||||
self.units = 'metric'
|
||||
self.angle_units = 'degrees'
|
||||
elif p.value == 'CUST 2':
|
||||
self.units = 'inch'
|
||||
self.angle_units = 'radians'
|
||||
self.statements.append(p)
|
||||
if _NNAME.match(p.parameter):
|
||||
# Add to list of net name variables
|
||||
self.nnames[p.parameter] = p.value
|
||||
|
||||
elif line[0] == '9':
|
||||
self.statements.append(IPC356_EndOfFile())
|
||||
|
||||
elif line[0:3] in ('317', '327', '367'):
|
||||
# Test Record
|
||||
record = IPC356_TestRecord.from_line(line, self.settings)
|
||||
|
||||
# Substitute net name variables
|
||||
net = record.net_name
|
||||
if (_NNAME.match(net) and net in self.nnames.keys()):
|
||||
record.net_name = self.nnames[record.net_name]
|
||||
self.statements.append(record)
|
||||
|
||||
elif line[0:3] == '378':
|
||||
# Conductor
|
||||
self.statements.append(
|
||||
IPC356_Conductor.from_line(
|
||||
line, self.settings))
|
||||
|
||||
elif line[0:3] == '379':
|
||||
# Net Adjacency
|
||||
self.statements.append(IPC356_Adjacency.from_line(line))
|
||||
|
||||
elif line[0:3] == '389':
|
||||
# Outline
|
||||
self.statements.append(
|
||||
IPC356_Outline.from_line(
|
||||
line, self.settings))
|
||||
|
||||
|
||||
class IPC356_Comment(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line):
|
||||
if line[0] != 'C':
|
||||
raise ValueError('Not a valid comment statment')
|
||||
comment = line[2:].strip()
|
||||
return cls(comment)
|
||||
|
||||
def __init__(self, comment):
|
||||
self.comment = comment
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 Comment: %s>' % self.comment
|
||||
|
||||
|
||||
class IPC356_Parameter(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line):
|
||||
if line[0] != 'P':
|
||||
raise ValueError('Not a valid parameter statment')
|
||||
splitline = line[2:].split()
|
||||
parameter = splitline[0].strip()
|
||||
value = ' '.join(splitline[1:]).strip()
|
||||
return cls(parameter, value)
|
||||
|
||||
def __init__(self, parameter, value):
|
||||
self.parameter = parameter
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 Parameter: %s=%s>' % (self.parameter, self.value)
|
||||
|
||||
|
||||
class IPC356_TestRecord(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line, settings):
|
||||
offset = 0
|
||||
units = settings.units
|
||||
angle = settings.angle_units
|
||||
feature_types = {'1': 'through-hole', '2': 'smt',
|
||||
'3': 'tooling-feature', '4': 'tooling-hole',
|
||||
'6': 'non-plated-tooling-hole'}
|
||||
access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5',
|
||||
'layer6', 'layer7', 'bottom']
|
||||
record = {}
|
||||
line = line.strip()
|
||||
if line[0] != '3':
|
||||
raise ValueError('Not a valid test record statment')
|
||||
record['feature_type'] = feature_types[line[1]]
|
||||
|
||||
end = len(line) - 1 if len(line) < 18 else 17
|
||||
record['net_name'] = line[3:end].strip()
|
||||
|
||||
if len(line) >= 27 and line[26] != '-':
|
||||
offset = line[26:].find('-')
|
||||
offset = 0 if offset == -1 else offset
|
||||
end = len(line) - 1 if len(line) < (27 + offset) else (26 + offset)
|
||||
record['id'] = line[20:end].strip()
|
||||
|
||||
end = len(line) - 1 if len(line) < (32 + offset) else (31 + offset)
|
||||
record['pin'] = (line[27 + offset:end].strip() if line[27 + offset:end].strip() != ''
|
||||
else None)
|
||||
|
||||
record['location'] = 'middle' if line[31 + offset] == 'M' else 'end'
|
||||
if line[32 + offset] == 'D':
|
||||
end = len(line) - 1 if len(line) < (38 + offset) else (37 + offset)
|
||||
dia = int(line[33 + offset:end].strip())
|
||||
record['hole_diameter'] = (dia * 0.0001 if units == 'inch'
|
||||
else dia * 0.001)
|
||||
if len(line) >= (38 + offset):
|
||||
record['plated'] = (line[37 + offset] == 'P')
|
||||
|
||||
if len(line) >= (40 + offset):
|
||||
end = len(line) - 1 if len(line) < (42 + offset) else (41 + offset)
|
||||
record['access'] = access[int(line[39 + offset:end])]
|
||||
|
||||
if len(line) >= (43 + offset):
|
||||
end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset)
|
||||
coord = int(line[42 + offset:end].strip())
|
||||
record['x_coord'] = (coord * 0.0001 if units == 'inch'
|
||||
else coord * 0.001)
|
||||
|
||||
if len(line) >= (51 + offset):
|
||||
end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset)
|
||||
coord = int(line[50 + offset:end].strip())
|
||||
record['y_coord'] = (coord * 0.0001 if units == 'inch'
|
||||
else coord * 0.001)
|
||||
|
||||
if len(line) >= (59 + offset):
|
||||
end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset)
|
||||
dim = line[58 + offset:end].strip()
|
||||
if dim != '':
|
||||
record['rect_x'] = (int(dim) * 0.0001 if units == 'inch'
|
||||
else int(dim) * 0.001)
|
||||
|
||||
if len(line) >= (64 + offset):
|
||||
end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset)
|
||||
dim = line[63 + offset:end].strip()
|
||||
if dim != '':
|
||||
record['rect_y'] = (int(dim) * 0.0001 if units == 'inch'
|
||||
else int(dim) * 0.001)
|
||||
|
||||
if len(line) >= (69 + offset):
|
||||
end = len(line) - 1 if len(line) < (72 + offset) else (71 + offset)
|
||||
rot = line[68 + offset:end].strip()
|
||||
if rot != '':
|
||||
record['rect_rotation'] = (int(rot) if angle == 'degrees'
|
||||
else math.degrees(rot))
|
||||
|
||||
if len(line) >= (74 + offset):
|
||||
end = 74 + offset
|
||||
sm_info = line[73 + offset:end].strip()
|
||||
record['soldermask_info'] = _SM_FIELD.get(sm_info)
|
||||
|
||||
if len(line) >= (76 + offset):
|
||||
end = len(line) - 1 if len(line) < (80 + offset) else 79 + offset
|
||||
record['optional_info'] = line[75 + offset:end]
|
||||
|
||||
return cls(**record)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 %s Test Record: %s>' % (self.net_name,
|
||||
self.feature_type)
|
||||
|
||||
|
||||
class IPC356_Outline(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line, settings):
|
||||
type = line[3:17].strip()
|
||||
scale = 0.0001 if settings.units == 'inch' else 0.001
|
||||
points = []
|
||||
x = 0
|
||||
y = 0
|
||||
coord_strings = line.strip().split()[1:]
|
||||
for coord in coord_strings:
|
||||
coord_dict = _COORD.match(coord).groupdict()
|
||||
x = int(coord_dict['x']) if coord_dict['x'] is not '' else x
|
||||
y = int(coord_dict['y']) if coord_dict['y'] is not '' else y
|
||||
points.append((x * scale, y * scale))
|
||||
return cls(type, points)
|
||||
|
||||
def __init__(self, type, points):
|
||||
self.type = type
|
||||
self.points = points
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 %s Outline Definition>' % self.type
|
||||
|
||||
|
||||
class IPC356_Conductor(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line, settings):
|
||||
if line[0:3] != '378':
|
||||
raise ValueError('Not a valid IPC-D-356 Conductor statement')
|
||||
|
||||
scale = 0.0001 if settings.units == 'inch' else 0.001
|
||||
net_name = line[3:17].strip()
|
||||
layer = int(line[19:21])
|
||||
|
||||
# Parse out aperture definiting
|
||||
raw_aperture = line[22:].split()[0]
|
||||
aperture_dict = _COORD.match(raw_aperture).groupdict()
|
||||
x = 0
|
||||
y = 0
|
||||
x = int(aperture_dict['x']) * \
|
||||
scale if aperture_dict['x'] is not '' else None
|
||||
y = int(aperture_dict['y']) * \
|
||||
scale if aperture_dict['y'] is not '' else None
|
||||
aperture = (x, y)
|
||||
|
||||
# Parse out conductor shapes
|
||||
shapes = []
|
||||
coord_list = ' '.join(line[22:].split()[1:])
|
||||
raw_shapes = coord_list.split('*')
|
||||
for rshape in raw_shapes:
|
||||
x = 0
|
||||
y = 0
|
||||
shape = []
|
||||
coords = rshape.split()
|
||||
for coord in coords:
|
||||
coord_dict = _COORD.match(coord).groupdict()
|
||||
x = int(coord_dict['x']) if coord_dict['x'] is not '' else x
|
||||
y = int(coord_dict['y']) if coord_dict['y'] is not '' else y
|
||||
shape.append((x * scale, y * scale))
|
||||
shapes.append(tuple(shape))
|
||||
return cls(net_name, layer, aperture, tuple(shapes))
|
||||
|
||||
def __init__(self, net_name, layer, aperture, shapes):
|
||||
self.net_name = net_name
|
||||
self.layer = layer
|
||||
self.aperture = aperture
|
||||
self.shapes = shapes
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 %s Conductor Record>' % self.net_name
|
||||
|
||||
|
||||
class IPC356_Adjacency(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line):
|
||||
if line[0:3] != '379':
|
||||
raise ValueError('Not a valid IPC-D-356 Conductor statement')
|
||||
nets = line[3:].strip().split()
|
||||
|
||||
return cls(nets[0], nets[1:])
|
||||
|
||||
def __init__(self, net, adjacent_nets):
|
||||
self.net = net
|
||||
self.adjacent_nets = adjacent_nets
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 %s Adjacency Record>' % self.net
|
||||
|
||||
|
||||
class IPC356_EndOfFile(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def to_netlist(self):
|
||||
return '999'
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 EOF>'
|
||||
|
||||
|
||||
class IPC356_Net(object):
|
||||
|
||||
def __init__(self, name, adjacent_nets):
|
||||
self.name = name
|
||||
self.adjacent_nets = set(
|
||||
adjacent_nets) if adjacent_nets is not None else set()
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 Net %s>' % self.name
|
|
@ -0,0 +1,295 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
from . import common
|
||||
from .excellon import ExcellonFile
|
||||
from .ipc356 import IPCNetlist
|
||||
|
||||
|
||||
Hint = namedtuple('Hint', 'layer ext name regex content')
|
||||
|
||||
hints = [
|
||||
Hint(layer='top',
|
||||
ext=['gtl', 'cmp', 'top', ],
|
||||
name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', 'F.Cu', ],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='bottom',
|
||||
ext=['gbl', 'sld', 'bot', 'sol', 'bottom', ],
|
||||
name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', 'B.Cu', ],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='internal',
|
||||
ext=['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6',
|
||||
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', ],
|
||||
name=['art', 'internal', 'pgp', 'pwr', 'gnd', 'ground',
|
||||
'gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6',
|
||||
'In1.Cu', 'In2.Cu', 'In3.Cu', 'In4.Cu',
|
||||
'group3', 'group4', 'group5', 'group6', 'group7', 'group8', ],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='topsilk',
|
||||
ext=['gto', 'sst', 'plc', 'ts', 'skt', 'topsilk', ],
|
||||
name=['sst01', 'topsilk', 'silk', 'slk', 'sst', 'F.SilkS'],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='bottomsilk',
|
||||
ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', ],
|
||||
name=['bsilk', 'ssb', 'botsilk', 'bottomsilk', 'B.SilkS'],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='topmask',
|
||||
ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ],
|
||||
name=['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
|
||||
'mst', 'F.Mask', ],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='bottommask',
|
||||
ext=['gbs', 'sts', 'bmk', 'smb', 'br', 'bottommask', ],
|
||||
name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'bottommask',
|
||||
'msb', 'B.Mask', ],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='toppaste',
|
||||
ext=['gtp', 'tm', 'toppaste', ],
|
||||
name=['sp01', 'toppaste', 'pst', 'F.Paste'],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='bottompaste',
|
||||
ext=['gbp', 'bm', 'bottompaste', ],
|
||||
name=['sp02', 'botpaste', 'bottompaste', 'psb', 'B.Paste', ],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='outline',
|
||||
ext=['gko', 'outline', ],
|
||||
name=['BDR', 'border', 'out', 'outline', 'Edge.Cuts', ],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='ipc_netlist',
|
||||
ext=['ipc'],
|
||||
name=[],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
Hint(layer='drawing',
|
||||
ext=['fab'],
|
||||
name=['assembly drawing', 'assembly', 'fabrication',
|
||||
'fab drawing', 'fab'],
|
||||
regex='',
|
||||
content=[]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def layer_signatures(layer_class):
|
||||
for hint in hints:
|
||||
if hint.layer == layer_class:
|
||||
return hint.ext + hint.name
|
||||
return []
|
||||
|
||||
|
||||
def load_layer(filename):
|
||||
return PCBLayer.from_cam(common.read(filename))
|
||||
|
||||
|
||||
def load_layer_data(data, filename=None):
|
||||
return PCBLayer.from_cam(common.loads(data, filename))
|
||||
|
||||
|
||||
def guess_layer_class(filename):
|
||||
try:
|
||||
layer = guess_layer_class_by_content(filename)
|
||||
if layer:
|
||||
return layer
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
directory, filename = os.path.split(filename)
|
||||
name, ext = os.path.splitext(filename.lower())
|
||||
for hint in hints:
|
||||
if hint.regex:
|
||||
if re.findall(hint.regex, filename, re.IGNORECASE):
|
||||
return hint.layer
|
||||
|
||||
patterns = [r'^(\w*[.-])*{}([.-]\w*)?$'.format(x) for x in hint.name]
|
||||
if ext[1:] in hint.ext or any(re.findall(p, name, re.IGNORECASE) for p in patterns):
|
||||
return hint.layer
|
||||
except:
|
||||
pass
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def guess_layer_class_by_content(filename):
|
||||
try:
|
||||
file = open(filename, 'r')
|
||||
for line in file:
|
||||
for hint in hints:
|
||||
if len(hint.content) > 0:
|
||||
patterns = [r'^(.*){}(.*)$'.format(x) for x in hint.content]
|
||||
if any(re.findall(p, line, re.IGNORECASE) for p in patterns):
|
||||
return hint.layer
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def sort_layers(layers, from_top=True):
|
||||
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
|
||||
'internal', 'bottom', 'bottommask', 'bottomsilk',
|
||||
'bottompaste']
|
||||
append_after = ['drill', 'drawing']
|
||||
|
||||
output = []
|
||||
drill_layers = [layer for layer in layers if layer.layer_class == 'drill']
|
||||
internal_layers = list(sorted([layer for layer in layers
|
||||
if layer.layer_class == 'internal']))
|
||||
|
||||
for layer_class in layer_order:
|
||||
if layer_class == 'internal':
|
||||
output += internal_layers
|
||||
elif layer_class == 'drill':
|
||||
output += drill_layers
|
||||
else:
|
||||
for layer in layers:
|
||||
if layer.layer_class == layer_class:
|
||||
output.append(layer)
|
||||
if not from_top:
|
||||
output = list(reversed(output))
|
||||
|
||||
for layer_class in append_after:
|
||||
for layer in layers:
|
||||
if layer.layer_class == layer_class:
|
||||
output.append(layer)
|
||||
return output
|
||||
|
||||
|
||||
class PCBLayer(object):
|
||||
""" Base class for PCB Layers
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : CAMFile
|
||||
CAMFile representing the layer
|
||||
|
||||
|
||||
Attributes
|
||||
----------
|
||||
filename : string
|
||||
Source Filename
|
||||
|
||||
"""
|
||||
@classmethod
|
||||
def from_cam(cls, camfile):
|
||||
filename = camfile.filename
|
||||
layer_class = guess_layer_class(filename)
|
||||
if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'):
|
||||
return DrillLayer.from_cam(camfile)
|
||||
elif layer_class == 'internal':
|
||||
return InternalLayer.from_cam(camfile)
|
||||
if isinstance(camfile, IPCNetlist):
|
||||
layer_class = 'ipc_netlist'
|
||||
return cls(filename, layer_class, camfile)
|
||||
|
||||
def __init__(self, filename=None, layer_class=None, cam_source=None, **kwargs):
|
||||
super(PCBLayer, self).__init__(**kwargs)
|
||||
self.filename = filename
|
||||
self.layer_class = layer_class
|
||||
self.cam_source = cam_source
|
||||
self.surface = None
|
||||
self.primitives = cam_source.primitives if cam_source is not None else []
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
if self.cam_source is not None:
|
||||
return self.cam_source.bounds
|
||||
else:
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return '<PCBLayer: {}>'.format(self.layer_class)
|
||||
|
||||
|
||||
class DrillLayer(PCBLayer):
|
||||
@classmethod
|
||||
def from_cam(cls, camfile):
|
||||
return cls(camfile.filename, camfile)
|
||||
|
||||
def __init__(self, filename=None, cam_source=None, layers=None, **kwargs):
|
||||
super(DrillLayer, self).__init__(filename, 'drill', cam_source, **kwargs)
|
||||
self.layers = layers if layers is not None else ['top', 'bottom']
|
||||
|
||||
|
||||
class InternalLayer(PCBLayer):
|
||||
|
||||
@classmethod
|
||||
def from_cam(cls, camfile):
|
||||
filename = camfile.filename
|
||||
try:
|
||||
order = int(re.search(r'\d+', filename).group())
|
||||
except AttributeError:
|
||||
order = 0
|
||||
return cls(filename, camfile, order)
|
||||
|
||||
def __init__(self, filename=None, cam_source=None, order=0, **kwargs):
|
||||
super(InternalLayer, self).__init__(filename, 'internal', cam_source, **kwargs)
|
||||
self.order = order
|
||||
|
||||
def __eq__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order == other.order)
|
||||
|
||||
def __ne__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order != other.order)
|
||||
|
||||
def __gt__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order > other.order)
|
||||
|
||||
def __lt__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order < other.order)
|
||||
|
||||
def __ge__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order >= other.order)
|
||||
|
||||
def __le__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order <= other.order)
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Allegro File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
|
||||
"""
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
CAM File Operations
|
||||
===================
|
||||
**Transformations and other operations performed on Gerber and Excellon files**
|
||||
|
||||
"""
|
||||
import copy
|
||||
|
||||
|
||||
def to_inch(cam_file):
|
||||
""" Convert Gerber or Excellon file units to imperial
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
Gerber or Excellon file to convert
|
||||
|
||||
Returns
|
||||
-------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
A deep copy of the source file with units converted to imperial.
|
||||
"""
|
||||
cam_file = copy.deepcopy(cam_file)
|
||||
cam_file.to_inch()
|
||||
return cam_file
|
||||
|
||||
|
||||
def to_metric(cam_file):
|
||||
""" Convert Gerber or Excellon file units to metric
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
Gerber or Excellon file to convert
|
||||
|
||||
Returns
|
||||
-------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
A deep copy of the source file with units converted to metric.
|
||||
"""
|
||||
cam_file = copy.deepcopy(cam_file)
|
||||
cam_file.to_metric()
|
||||
return cam_file
|
||||
|
||||
|
||||
def offset(cam_file, x_offset, y_offset):
|
||||
""" Offset a Cam file by a specified amount in the X and Y directions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
Gerber or Excellon file to offset
|
||||
|
||||
x_offset : float
|
||||
Amount to offset the file in the X direction
|
||||
|
||||
y_offset : float
|
||||
Amount to offset the file in the Y direction
|
||||
|
||||
Returns
|
||||
-------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
An offset deep copy of the source file.
|
||||
"""
|
||||
cam_file = copy.deepcopy(cam_file)
|
||||
cam_file.offset(x_offset, y_offset)
|
||||
return cam_file
|
||||
|
||||
|
||||
def scale(cam_file, x_scale, y_scale):
|
||||
""" Scale a Cam file by a specified amount in the X and Y directions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
Gerber or Excellon file to scale
|
||||
|
||||
x_scale : float
|
||||
X-axis scale factor
|
||||
|
||||
y_scale : float
|
||||
Y-axis scale factor
|
||||
|
||||
Returns
|
||||
-------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
An scaled deep copy of the source file.
|
||||
"""
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
def rotate(cam_file, angle):
|
||||
""" Rotate a Cam file a specified amount about the origin.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
Gerber or Excellon file to rotate
|
||||
|
||||
angle : float
|
||||
Angle to rotate the file in degrees.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cam_file : :class:`gerber.cam.CamFile` subclass
|
||||
An rotated deep copy of the source file.
|
||||
"""
|
||||
# TODO
|
||||
pass
|
|
@ -0,0 +1,124 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import os
|
||||
from .exceptions import ParseError
|
||||
from .layers import PCBLayer, sort_layers, layer_signatures
|
||||
from .common import read as gerber_read
|
||||
from .utils import listdir
|
||||
|
||||
|
||||
class PCB(object):
|
||||
|
||||
@classmethod
|
||||
def from_directory(cls, directory, board_name=None, verbose=False):
|
||||
layers = []
|
||||
names = set()
|
||||
|
||||
# Validate
|
||||
directory = os.path.abspath(directory)
|
||||
if not os.path.isdir(directory):
|
||||
raise TypeError('{} is not a directory.'.format(directory))
|
||||
|
||||
# Load gerber files
|
||||
for filename in listdir(directory, True, True):
|
||||
try:
|
||||
camfile = gerber_read(os.path.join(directory, filename))
|
||||
layer = PCBLayer.from_cam(camfile)
|
||||
layers.append(layer)
|
||||
name = os.path.splitext(filename)[0]
|
||||
if len(os.path.splitext(filename)) > 1:
|
||||
_name, ext = os.path.splitext(name)
|
||||
if ext[1:] in layer_signatures(layer.layer_class):
|
||||
name = _name
|
||||
if layer.layer_class == 'drill' and 'drill' in ext:
|
||||
name = _name
|
||||
names.add(name)
|
||||
if verbose:
|
||||
print('[PCB]: Added {} layer <{}>'.format(layer.layer_class,
|
||||
filename))
|
||||
except ParseError:
|
||||
if verbose:
|
||||
print('[PCB]: Skipping file {}'.format(filename))
|
||||
except IOError:
|
||||
if verbose:
|
||||
print('[PCB]: Skipping file {}'.format(filename))
|
||||
|
||||
# Try to guess board name
|
||||
if board_name is None:
|
||||
if len(names) == 1:
|
||||
board_name = names.pop()
|
||||
else:
|
||||
board_name = os.path.basename(directory)
|
||||
# Return PCB
|
||||
return cls(layers, board_name)
|
||||
|
||||
def __init__(self, layers, name=None):
|
||||
self.layers = sort_layers(layers)
|
||||
self.name = name
|
||||
|
||||
def __len__(self):
|
||||
return len(self.layers)
|
||||
|
||||
@property
|
||||
def top_layers(self):
|
||||
board_layers = [l for l in reversed(self.layers) if l.layer_class in
|
||||
('topsilk', 'topmask', 'top')]
|
||||
drill_layers = [l for l in self.drill_layers if 'top' in l.layers]
|
||||
# Drill layer goes under soldermask for proper rendering of tented vias
|
||||
return [board_layers[0]] + drill_layers + board_layers[1:]
|
||||
|
||||
@property
|
||||
def bottom_layers(self):
|
||||
board_layers = [l for l in self.layers if l.layer_class in
|
||||
('bottomsilk', 'bottommask', 'bottom')]
|
||||
drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers]
|
||||
# Drill layer goes under soldermask for proper rendering of tented vias
|
||||
return [board_layers[0]] + drill_layers + board_layers[1:]
|
||||
|
||||
@property
|
||||
def drill_layers(self):
|
||||
return [l for l in self.layers if l.layer_class == 'drill']
|
||||
|
||||
@property
|
||||
def copper_layers(self):
|
||||
return list(reversed([layer for layer in self.layers if
|
||||
layer.layer_class in
|
||||
('top', 'bottom', 'internal')]))
|
||||
|
||||
@property
|
||||
def outline_layer(self):
|
||||
for layer in self.layers:
|
||||
if layer.layer_class == 'outline':
|
||||
return layer
|
||||
|
||||
@property
|
||||
def layer_count(self):
|
||||
""" Number of *COPPER* layers
|
||||
"""
|
||||
return len([l for l in self.layers if l.layer_class in
|
||||
('top', 'bottom', 'internal')])
|
||||
|
||||
@property
|
||||
def board_bounds(self):
|
||||
for layer in self.layers:
|
||||
if layer.layer_class == 'outline':
|
||||
return layer.bounds
|
||||
for layer in self.layers:
|
||||
if layer.layer_class == 'top':
|
||||
return layer.bounds
|
|
@ -0,0 +1,31 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
gerber.render
|
||||
============
|
||||
**Gerber Renderers**
|
||||
|
||||
This module provides contexts for rendering images of gerber layers. Currently
|
||||
SVG is the only supported format.
|
||||
"""
|
||||
|
||||
from .render import RenderSettings
|
||||
from .cairo_backend import GerberCairoContext
|
||||
|
||||
available_renderers = {
|
||||
'cairo': GerberCairoContext,
|
||||
}
|
|
@ -0,0 +1,616 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
try:
|
||||
import cairo
|
||||
except ImportError:
|
||||
import cairocffi as cairo
|
||||
|
||||
from operator import mul
|
||||
import tempfile
|
||||
import copy
|
||||
import os
|
||||
|
||||
from .render import GerberContext, RenderSettings
|
||||
from .theme import THEMES
|
||||
from ..primitives import *
|
||||
from ..utils import rotate_point
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class GerberCairoContext(GerberContext):
|
||||
|
||||
def __init__(self, scale=300):
|
||||
super(GerberCairoContext, self).__init__()
|
||||
self.scale = (scale, scale)
|
||||
self.surface = None
|
||||
self.surface_buffer = None
|
||||
self.ctx = None
|
||||
self.active_layer = None
|
||||
self.active_matrix = None
|
||||
self.output_ctx = None
|
||||
self.has_bg = False
|
||||
self.origin_in_inch = None
|
||||
self.size_in_inch = None
|
||||
self._xform_matrix = None
|
||||
self._render_count = 0
|
||||
|
||||
@property
|
||||
def origin_in_pixels(self):
|
||||
return (self.scale_point(self.origin_in_inch)
|
||||
if self.origin_in_inch is not None else (0.0, 0.0))
|
||||
|
||||
@property
|
||||
def size_in_pixels(self):
|
||||
return (self.scale_point(self.size_in_inch)
|
||||
if self.size_in_inch is not None else (0.0, 0.0))
|
||||
|
||||
def set_bounds(self, bounds, new_surface=False):
|
||||
origin_in_inch = (bounds[0][0], bounds[1][0])
|
||||
size_in_inch = (abs(bounds[0][1] - bounds[0][0]),
|
||||
abs(bounds[1][1] - bounds[1][0]))
|
||||
size_in_pixels = self.scale_point(size_in_inch)
|
||||
self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch
|
||||
self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch
|
||||
self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0,
|
||||
x0=-self.origin_in_pixels[0],
|
||||
y0=self.size_in_pixels[1])
|
||||
if (self.surface is None) or new_surface:
|
||||
self.surface_buffer = tempfile.NamedTemporaryFile()
|
||||
self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
|
||||
self.output_ctx = cairo.Context(self.surface)
|
||||
|
||||
def render_layer(self, layer, filename=None, settings=None, bgsettings=None,
|
||||
verbose=False, bounds=None):
|
||||
if settings is None:
|
||||
settings = THEMES['default'].get(layer.layer_class, RenderSettings())
|
||||
if bgsettings is None:
|
||||
bgsettings = THEMES['default'].get('background', RenderSettings())
|
||||
|
||||
if self._render_count == 0:
|
||||
if verbose:
|
||||
print('[Render]: Rendering Background.')
|
||||
self.clear()
|
||||
if bounds is not None:
|
||||
self.set_bounds(bounds)
|
||||
else:
|
||||
self.set_bounds(layer.bounds)
|
||||
self.paint_background(bgsettings)
|
||||
if verbose:
|
||||
print('[Render]: Rendering {} Layer.'.format(layer.layer_class))
|
||||
self._render_count += 1
|
||||
self._render_layer(layer, settings)
|
||||
if filename is not None:
|
||||
self.dump(filename, verbose)
|
||||
|
||||
def render_layers(self, layers, filename, theme=THEMES['default'],
|
||||
verbose=False, max_width=800, max_height=600):
|
||||
""" Render a set of layers
|
||||
"""
|
||||
# Calculate scale parameter
|
||||
x_range = [10000, -10000]
|
||||
y_range = [10000, -10000]
|
||||
for layer in layers:
|
||||
bounds = layer.bounds
|
||||
if bounds is not None:
|
||||
layer_x, layer_y = bounds
|
||||
x_range[0] = min(x_range[0], layer_x[0])
|
||||
x_range[1] = max(x_range[1], layer_x[1])
|
||||
y_range[0] = min(y_range[0], layer_y[0])
|
||||
y_range[1] = max(y_range[1], layer_y[1])
|
||||
width = x_range[1] - x_range[0]
|
||||
height = y_range[1] - y_range[0]
|
||||
|
||||
scale = math.floor(min(float(max_width)/width, float(max_height)/height))
|
||||
self.scale = (scale, scale)
|
||||
|
||||
self.clear()
|
||||
|
||||
# Render layers
|
||||
bgsettings = theme['background']
|
||||
for layer in layers:
|
||||
settings = theme.get(layer.layer_class, RenderSettings())
|
||||
self.render_layer(layer, settings=settings, bgsettings=bgsettings,
|
||||
verbose=verbose)
|
||||
self.dump(filename, verbose)
|
||||
|
||||
def dump(self, filename=None, verbose=False):
|
||||
""" Save image as `filename`
|
||||
"""
|
||||
try:
|
||||
is_svg = os.path.splitext(filename.lower())[1] == '.svg'
|
||||
except:
|
||||
is_svg = False
|
||||
if verbose:
|
||||
print('[Render]: Writing image to {}'.format(filename))
|
||||
if is_svg:
|
||||
self.surface.finish()
|
||||
self.surface_buffer.flush()
|
||||
with open(filename, "wb") as f:
|
||||
self.surface_buffer.seek(0)
|
||||
f.write(self.surface_buffer.read())
|
||||
f.flush()
|
||||
else:
|
||||
return self.surface.write_to_png(filename)
|
||||
|
||||
def dump_str(self):
|
||||
""" Return a byte-string containing the rendered image.
|
||||
"""
|
||||
fobj = BytesIO()
|
||||
self.surface.write_to_png(fobj)
|
||||
return fobj.getvalue()
|
||||
|
||||
def dump_svg_str(self):
|
||||
""" Return a string containg the rendered SVG.
|
||||
"""
|
||||
self.surface.finish()
|
||||
self.surface_buffer.flush()
|
||||
return self.surface_buffer.read()
|
||||
|
||||
def clear(self):
|
||||
self.surface = None
|
||||
self.output_ctx = None
|
||||
self.has_bg = False
|
||||
self.origin_in_inch = None
|
||||
self.size_in_inch = None
|
||||
self._xform_matrix = None
|
||||
self._render_count = 0
|
||||
self.surface_buffer = None
|
||||
|
||||
def _new_mask(self):
|
||||
class Mask:
|
||||
def __enter__(msk):
|
||||
size_in_pixels = self.size_in_pixels
|
||||
msk.surface = cairo.SVGSurface(None, size_in_pixels[0],
|
||||
size_in_pixels[1])
|
||||
msk.ctx = cairo.Context(msk.surface)
|
||||
msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1])
|
||||
return msk
|
||||
|
||||
|
||||
def __exit__(msk, exc_type, exc_val, traceback):
|
||||
if hasattr(msk.surface, 'finish'):
|
||||
msk.surface.finish()
|
||||
|
||||
return Mask()
|
||||
|
||||
def _render_layer(self, layer, settings):
|
||||
self.invert = settings.invert
|
||||
# Get a new clean layer to render on
|
||||
self.new_render_layer(mirror=settings.mirror)
|
||||
for prim in layer.primitives:
|
||||
self.render(prim)
|
||||
# Add layer to image
|
||||
self.flatten(settings.color, settings.alpha)
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = self.scale_point(line.start)
|
||||
end = self.scale_point(line.end)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if (not self.invert)
|
||||
and line.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
|
||||
with self._clip_primitive(line):
|
||||
with self._new_mask() as mask:
|
||||
if isinstance(line.aperture, Circle):
|
||||
width = line.aperture.diameter
|
||||
mask.ctx.set_line_width(width * self.scale[0])
|
||||
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
mask.ctx.move_to(*start)
|
||||
mask.ctx.line_to(*end)
|
||||
mask.ctx.stroke()
|
||||
|
||||
elif hasattr(line, 'vertices') and line.vertices is not None:
|
||||
points = [self.scale_point(x) for x in line.vertices]
|
||||
mask.ctx.set_line_width(0)
|
||||
mask.ctx.move_to(*points[-1])
|
||||
for point in points:
|
||||
mask.ctx.line_to(*point)
|
||||
mask.ctx.fill()
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_arc(self, arc, color):
|
||||
center = self.scale_point(arc.center)
|
||||
start = self.scale_point(arc.start)
|
||||
end = self.scale_point(arc.end)
|
||||
radius = self.scale[0] * arc.radius
|
||||
two_pi = 2 * math.pi
|
||||
angle1 = (arc.start_angle + two_pi) % two_pi
|
||||
angle2 = (arc.end_angle + two_pi) % two_pi
|
||||
if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant':
|
||||
# Make the angles slightly different otherwise Cario will draw nothing
|
||||
angle2 -= 0.000000001
|
||||
if isinstance(arc.aperture, Circle):
|
||||
width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
|
||||
else:
|
||||
width = max(arc.aperture.width, arc.aperture.height, 0.001)
|
||||
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if (not self.invert)
|
||||
and arc.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
with self._clip_primitive(arc):
|
||||
with self._new_mask() as mask:
|
||||
mask.ctx.set_line_width(width * self.scale[0])
|
||||
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND if isinstance(arc.aperture, Circle) else cairo.LINE_CAP_SQUARE)
|
||||
mask.ctx.move_to(*start) # You actually have to do this...
|
||||
if arc.direction == 'counterclockwise':
|
||||
mask.ctx.arc(center[0], center[1], radius, angle1, angle2)
|
||||
else:
|
||||
mask.ctx.arc_negative(center[0], center[1], radius,
|
||||
angle1, angle2)
|
||||
mask.ctx.move_to(*end) # ...lame
|
||||
mask.ctx.stroke()
|
||||
|
||||
#if isinstance(arc.aperture, Rectangle):
|
||||
# print("Flash Rectangle Ends")
|
||||
# print(arc.aperture.rotation * 180/math.pi)
|
||||
# rect = arc.aperture
|
||||
# width = self.scale[0] * rect.width
|
||||
# height = self.scale[1] * rect.height
|
||||
# for point, angle in zip((start, end), (angle1, angle2)):
|
||||
# print("{} w {} h{}".format(point, rect.width, rect.height))
|
||||
# mask.ctx.rectangle(point[0] - width/2.0,
|
||||
# point[1] - height/2.0, width, height)
|
||||
# mask.ctx.fill()
|
||||
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_region(self, region, color):
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if (not self.invert) and region.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
with self._clip_primitive(region):
|
||||
with self._new_mask() as mask:
|
||||
mask.ctx.set_line_width(0)
|
||||
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
mask.ctx.move_to(*self.scale_point(region.primitives[0].start))
|
||||
for prim in region.primitives:
|
||||
if isinstance(prim, Line):
|
||||
mask.ctx.line_to(*self.scale_point(prim.end))
|
||||
else:
|
||||
center = self.scale_point(prim.center)
|
||||
radius = self.scale[0] * prim.radius
|
||||
angle1 = prim.start_angle
|
||||
angle2 = prim.end_angle
|
||||
if prim.direction == 'counterclockwise':
|
||||
mask.ctx.arc(center[0], center[1], radius,
|
||||
angle1, angle2)
|
||||
else:
|
||||
mask.ctx.arc_negative(center[0], center[1], radius,
|
||||
angle1, angle2)
|
||||
mask.ctx.fill()
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
center = self.scale_point(circle.position)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if (not self.invert)
|
||||
and circle.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
with self._clip_primitive(circle):
|
||||
with self._new_mask() as mask:
|
||||
mask.ctx.set_line_width(0)
|
||||
mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi))
|
||||
mask.ctx.fill()
|
||||
|
||||
if hasattr(circle, 'hole_diameter') and circle.hole_diameter is not None and circle.hole_diameter > 0:
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
mask.ctx.arc(center[0], center[1], circle.hole_radius * self.scale[0], 0, 2 * math.pi)
|
||||
mask.ctx.fill()
|
||||
|
||||
if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height')
|
||||
and circle.hole_width is not None and circle.hole_height is not None
|
||||
and circle.hole_width > 0 and circle.hole_height > 0):
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if circle.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_OVER)
|
||||
width, height = self.scale_point((circle.hole_width, circle.hole_height))
|
||||
lower_left = rotate_point(
|
||||
(center[0] - width / 2.0, center[1] - height / 2.0),
|
||||
circle.rotation, center)
|
||||
lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
|
||||
circle.rotation, center)
|
||||
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
|
||||
circle.rotation, center)
|
||||
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
|
||||
circle.rotation, center)
|
||||
points = (lower_left, lower_right, upper_right, upper_left)
|
||||
mask.ctx.move_to(*points[-1])
|
||||
for point in points:
|
||||
mask.ctx.line_to(*point)
|
||||
mask.ctx.fill()
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
lower_left = self.scale_point(rectangle.lower_left)
|
||||
width, height = tuple([abs(coord) for coord in
|
||||
self.scale_point((rectangle.width,
|
||||
rectangle.height))])
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if (not self.invert)
|
||||
and rectangle.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
with self._clip_primitive(rectangle):
|
||||
with self._new_mask() as mask:
|
||||
mask.ctx.set_line_width(0)
|
||||
mask.ctx.rectangle(lower_left[0], lower_left[1], width, height)
|
||||
mask.ctx.fill()
|
||||
|
||||
center = self.scale_point(rectangle.position)
|
||||
if rectangle.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if rectangle.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_OVER)
|
||||
|
||||
mask.ctx.arc(center[0], center[1], rectangle.hole_radius * self.scale[0], 0, 2 * math.pi)
|
||||
mask.ctx.fill()
|
||||
|
||||
if rectangle.hole_width > 0 and rectangle.hole_height > 0:
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if rectangle.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_OVER)
|
||||
width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height))
|
||||
lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center)
|
||||
lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center)
|
||||
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center)
|
||||
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center)
|
||||
points = (lower_left, lower_right, upper_right, upper_left)
|
||||
mask.ctx.move_to(*points[-1])
|
||||
for point in points:
|
||||
mask.ctx.line_to(*point)
|
||||
mask.ctx.fill()
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if (not self.invert)
|
||||
and obround.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
with self._clip_primitive(obround):
|
||||
with self._new_mask() as mask:
|
||||
mask.ctx.set_line_width(0)
|
||||
|
||||
# Render circles
|
||||
for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']):
|
||||
center = self.scale_point(circle.position)
|
||||
mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi))
|
||||
mask.ctx.fill()
|
||||
|
||||
# Render Rectangle
|
||||
rectangle = obround.subshapes['rectangle']
|
||||
lower_left = self.scale_point(rectangle.lower_left)
|
||||
width, height = tuple([abs(coord) for coord in
|
||||
self.scale_point((rectangle.width,
|
||||
rectangle.height))])
|
||||
mask.ctx.rectangle(lower_left[0], lower_left[1], width, height)
|
||||
mask.ctx.fill()
|
||||
|
||||
center = self.scale_point(obround.position)
|
||||
if obround.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
mask.ctx.arc(center[0], center[1], obround.hole_radius * self.scale[0], 0, 2 * math.pi)
|
||||
mask.ctx.fill()
|
||||
|
||||
if obround.hole_width > 0 and obround.hole_height > 0:
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if rectangle.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_OVER)
|
||||
width, height =self.scale_point((obround.hole_width, obround.hole_height))
|
||||
lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0),
|
||||
obround.rotation, center)
|
||||
lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
|
||||
obround.rotation, center)
|
||||
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
|
||||
obround.rotation, center)
|
||||
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
|
||||
obround.rotation, center)
|
||||
points = (lower_left, lower_right, upper_right, upper_left)
|
||||
mask.ctx.move_to(*points[-1])
|
||||
for point in points:
|
||||
mask.ctx.line_to(*point)
|
||||
mask.ctx.fill()
|
||||
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if (not self.invert)
|
||||
and polygon.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
with self._clip_primitive(polygon):
|
||||
with self._new_mask() as mask:
|
||||
|
||||
vertices = polygon.vertices
|
||||
mask.ctx.set_line_width(0)
|
||||
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
# Start from before the end so it is easy to iterate and make sure
|
||||
# it is closed
|
||||
mask.ctx.move_to(*self.scale_point(vertices[-1]))
|
||||
for v in vertices:
|
||||
mask.ctx.line_to(*self.scale_point(v))
|
||||
mask.ctx.fill()
|
||||
|
||||
center = self.scale_point(polygon.position)
|
||||
if polygon.hole_radius > 0:
|
||||
# Render the center clear
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if polygon.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_OVER)
|
||||
mask.ctx.set_line_width(0)
|
||||
mask.ctx.arc(center[0],
|
||||
center[1],
|
||||
polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
|
||||
mask.ctx.fill()
|
||||
|
||||
if polygon.hole_width > 0 and polygon.hole_height > 0:
|
||||
mask.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if polygon.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_OVER)
|
||||
width, height = self.scale_point((polygon.hole_width, polygon.hole_height))
|
||||
lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0),
|
||||
polygon.rotation, center)
|
||||
lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
|
||||
polygon.rotation, center)
|
||||
upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
|
||||
polygon.rotation, center)
|
||||
upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
|
||||
polygon.rotation, center)
|
||||
points = (lower_left, lower_right, upper_right, upper_left)
|
||||
mask.ctx.move_to(*points[-1])
|
||||
for point in points:
|
||||
mask.ctx.line_to(*point)
|
||||
mask.ctx.fill()
|
||||
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_drill(self, circle, color=None):
|
||||
color = color if color is not None else self.drill_color
|
||||
self._render_circle(circle, color)
|
||||
|
||||
def _render_slot(self, slot, color):
|
||||
start = map(mul, slot.start, self.scale)
|
||||
end = map(mul, slot.end, self.scale)
|
||||
|
||||
width = slot.diameter
|
||||
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if slot.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
with self._clip_primitive(slot):
|
||||
with self._new_mask() as mask:
|
||||
mask.ctx.set_line_width(width * self.scale[0])
|
||||
mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
mask.ctx.move_to(*start)
|
||||
mask.ctx.line_to(*end)
|
||||
mask.ctx.stroke()
|
||||
self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
|
||||
|
||||
def _render_amgroup(self, amgroup, color):
|
||||
for primitive in amgroup.primitives:
|
||||
self.render(primitive)
|
||||
|
||||
def _render_test_record(self, primitive, color):
|
||||
position = [pos + origin for pos, origin in
|
||||
zip(primitive.position, self.origin_in_inch)]
|
||||
self.ctx.select_font_face(
|
||||
'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
|
||||
self.ctx.set_font_size(13)
|
||||
self._render_circle(Circle(position, 0.015), color)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if primitive.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position])
|
||||
self.ctx.scale(1, -1)
|
||||
self.ctx.show_text(primitive.net_name)
|
||||
self.ctx.scale(1, -1)
|
||||
|
||||
def new_render_layer(self, color=None, mirror=False):
|
||||
size_in_pixels = self.scale_point(self.size_in_inch)
|
||||
matrix = copy.copy(self._xform_matrix)
|
||||
layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
|
||||
ctx = cairo.Context(layer)
|
||||
|
||||
if self.invert:
|
||||
ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
ctx.paint()
|
||||
if mirror:
|
||||
matrix.xx = -1.0
|
||||
matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0]
|
||||
self.ctx = ctx
|
||||
self.ctx.set_matrix(matrix)
|
||||
self.active_layer = layer
|
||||
self.active_matrix = matrix
|
||||
|
||||
def flatten(self, color=None, alpha=None):
|
||||
color = color if color is not None else self.color
|
||||
alpha = alpha if alpha is not None else self.alpha
|
||||
self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha)
|
||||
self.output_ctx.mask_surface(self.active_layer)
|
||||
self.ctx = None
|
||||
self.active_layer = None
|
||||
self.active_matrix = None
|
||||
|
||||
def paint_background(self, settings=None):
|
||||
color = settings.color if settings is not None else self.background_color
|
||||
alpha = settings.alpha if settings is not None else 1.0
|
||||
if not self.has_bg:
|
||||
self.has_bg = True
|
||||
self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha)
|
||||
self.output_ctx.paint()
|
||||
|
||||
def _clip_primitive(self, primitive):
|
||||
""" Clip rendering context to pixel-aligned bounding box
|
||||
|
||||
Calculates pixel- and axis- aligned bounding box, and clips current
|
||||
context to that region. Improves rendering speed significantly. This
|
||||
returns a context manager, use as follows:
|
||||
|
||||
with self._clip_primitive(some_primitive):
|
||||
do_rendering_stuff()
|
||||
do_more_rendering stuff(with, arguments)
|
||||
|
||||
The context manager will reset the context's clipping region when it
|
||||
goes out of scope.
|
||||
|
||||
"""
|
||||
class Clip:
|
||||
def __init__(clp, primitive):
|
||||
x_range, y_range = primitive.bounding_box
|
||||
xmin, xmax = x_range
|
||||
ymin, ymax = y_range
|
||||
|
||||
# Round bounds to the nearest pixel outside of the primitive
|
||||
clp.xmin = math.floor(self.scale[0] * xmin)
|
||||
clp.xmax = math.ceil(self.scale[0] * xmax)
|
||||
|
||||
# We need to offset Y to take care of the difference in y-pos
|
||||
# caused by flipping the axis.
|
||||
clp.ymin = math.floor(
|
||||
(self.scale[1] * ymin) - math.ceil(self.origin_in_pixels[1]))
|
||||
clp.ymax = math.floor(
|
||||
(self.scale[1] * ymax) - math.floor(self.origin_in_pixels[1]))
|
||||
|
||||
# Calculate width and height, rounded to the nearest pixel
|
||||
clp.width = abs(clp.xmax - clp.xmin)
|
||||
clp.height = abs(clp.ymax - clp.ymin)
|
||||
|
||||
def __enter__(clp):
|
||||
# Clip current context to primitive's bounding box
|
||||
self.ctx.rectangle(clp.xmin, clp.ymin, clp.width, clp.height)
|
||||
self.ctx.clip()
|
||||
|
||||
def __exit__(clp, exc_type, exc_val, traceback):
|
||||
# Reset context clip region
|
||||
self.ctx.reset_clip()
|
||||
|
||||
return Clip(primitive)
|
||||
|
||||
def scale_point(self, point):
|
||||
return tuple([coord * scale for coord, scale in zip(point, self.scale)])
|
|
@ -0,0 +1,188 @@
|
|||
|
||||
from .render import GerberContext
|
||||
from ..excellon import DrillSlot
|
||||
from ..excellon_statements import *
|
||||
|
||||
class ExcellonContext(GerberContext):
|
||||
|
||||
MODE_DRILL = 1
|
||||
MODE_SLOT =2
|
||||
|
||||
def __init__(self, settings):
|
||||
GerberContext.__init__(self)
|
||||
|
||||
# Statements that we write
|
||||
self.comments = []
|
||||
self.header = []
|
||||
self.tool_def = []
|
||||
self.body_start = [RewindStopStmt()]
|
||||
self.body = []
|
||||
self.start = [HeaderBeginStmt()]
|
||||
|
||||
# Current tool and position
|
||||
self.handled_tools = set()
|
||||
self.cur_tool = None
|
||||
self.drill_mode = ExcellonContext.MODE_DRILL
|
||||
self.drill_down = False
|
||||
self._pos = (None, None)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self._start_header()
|
||||
self._start_comments()
|
||||
|
||||
def _start_header(self):
|
||||
"""Create the header from the settings"""
|
||||
|
||||
self.header.append(UnitStmt.from_settings(self.settings))
|
||||
|
||||
if self.settings.notation == 'incremental':
|
||||
raise NotImplementedError('Incremental mode is not implemented')
|
||||
else:
|
||||
self.body.append(AbsoluteModeStmt())
|
||||
|
||||
def _start_comments(self):
|
||||
|
||||
# Write the digits used - this isn't valid Excellon statement, so we write as a comment
|
||||
self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1])))
|
||||
|
||||
def _get_end(self):
|
||||
"""How we end depends on our mode"""
|
||||
|
||||
end = []
|
||||
|
||||
if self.drill_down:
|
||||
end.append(RetractWithClampingStmt())
|
||||
end.append(RetractWithoutClampingStmt())
|
||||
|
||||
end.append(EndOfProgramStmt())
|
||||
|
||||
return end
|
||||
|
||||
@property
|
||||
def statements(self):
|
||||
return self.start + self.comments + self.header + self.body_start + self.body + self._get_end()
|
||||
|
||||
def set_bounds(self, bounds, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def paint_background(self):
|
||||
pass
|
||||
|
||||
def _render_line(self, line, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
def _render_arc(self, arc, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_region(self, region, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_level_polarity(self, region):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _simplify_point(self, point):
|
||||
return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
|
||||
|
||||
def _render_drill(self, drill, color):
|
||||
|
||||
if self.drill_mode != ExcellonContext.MODE_DRILL:
|
||||
self._start_drill_mode()
|
||||
|
||||
tool = drill.hit.tool
|
||||
if not tool in self.handled_tools:
|
||||
self.handled_tools.add(tool)
|
||||
self.header.append(ExcellonTool.from_tool(tool))
|
||||
|
||||
if tool != self.cur_tool:
|
||||
self.body.append(ToolSelectionStmt(tool.number))
|
||||
self.cur_tool = tool
|
||||
|
||||
point = self._simplify_point(drill.position)
|
||||
self._pos = drill.position
|
||||
self.body.append(CoordinateStmt.from_point(point))
|
||||
|
||||
def _start_drill_mode(self):
|
||||
"""
|
||||
If we are not in drill mode, then end the ROUT so we can do basic drilling
|
||||
"""
|
||||
|
||||
if self.drill_mode == ExcellonContext.MODE_SLOT:
|
||||
|
||||
# Make sure we are retracted before changing modes
|
||||
last_cmd = self.body[-1]
|
||||
if self.drill_down:
|
||||
self.body.append(RetractWithClampingStmt())
|
||||
self.body.append(RetractWithoutClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
# Switch to drill mode
|
||||
self.body.append(DrillModeStmt())
|
||||
self.drill_mode = ExcellonContext.MODE_DRILL
|
||||
|
||||
else:
|
||||
raise ValueError('Should be in slot mode')
|
||||
|
||||
def _render_slot(self, slot, color):
|
||||
|
||||
# Set the tool first, before we might go into drill mode
|
||||
tool = slot.hit.tool
|
||||
if not tool in self.handled_tools:
|
||||
self.handled_tools.add(tool)
|
||||
self.header.append(ExcellonTool.from_tool(tool))
|
||||
|
||||
if tool != self.cur_tool:
|
||||
self.body.append(ToolSelectionStmt(tool.number))
|
||||
self.cur_tool = tool
|
||||
|
||||
# Two types of drilling - normal drill and slots
|
||||
if slot.hit.slot_type == DrillSlot.TYPE_ROUT:
|
||||
|
||||
# For ROUT, setting the mode is part of the actual command.
|
||||
|
||||
# Are we in the right position?
|
||||
if slot.start != self._pos:
|
||||
if self.drill_down:
|
||||
# We need to move into the right position, so retract
|
||||
self.body.append(RetractWithClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
# Move to the right spot
|
||||
point = self._simplify_point(slot.start)
|
||||
self._pos = slot.start
|
||||
self.body.append(CoordinateStmt.from_point(point, mode="ROUT"))
|
||||
|
||||
# Now we are in the right spot, so drill down
|
||||
if not self.drill_down:
|
||||
self.body.append(ZAxisRoutPositionStmt())
|
||||
self.drill_down = True
|
||||
|
||||
# Do a linear move from our current position to the end position
|
||||
point = self._simplify_point(slot.end)
|
||||
self._pos = slot.end
|
||||
self.body.append(CoordinateStmt.from_point(point, mode="LINEAR"))
|
||||
|
||||
self.drill_mode = ExcellonContext.MODE_SLOT
|
||||
|
||||
else:
|
||||
# This is a G85 slot, so do this in normally drilling mode
|
||||
if self.drill_mode != ExcellonContext.MODE_DRILL:
|
||||
self._start_drill_mode()
|
||||
|
||||
# Slots don't use simplified points
|
||||
self._pos = slot.end
|
||||
self.body.append(SlotStmt.from_points(slot.start, slot.end))
|
||||
|
||||
def _render_inverted_layer(self):
|
||||
pass
|
|
@ -0,0 +1,246 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Modified from code by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Rendering
|
||||
============
|
||||
**Gerber (RS-274X) and Excellon file rendering**
|
||||
|
||||
Render Gerber and Excellon files to a variety of formats. The render module
|
||||
currently supports SVG rendering using the `svgwrite` library.
|
||||
"""
|
||||
|
||||
|
||||
from ..primitives import *
|
||||
from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt,
|
||||
CoordStmt, ApertureStmt, RegionModeStmt,
|
||||
QuadrantModeStmt,)
|
||||
|
||||
|
||||
class GerberContext(object):
|
||||
""" Gerber rendering context base class
|
||||
|
||||
Provides basic functionality and API for rendering gerber files. Medium-
|
||||
specific renderers should subclass GerberContext and implement the drawing
|
||||
functions. Colors are stored internally as 32-bit RGB and may need to be
|
||||
converted to a native format in the rendering subclass.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
units : string
|
||||
Measurement units. 'inch' or 'metric'
|
||||
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color used for rendering as a tuple of normalized (red, green, blue)
|
||||
values.
|
||||
|
||||
drill_color : tuple (<float>, <float>, <float>)
|
||||
Color used for rendering drill hits. Format is the same as for `color`.
|
||||
|
||||
background_color : tuple (<float>, <float>, <float>)
|
||||
Color of the background. Used when exposing areas in 'clear' level
|
||||
polarity mode. Format is the same as for `color`.
|
||||
|
||||
alpha : float
|
||||
Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.)
|
||||
"""
|
||||
|
||||
def __init__(self, units='inch'):
|
||||
self._units = units
|
||||
self._color = (0.7215, 0.451, 0.200)
|
||||
self._background_color = (0.0, 0.0, 0.0)
|
||||
self._drill_color = (0.0, 0.0, 0.0)
|
||||
self._alpha = 1.0
|
||||
self._invert = False
|
||||
self.ctx = None
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return self._units
|
||||
|
||||
@units.setter
|
||||
def units(self, units):
|
||||
if units not in ('inch', 'metric'):
|
||||
raise ValueError('Units may be "inch" or "metric"')
|
||||
self._units = units
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self._color
|
||||
|
||||
@color.setter
|
||||
def color(self, color):
|
||||
if len(color) != 3:
|
||||
raise TypeError('Color must be a tuple of R, G, and B values')
|
||||
for c in color:
|
||||
if c < 0 or c > 1:
|
||||
raise ValueError('Channel values must be between 0.0 and 1.0')
|
||||
self._color = color
|
||||
|
||||
@property
|
||||
def drill_color(self):
|
||||
return self._drill_color
|
||||
|
||||
@drill_color.setter
|
||||
def drill_color(self, color):
|
||||
if len(color) != 3:
|
||||
raise TypeError('Drill color must be a tuple of R, G, and B values')
|
||||
for c in color:
|
||||
if c < 0 or c > 1:
|
||||
raise ValueError('Channel values must be between 0.0 and 1.0')
|
||||
self._drill_color = color
|
||||
|
||||
@property
|
||||
def background_color(self):
|
||||
return self._background_color
|
||||
|
||||
@background_color.setter
|
||||
def background_color(self, color):
|
||||
if len(color) != 3:
|
||||
raise TypeError('Background color must be a tuple of R, G, and B values')
|
||||
for c in color:
|
||||
if c < 0 or c > 1:
|
||||
raise ValueError('Channel values must be between 0.0 and 1.0')
|
||||
self._background_color = color
|
||||
|
||||
@property
|
||||
def alpha(self):
|
||||
return self._alpha
|
||||
|
||||
@alpha.setter
|
||||
def alpha(self, alpha):
|
||||
if alpha < 0 or alpha > 1:
|
||||
raise ValueError('Alpha must be between 0.0 and 1.0')
|
||||
self._alpha = alpha
|
||||
|
||||
@property
|
||||
def invert(self):
|
||||
return self._invert
|
||||
|
||||
@invert.setter
|
||||
def invert(self, invert):
|
||||
self._invert = invert
|
||||
|
||||
def render(self, primitive):
|
||||
if not primitive:
|
||||
return
|
||||
|
||||
self.pre_render_primitive(primitive)
|
||||
|
||||
color = self.color
|
||||
if isinstance(primitive, Line):
|
||||
self._render_line(primitive, color)
|
||||
elif isinstance(primitive, Arc):
|
||||
self._render_arc(primitive, color)
|
||||
elif isinstance(primitive, Region):
|
||||
self._render_region(primitive, color)
|
||||
elif isinstance(primitive, Circle):
|
||||
self._render_circle(primitive, color)
|
||||
elif isinstance(primitive, Rectangle):
|
||||
self._render_rectangle(primitive, color)
|
||||
elif isinstance(primitive, Obround):
|
||||
self._render_obround(primitive, color)
|
||||
elif isinstance(primitive, Polygon):
|
||||
self._render_polygon(primitive, color)
|
||||
elif isinstance(primitive, Drill):
|
||||
self._render_drill(primitive, self.color)
|
||||
elif isinstance(primitive, Slot):
|
||||
self._render_slot(primitive, self.color)
|
||||
elif isinstance(primitive, AMGroup):
|
||||
self._render_amgroup(primitive, color)
|
||||
elif isinstance(primitive, Outline):
|
||||
self._render_region(primitive, color)
|
||||
elif isinstance(primitive, TestRecord):
|
||||
self._render_test_record(primitive, color)
|
||||
|
||||
self.post_render_primitive(primitive)
|
||||
|
||||
def set_bounds(self, bounds, *args, **kwargs):
|
||||
"""Called by the renderer to set the extents of the file to render.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bounds: Tuple[Tuple[float, float], Tuple[float, float]]
|
||||
( (x_min, x_max), (y_min, y_max)
|
||||
"""
|
||||
pass
|
||||
|
||||
def paint_background(self):
|
||||
pass
|
||||
|
||||
def new_render_layer(self):
|
||||
pass
|
||||
|
||||
def flatten(self):
|
||||
pass
|
||||
|
||||
def pre_render_primitive(self, primitive):
|
||||
"""
|
||||
Called before rendering a primitive. Use the callback to perform some action before rendering
|
||||
a primitive, for example adding a comment.
|
||||
"""
|
||||
return
|
||||
|
||||
def post_render_primitive(self, primitive):
|
||||
"""
|
||||
Called after rendering a primitive. Use the callback to perform some action after rendering
|
||||
a primitive
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
def _render_line(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_arc(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_region(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_circle(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_rectangle(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_obround(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_polygon(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_drill(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_slot(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_amgroup(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_test_record(self, primitive, color):
|
||||
pass
|
||||
|
||||
|
||||
class RenderSettings(object):
|
||||
def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False,
|
||||
mirror=False):
|
||||
self.color = color
|
||||
self.alpha = alpha
|
||||
self.invert = invert
|
||||
self.mirror = mirror
|
|
@ -0,0 +1,510 @@
|
|||
"""Renders an in-memory Gerber file to statements which can be written to a string
|
||||
"""
|
||||
from copy import deepcopy
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .render import GerberContext
|
||||
from ..am_statements import *
|
||||
from ..gerber_statements import *
|
||||
from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle
|
||||
|
||||
|
||||
class AMGroupContext(object):
|
||||
'''A special renderer to generate aperature macros from an AMGroup'''
|
||||
|
||||
def __init__(self):
|
||||
self.statements = []
|
||||
|
||||
def render(self, amgroup, name):
|
||||
|
||||
if amgroup.stmt:
|
||||
# We know the statement it was generated from, so use that to create the AMParamStmt
|
||||
# It will give a much better result
|
||||
|
||||
stmt = deepcopy(amgroup.stmt)
|
||||
stmt.name = name
|
||||
|
||||
return stmt
|
||||
|
||||
else:
|
||||
# Clone ourselves, then offset by the psotion so that
|
||||
# our render doesn't have to consider offset. Just makes things simpler
|
||||
nooffset_group = deepcopy(amgroup)
|
||||
nooffset_group.position = (0, 0)
|
||||
|
||||
# Now draw the shapes
|
||||
for primitive in nooffset_group.primitives:
|
||||
if isinstance(primitive, Outline):
|
||||
self._render_outline(primitive)
|
||||
elif isinstance(primitive, Circle):
|
||||
self._render_circle(primitive)
|
||||
elif isinstance(primitive, Rectangle):
|
||||
self._render_rectangle(primitive)
|
||||
elif isinstance(primitive, Line):
|
||||
self._render_line(primitive)
|
||||
elif isinstance(primitive, Polygon):
|
||||
self._render_polygon(primitive)
|
||||
else:
|
||||
raise ValueError('amgroup')
|
||||
|
||||
statement = AMParamStmt('AM', name, self._statements_to_string())
|
||||
return statement
|
||||
|
||||
def _statements_to_string(self):
|
||||
macro = ''
|
||||
|
||||
for statement in self.statements:
|
||||
macro += statement.to_gerber()
|
||||
|
||||
return macro
|
||||
|
||||
def _render_circle(self, circle):
|
||||
self.statements.append(AMCirclePrimitive.from_primitive(circle))
|
||||
|
||||
def _render_rectangle(self, rectangle):
|
||||
self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle))
|
||||
|
||||
def _render_line(self, line):
|
||||
self.statements.append(AMVectorLinePrimitive.from_primitive(line))
|
||||
|
||||
def _render_outline(self, outline):
|
||||
self.statements.append(AMOutlinePrimitive.from_primitive(outline))
|
||||
|
||||
def _render_polygon(self, polygon):
|
||||
self.statements.append(AMPolygonPrimitive.from_primitive(polygon))
|
||||
|
||||
def _render_thermal(self, thermal):
|
||||
pass
|
||||
|
||||
|
||||
class Rs274xContext(GerberContext):
|
||||
|
||||
def __init__(self, settings):
|
||||
GerberContext.__init__(self)
|
||||
self.comments = []
|
||||
self.header = []
|
||||
self.body = []
|
||||
self.end = [EofStmt()]
|
||||
|
||||
# Current values so we know if we have to execute
|
||||
# moves, levey changes before anything else
|
||||
self._level_polarity = None
|
||||
self._pos = (None, None)
|
||||
self._func = None
|
||||
self._quadrant_mode = None
|
||||
self._dcode = None
|
||||
|
||||
# Primarily for testing and comarison to files, should we write
|
||||
# flashes as a single statement or a move plus flash? Set to true
|
||||
# to do in a single statement. Normally this can be false
|
||||
self.condensed_flash = True
|
||||
|
||||
# When closing a region, force a D02 staement to close a region.
|
||||
# This is normally not necessary because regions are closed with a G37
|
||||
# staement, but this will add an extra statement for doubly close
|
||||
# the region
|
||||
self.explicit_region_move_end = False
|
||||
|
||||
self._next_dcode = 10
|
||||
self._rects = {}
|
||||
self._circles = {}
|
||||
self._obrounds = {}
|
||||
self._polygons = {}
|
||||
self._macros = {}
|
||||
|
||||
self._i_none = 0
|
||||
self._j_none = 0
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self._start_header(settings)
|
||||
|
||||
def _start_header(self, settings):
|
||||
self.header.append(FSParamStmt.from_settings(settings))
|
||||
self.header.append(MOParamStmt.from_units(settings.units))
|
||||
|
||||
def _simplify_point(self, point):
|
||||
return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
|
||||
|
||||
def _simplify_offset(self, point, offset):
|
||||
|
||||
if point[0] != offset[0]:
|
||||
xoffset = point[0] - offset[0]
|
||||
else:
|
||||
xoffset = self._i_none
|
||||
|
||||
if point[1] != offset[1]:
|
||||
yoffset = point[1] - offset[1]
|
||||
else:
|
||||
yoffset = self._j_none
|
||||
|
||||
return (xoffset, yoffset)
|
||||
|
||||
@property
|
||||
def statements(self):
|
||||
return self.comments + self.header + self.body + self.end
|
||||
|
||||
def set_bounds(self, bounds, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def paint_background(self):
|
||||
pass
|
||||
|
||||
def _select_aperture(self, aperture):
|
||||
|
||||
# Select the right aperture if not already selected
|
||||
if aperture:
|
||||
if isinstance(aperture, Circle):
|
||||
aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height)
|
||||
elif isinstance(aperture, Rectangle):
|
||||
aper = self._get_rectangle(aperture.width, aperture.height)
|
||||
elif isinstance(aperture, Obround):
|
||||
aper = self._get_obround(aperture.width, aperture.height)
|
||||
elif isinstance(aperture, AMGroup):
|
||||
aper = self._get_amacro(aperture)
|
||||
else:
|
||||
raise NotImplementedError('Line with invalid aperture type')
|
||||
|
||||
if aper.d != self._dcode:
|
||||
self.body.append(ApertureStmt(aper.d))
|
||||
self._dcode = aper.d
|
||||
|
||||
def pre_render_primitive(self, primitive):
|
||||
|
||||
if hasattr(primitive, 'comment'):
|
||||
self.body.append(CommentStmt(primitive.comment))
|
||||
|
||||
def _render_line(self, line, color, default_polarity='dark'):
|
||||
|
||||
self._select_aperture(line.aperture)
|
||||
|
||||
self._render_level_polarity(line, default_polarity)
|
||||
|
||||
# Get the right function
|
||||
if self._func != CoordStmt.FUNC_LINEAR:
|
||||
func = CoordStmt.FUNC_LINEAR
|
||||
else:
|
||||
func = None
|
||||
self._func = CoordStmt.FUNC_LINEAR
|
||||
|
||||
if self._pos != line.start:
|
||||
self.body.append(CoordStmt.move(func, self._simplify_point(line.start)))
|
||||
self._pos = line.start
|
||||
# We already set the function, so the next command doesn't require that
|
||||
func = None
|
||||
|
||||
point = self._simplify_point(line.end)
|
||||
|
||||
# In some files, we see a lot of duplicated ponts, so omit those
|
||||
if point[0] != None or point[1] != None:
|
||||
self.body.append(CoordStmt.line(func, self._simplify_point(line.end)))
|
||||
self._pos = line.end
|
||||
elif func:
|
||||
self.body.append(CoordStmt.mode(func))
|
||||
|
||||
def _render_arc(self, arc, color, default_polarity='dark'):
|
||||
|
||||
# Optionally set the quadrant mode if it has changed:
|
||||
if arc.quadrant_mode != self._quadrant_mode:
|
||||
|
||||
if arc.quadrant_mode != 'multi-quadrant':
|
||||
self.body.append(QuadrantModeStmt.single())
|
||||
else:
|
||||
self.body.append(QuadrantModeStmt.multi())
|
||||
|
||||
self._quadrant_mode = arc.quadrant_mode
|
||||
|
||||
# Select the right aperture if not already selected
|
||||
self._select_aperture(arc.aperture)
|
||||
|
||||
self._render_level_polarity(arc, default_polarity)
|
||||
|
||||
# Find the right movement mode. Always set to be sure it is really right
|
||||
dir = arc.direction
|
||||
if dir == 'clockwise':
|
||||
func = CoordStmt.FUNC_ARC_CW
|
||||
self._func = CoordStmt.FUNC_ARC_CW
|
||||
elif dir == 'counterclockwise':
|
||||
func = CoordStmt.FUNC_ARC_CCW
|
||||
self._func = CoordStmt.FUNC_ARC_CCW
|
||||
else:
|
||||
raise ValueError('Invalid circular interpolation mode')
|
||||
|
||||
if self._pos != arc.start:
|
||||
# TODO I'm not sure if this is right
|
||||
self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start)))
|
||||
self._pos = arc.start
|
||||
|
||||
center = self._simplify_offset(arc.center, arc.start)
|
||||
end = self._simplify_point(arc.end)
|
||||
self.body.append(CoordStmt.arc(func, end, center))
|
||||
self._pos = arc.end
|
||||
|
||||
def _render_region(self, region, color):
|
||||
|
||||
self._render_level_polarity(region)
|
||||
|
||||
self.body.append(RegionModeStmt.on())
|
||||
|
||||
for p in region.primitives:
|
||||
|
||||
# Make programmatically generated primitives within a region with
|
||||
# unset level polarity inherit the region's level polarity
|
||||
if isinstance(p, Line):
|
||||
self._render_line(p, color, default_polarity=region.level_polarity)
|
||||
else:
|
||||
self._render_arc(p, color, default_polarity=region.level_polarity)
|
||||
|
||||
if self.explicit_region_move_end:
|
||||
self.body.append(CoordStmt.move(None, None))
|
||||
|
||||
self.body.append(RegionModeStmt.off())
|
||||
|
||||
def _render_level_polarity(self, obj, default='dark'):
|
||||
obj_polarity = obj.level_polarity if obj.level_polarity is not None else default
|
||||
if obj_polarity != self._level_polarity:
|
||||
self._level_polarity = obj_polarity
|
||||
self.body.append(LPParamStmt('LP', obj_polarity))
|
||||
|
||||
def _render_flash(self, primitive, aperture):
|
||||
|
||||
self._render_level_polarity(primitive)
|
||||
|
||||
if aperture.d != self._dcode:
|
||||
self.body.append(ApertureStmt(aperture.d))
|
||||
self._dcode = aperture.d
|
||||
|
||||
if self.condensed_flash:
|
||||
self.body.append(CoordStmt.flash(self._simplify_point(primitive.position)))
|
||||
else:
|
||||
self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position)))
|
||||
self.body.append(CoordStmt.flash(None))
|
||||
|
||||
self._pos = primitive.position
|
||||
|
||||
def _get_circle(self, diameter, hole_diameter=None, hole_width=None,
|
||||
hole_height=None, dcode = None):
|
||||
'''Define a circlar aperture'''
|
||||
|
||||
key = (diameter, hole_diameter, hole_width, hole_height)
|
||||
aper = self._circles.get(key, None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height)
|
||||
self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
|
||||
aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height)
|
||||
self._render_flash(circle, aper)
|
||||
|
||||
def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None,
|
||||
hole_height=None, dcode = None):
|
||||
'''Get a rectanglar aperture. If it isn't defined, create it'''
|
||||
|
||||
key = (width, height, hole_diameter, hole_width, hole_height)
|
||||
aper = self._rects.get(key, None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height)
|
||||
self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
|
||||
aper = self._get_rectangle(rectangle.width, rectangle.height,
|
||||
rectangle.hole_diameter,
|
||||
rectangle.hole_width, rectangle.hole_height)
|
||||
self._render_flash(rectangle, aper)
|
||||
|
||||
def _get_obround(self, width, height, hole_diameter=None, hole_width=None,
|
||||
hole_height=None, dcode = None):
|
||||
|
||||
key = (width, height, hole_diameter, hole_width, hole_height)
|
||||
aper = self._obrounds.get(key, None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height)
|
||||
self._obrounds[key] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
|
||||
aper = self._get_obround(obround.width, obround.height,
|
||||
obround.hole_diameter, obround.hole_width,
|
||||
obround.hole_height)
|
||||
self._render_flash(obround, aper)
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
|
||||
aper = self._get_polygon(polygon.radius, polygon.sides,
|
||||
polygon.rotation, polygon.hole_diameter,
|
||||
polygon.hole_width, polygon.hole_height)
|
||||
self._render_flash(polygon, aper)
|
||||
|
||||
def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None,
|
||||
hole_width=None, hole_height=None, dcode = None):
|
||||
|
||||
key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height)
|
||||
aper = self._polygons.get(key, None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices,
|
||||
rotation, hole_diameter, hole_width,
|
||||
hole_height)
|
||||
self._polygons[key] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_drill(self, drill, color):
|
||||
raise ValueError('Drills are not valid in RS274X files')
|
||||
|
||||
def _hash_amacro(self, amgroup):
|
||||
'''Calculate a very quick hash code for deciding if we should even check AM groups for comparision'''
|
||||
|
||||
# We always start with an X because this forms part of the name
|
||||
# Basically, in some cases, the name might start with a C, R, etc. That can appear
|
||||
# to conflict with normal aperture definitions. Technically, it shouldn't because normal
|
||||
# aperture definitions should have a comma, but in some cases the commit is omitted
|
||||
hash = 'X'
|
||||
for primitive in amgroup.primitives:
|
||||
|
||||
hash += primitive.__class__.__name__[0]
|
||||
|
||||
bbox = primitive.bounding_box
|
||||
hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2]
|
||||
hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2]
|
||||
|
||||
if hasattr(primitive, 'primitives'):
|
||||
hash += str(len(primitive.primitives))
|
||||
|
||||
if isinstance(primitive, Rectangle):
|
||||
hash += str(primitive.width * 1000000)[0:2]
|
||||
hash += str(primitive.height * 1000000)[0:2]
|
||||
elif isinstance(primitive, Circle):
|
||||
hash += str(primitive.diameter * 1000000)[0:2]
|
||||
|
||||
if len(hash) > 20:
|
||||
# The hash might actually get quite complex, so stop before
|
||||
# it gets too long
|
||||
break
|
||||
|
||||
return hash
|
||||
|
||||
def _get_amacro(self, amgroup, dcode = None):
|
||||
# Macros are a little special since we don't have a good way to compare them quickly
|
||||
# but in most cases, this should work
|
||||
|
||||
hash = self._hash_amacro(amgroup)
|
||||
macro = None
|
||||
macroinfo = self._macros.get(hash, None)
|
||||
|
||||
if macroinfo:
|
||||
|
||||
# We have a definition, but check that the groups actually are the same
|
||||
for macro in macroinfo:
|
||||
|
||||
# Macros should have positions, right? But if the macro is selected for non-flashes
|
||||
# then it won't have a position. This is of course a bad gerber, but they do exist
|
||||
if amgroup.position:
|
||||
position = amgroup.position
|
||||
else:
|
||||
position = (0, 0)
|
||||
|
||||
offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1])
|
||||
if amgroup.equivalent(macro[1], offset):
|
||||
break
|
||||
macro = None
|
||||
|
||||
# Did we find one in the group0
|
||||
if not macro:
|
||||
# This is a new macro, so define it
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
# Create the statements
|
||||
# TODO
|
||||
amrenderer = AMGroupContext()
|
||||
statement = amrenderer.render(amgroup, hash)
|
||||
|
||||
self.header.append(statement)
|
||||
|
||||
aperdef = ADParamStmt.macro(dcode, hash)
|
||||
self.header.append(aperdef)
|
||||
|
||||
# Store the dcode and the original so we can check if it really is the same
|
||||
# If it didn't have a postition, set it to 0, 0
|
||||
if amgroup.position == None:
|
||||
amgroup.position = (0, 0)
|
||||
macro = (aperdef, amgroup)
|
||||
|
||||
if macroinfo:
|
||||
macroinfo.append(macro)
|
||||
else:
|
||||
self._macros[hash] = [macro]
|
||||
|
||||
return macro[0]
|
||||
|
||||
def _render_amgroup(self, amgroup, color):
|
||||
|
||||
aper = self._get_amacro(amgroup)
|
||||
self._render_flash(amgroup, aper)
|
||||
|
||||
def _render_inverted_layer(self):
|
||||
pass
|
||||
|
||||
def new_render_layer(self):
|
||||
# TODO Might need to implement this
|
||||
pass
|
||||
|
||||
def flatten(self):
|
||||
# TODO Might need to implement this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
"""Write the rendered file to a StringIO steam"""
|
||||
statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements)
|
||||
stream = StringIO()
|
||||
for statement in statements:
|
||||
stream.write(statement + '\n')
|
||||
|
||||
return stream
|
|
@ -0,0 +1,112 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from .render import RenderSettings
|
||||
|
||||
COLORS = {
|
||||
'black': (0.0, 0.0, 0.0),
|
||||
'white': (1.0, 1.0, 1.0),
|
||||
'red': (1.0, 0.0, 0.0),
|
||||
'green': (0.0, 1.0, 0.0),
|
||||
'yellow': (1.0, 1.0, 0),
|
||||
'blue': (0.0, 0.0, 1.0),
|
||||
'fr-4': (0.290, 0.345, 0.0),
|
||||
'green soldermask': (0.0, 0.412, 0.278),
|
||||
'blue soldermask': (0.059, 0.478, 0.651),
|
||||
'red soldermask': (0.968, 0.169, 0.165),
|
||||
'black soldermask': (0.298, 0.275, 0.282),
|
||||
'purple soldermask': (0.2, 0.0, 0.334),
|
||||
'enig copper': (0.694, 0.533, 0.514),
|
||||
'hasl copper': (0.871, 0.851, 0.839)
|
||||
}
|
||||
|
||||
|
||||
SPECTRUM = [
|
||||
(0.804, 0.216, 0),
|
||||
(0.78, 0.776, 0.251),
|
||||
(0.545, 0.451, 0.333),
|
||||
(0.545, 0.137, 0.137),
|
||||
(0.329, 0.545, 0.329),
|
||||
(0.133, 0.545, 0.133),
|
||||
(0, 0.525, 0.545),
|
||||
(0.227, 0.373, 0.804),
|
||||
]
|
||||
|
||||
|
||||
class Theme(object):
|
||||
|
||||
def __init__(self, name=None, **kwargs):
|
||||
self.name = 'Default' if name is None else name
|
||||
self.background = kwargs.get('background', RenderSettings(COLORS['fr-4']))
|
||||
self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white']))
|
||||
self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True))
|
||||
self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True))
|
||||
self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True))
|
||||
self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
|
||||
self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True))
|
||||
self.drill = kwargs.get('drill', RenderSettings(COLORS['black']))
|
||||
self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red']))
|
||||
self._internal = kwargs.get('internal', [RenderSettings(x) for x in SPECTRUM])
|
||||
self._internal_gen = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
@property
|
||||
def internal(self):
|
||||
if not self._internal_gen:
|
||||
self._internal_gen = self._internal_gen_func()
|
||||
return next(self._internal_gen)
|
||||
|
||||
def _internal_gen_func(self):
|
||||
for setting in self._internal:
|
||||
yield setting
|
||||
|
||||
def get(self, key, noneval=None):
|
||||
val = getattr(self, key, None)
|
||||
return val if val is not None else noneval
|
||||
|
||||
|
||||
THEMES = {
|
||||
'default': Theme(),
|
||||
'OSH Park': Theme(name='OSH Park',
|
||||
background=RenderSettings(COLORS['purple soldermask']),
|
||||
top=RenderSettings(COLORS['enig copper']),
|
||||
bottom=RenderSettings(COLORS['enig copper'], mirror=True),
|
||||
topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True),
|
||||
bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True),
|
||||
topsilk=RenderSettings(COLORS['white'], alpha=0.8),
|
||||
bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=True)),
|
||||
|
||||
'Blue': Theme(name='Blue',
|
||||
topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True),
|
||||
bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)),
|
||||
|
||||
'Transparent Copper': Theme(name='Transparent',
|
||||
background=RenderSettings((0.9, 0.9, 0.9)),
|
||||
top=RenderSettings(COLORS['red'], alpha=0.5),
|
||||
bottom=RenderSettings(COLORS['blue'], alpha=0.5),
|
||||
drill=RenderSettings((0.3, 0.3, 0.3))),
|
||||
|
||||
'Transparent Multilayer': Theme(name='Transparent Multilayer',
|
||||
background=RenderSettings((0, 0, 0)),
|
||||
top=RenderSettings(SPECTRUM[0], alpha=0.8),
|
||||
bottom=RenderSettings(SPECTRUM[-1], alpha=0.8),
|
||||
drill=RenderSettings((0.3, 0.3, 0.3)),
|
||||
internal=[RenderSettings(x, alpha=0.5) for x in SPECTRUM[1:-1]]),
|
||||
}
|
|
@ -0,0 +1,800 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
""" This module provides an RS-274-X class and parser.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .gerber_statements import *
|
||||
from .primitives import *
|
||||
from .cam import CamFile, FileSettings
|
||||
from .utils import sq_distance
|
||||
|
||||
|
||||
def read(filename):
|
||||
""" Read data from filename and return a GerberFile
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of file to parse
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.rs274x.GerberFile`
|
||||
A GerberFile created from the specified file.
|
||||
"""
|
||||
return GerberParser().parse(filename)
|
||||
|
||||
|
||||
def loads(data, filename=None):
|
||||
""" Generate a GerberFile object from rs274x data in memory
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing gerber file contents
|
||||
|
||||
filename : string, optional
|
||||
string containing the filename of the data source
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.rs274x.GerberFile`
|
||||
A GerberFile created from the specified file.
|
||||
"""
|
||||
return GerberParser().parse_raw(data, filename)
|
||||
|
||||
|
||||
class GerberFile(CamFile):
|
||||
""" A class representing a single gerber file
|
||||
|
||||
The GerberFile class represents a single gerber file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
statements : list
|
||||
list of gerber file statements
|
||||
|
||||
settings : dict
|
||||
Dictionary of gerber file settings
|
||||
|
||||
filename : string
|
||||
Filename of the source gerber file
|
||||
|
||||
Attributes
|
||||
----------
|
||||
comments: list of strings
|
||||
List of comments contained in the gerber file.
|
||||
|
||||
size : tuple, (<float>, <float>)
|
||||
Size in [self.units] of the layer described by the gerber file.
|
||||
|
||||
bounds: tuple, ((<float>, <float>), (<float>, <float>))
|
||||
boundaries of the layer described by the gerber file.
|
||||
`bounds` is stored as ((min x, max x), (min y, max y))
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, statements, settings, primitives, apertures, filename=None):
|
||||
super(GerberFile, self).__init__(statements, settings, primitives, filename)
|
||||
|
||||
self.apertures = apertures
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return [comment.comment for comment in self.statements
|
||||
if isinstance(comment, CommentStmt)]
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
xbounds, ybounds = self.bounds
|
||||
return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0])
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
min_x = min_y = 1000000
|
||||
max_x = max_y = -1000000
|
||||
|
||||
for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]:
|
||||
if stmt.x is not None:
|
||||
min_x = min(stmt.x, min_x)
|
||||
max_x = max(stmt.x, max_x)
|
||||
|
||||
if stmt.y is not None:
|
||||
min_y = min(stmt.y, min_y)
|
||||
max_y = max(stmt.y, max_y)
|
||||
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = min_y = 1000000
|
||||
max_x = max_y = -1000000
|
||||
|
||||
for prim in self.primitives:
|
||||
bounds = prim.bounding_box
|
||||
min_x = min(bounds[0][0], min_x)
|
||||
max_x = max(bounds[0][1], max_x)
|
||||
|
||||
min_y = min(bounds[1][0], min_y)
|
||||
max_y = max(bounds[1][1], max_y)
|
||||
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def write(self, filename, settings=None):
|
||||
""" Write data out to a gerber file.
|
||||
"""
|
||||
with open(filename, 'w') as f:
|
||||
for statement in self.statements:
|
||||
f.write(statement.to_gerber(settings or self.settings))
|
||||
f.write("\n")
|
||||
|
||||
def to_inch(self):
|
||||
if self.units != 'inch':
|
||||
self.units = 'inch'
|
||||
for statement in self.statements:
|
||||
statement.to_inch()
|
||||
for primitive in self.primitives:
|
||||
primitive.to_inch()
|
||||
|
||||
def to_metric(self):
|
||||
if self.units != 'metric':
|
||||
self.units = 'metric'
|
||||
for statement in self.statements:
|
||||
statement.to_metric()
|
||||
for primitive in self.primitives:
|
||||
primitive.to_metric()
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
for statement in self.statements:
|
||||
statement.offset(x_offset, y_offset)
|
||||
for primitive in self.primitives:
|
||||
primitive.offset(x_offset, y_offset)
|
||||
|
||||
|
||||
class GerberParser(object):
|
||||
""" GerberParser
|
||||
"""
|
||||
NUMBER = r"[\+-]?\d+"
|
||||
DECIMAL = r"[\+-]?\d+([.]?\d+)?"
|
||||
STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+"
|
||||
NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+"
|
||||
|
||||
FS = r"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*"
|
||||
MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
|
||||
LP = r"(?P<param>LP)(?P<lp>(D|C))"
|
||||
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)"
|
||||
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,%]*)"
|
||||
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,%]*)"
|
||||
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,%]*)"
|
||||
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)".format(name=NAME)
|
||||
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>[^%]*)".format(name=NAME)
|
||||
# Include File
|
||||
IF = r"(?P<param>IF)(?P<filename>.*)"
|
||||
|
||||
|
||||
# begin deprecated
|
||||
AS = r"(?P<param>AS)(?P<mode>(AXBY)|(AYBX))"
|
||||
IN = r"(?P<param>IN)(?P<name>.*)"
|
||||
IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
|
||||
IR = r"(?P<param>IR)(?P<angle>{number})".format(number=NUMBER)
|
||||
MI = r"(?P<param>MI)(A(?P<a>0|1))?(B(?P<b>0|1))?"
|
||||
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
|
||||
SF = r"(?P<param>SF)(?P<discarded>.*)"
|
||||
LN = r"(?P<param>LN)(?P<name>.*)"
|
||||
DEPRECATED_UNIT = re.compile(r'(?P<mode>G7[01])\*')
|
||||
DEPRECATED_FORMAT = re.compile(r'(?P<format>G9[01])\*')
|
||||
# end deprecated
|
||||
|
||||
PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY,
|
||||
AD_MACRO, AM, AS, IF, IN, IP, IR, MI, OF, SF, LN)
|
||||
|
||||
PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS]
|
||||
|
||||
COORD_FUNCTION = r"G0?[123]"
|
||||
COORD_OP = r"D0?[123]"
|
||||
|
||||
COORD_STMT = re.compile((
|
||||
r"(?P<function>{function})?"
|
||||
r"(X(?P<x>{number}))?(Y(?P<y>{number}))?"
|
||||
r"(I(?P<i>{number}))?(J(?P<j>{number}))?"
|
||||
r"(?P<op>{op})?\*".format(number=NUMBER, function=COORD_FUNCTION, op=COORD_OP)))
|
||||
|
||||
APERTURE_STMT = re.compile(r"(?P<deprecated>(G54)|(G55))?D(?P<d>\d+)\*")
|
||||
|
||||
COMMENT_STMT = re.compile(r"G0?4(?P<comment>[^*]*)(\*)?")
|
||||
|
||||
EOF_STMT = re.compile(r"(?P<eof>M[0]?[012])\*")
|
||||
|
||||
REGION_MODE_STMT = re.compile(r'(?P<mode>G3[67])\*')
|
||||
QUAD_MODE_STMT = re.compile(r'(?P<mode>G7[45])\*')
|
||||
|
||||
# Keep include loop from crashing us
|
||||
INCLUDE_FILE_RECURSION_LIMIT = 10
|
||||
|
||||
def __init__(self):
|
||||
self.filename = None
|
||||
self.settings = FileSettings()
|
||||
self.statements = []
|
||||
self.primitives = []
|
||||
self.apertures = {}
|
||||
self.macros = {}
|
||||
self.current_region = None
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.op = "D02"
|
||||
self.aperture = 0
|
||||
self.interpolation = 'linear'
|
||||
self.direction = 'clockwise'
|
||||
self.image_polarity = 'positive'
|
||||
self.level_polarity = 'dark'
|
||||
self.region_mode = 'off'
|
||||
self.quadrant_mode = 'multi-quadrant'
|
||||
self.step_and_repeat = (1, 1, 0, 0)
|
||||
self._recursion_depth = 0
|
||||
|
||||
def parse(self, filename):
|
||||
self.filename = filename
|
||||
with open(filename, "rU") as fp:
|
||||
data = fp.read()
|
||||
return self.parse_raw(data, filename)
|
||||
|
||||
def parse_raw(self, data, filename=None):
|
||||
self.filename = filename
|
||||
for stmt in self._parse(self._split_commands(data)):
|
||||
self.evaluate(stmt)
|
||||
self.statements.append(stmt)
|
||||
|
||||
# Initialize statement units
|
||||
for stmt in self.statements:
|
||||
stmt.units = self.settings.units
|
||||
|
||||
return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename)
|
||||
|
||||
def _split_commands(self, data):
|
||||
"""
|
||||
Split the data into commands. Commands end with * (and also newline to help with some badly formatted files)
|
||||
"""
|
||||
|
||||
length = len(data)
|
||||
start = 0
|
||||
in_header = True
|
||||
|
||||
for cur in range(0, length):
|
||||
|
||||
val = data[cur]
|
||||
|
||||
if val == '%' and start == cur:
|
||||
in_header = True
|
||||
continue
|
||||
|
||||
if val == '\r' or val == '\n':
|
||||
if start != cur:
|
||||
yield data[start:cur]
|
||||
start = cur + 1
|
||||
|
||||
elif not in_header and val == '*':
|
||||
yield data[start:cur + 1]
|
||||
start = cur + 1
|
||||
|
||||
elif in_header and val == '%':
|
||||
yield data[start:cur + 1]
|
||||
start = cur + 1
|
||||
in_header = False
|
||||
|
||||
def dump_json(self):
|
||||
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
|
||||
return json.dumps(stmts)
|
||||
|
||||
def dump_str(self):
|
||||
string = ""
|
||||
for stmt in self.statements:
|
||||
string += str(stmt) + "\n"
|
||||
return string
|
||||
|
||||
def _parse(self, data):
|
||||
oldline = ''
|
||||
|
||||
for line in data:
|
||||
line = oldline + line.strip()
|
||||
|
||||
# skip empty lines
|
||||
if not len(line):
|
||||
continue
|
||||
|
||||
# deal with multi-line parameters
|
||||
if line.startswith("%") and not line.endswith("%") and not "%" in line[1:]:
|
||||
oldline = line
|
||||
continue
|
||||
|
||||
did_something = True # make sure we do at least one loop
|
||||
while did_something and len(line) > 0:
|
||||
did_something = False
|
||||
|
||||
# consume empty data blocks
|
||||
if line[0] == '*':
|
||||
line = line[1:]
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# coord
|
||||
(coord, r) = _match_one(self.COORD_STMT, line)
|
||||
if coord:
|
||||
yield CoordStmt.from_dict(coord, self.settings)
|
||||
line = r
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# aperture selection
|
||||
(aperture, r) = _match_one(self.APERTURE_STMT, line)
|
||||
if aperture:
|
||||
yield ApertureStmt(**aperture)
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# parameter
|
||||
(param, r) = _match_one_from_many(self.PARAM_STMT, line)
|
||||
|
||||
if param:
|
||||
if param["param"] == "FS":
|
||||
stmt = FSParamStmt.from_dict(param)
|
||||
self.settings.zero_suppression = stmt.zero_suppression
|
||||
self.settings.format = stmt.format
|
||||
self.settings.notation = stmt.notation
|
||||
yield stmt
|
||||
elif param["param"] == "MO":
|
||||
stmt = MOParamStmt.from_dict(param)
|
||||
self.settings.units = stmt.mode
|
||||
yield stmt
|
||||
elif param["param"] == "LP":
|
||||
yield LPParamStmt.from_dict(param)
|
||||
elif param["param"] == "AD":
|
||||
yield ADParamStmt.from_dict(param)
|
||||
elif param["param"] == "AM":
|
||||
stmt = AMParamStmt.from_dict(param)
|
||||
stmt.units = self.settings.units
|
||||
yield stmt
|
||||
elif param["param"] == "OF":
|
||||
yield OFParamStmt.from_dict(param)
|
||||
elif param["param"] == "IF":
|
||||
# Don't crash on include loop
|
||||
if self._recursion_depth < self.INCLUDE_FILE_RECURSION_LIMIT:
|
||||
self._recursion_depth += 1
|
||||
with open(os.path.join(os.path.dirname(self.filename), param["filename"]), 'r') as f:
|
||||
inc_data = f.read()
|
||||
for stmt in self._parse(self._split_commands(inc_data)):
|
||||
yield stmt
|
||||
self._recursion_depth -= 1
|
||||
else:
|
||||
raise IOError("Include file nesting depth limit exceeded.")
|
||||
elif param["param"] == "IN":
|
||||
yield INParamStmt.from_dict(param)
|
||||
elif param["param"] == "LN":
|
||||
yield LNParamStmt.from_dict(param)
|
||||
# deprecated commands AS, IN, IP, IR, MI, OF, SF, LN
|
||||
elif param["param"] == "AS":
|
||||
yield ASParamStmt.from_dict(param)
|
||||
elif param["param"] == "IN":
|
||||
yield INParamStmt.from_dict(param)
|
||||
elif param["param"] == "IP":
|
||||
yield IPParamStmt.from_dict(param)
|
||||
elif param["param"] == "IR":
|
||||
yield IRParamStmt.from_dict(param)
|
||||
elif param["param"] == "MI":
|
||||
yield MIParamStmt.from_dict(param)
|
||||
elif param["param"] == "OF":
|
||||
yield OFParamStmt.from_dict(param)
|
||||
elif param["param"] == "SF":
|
||||
yield SFParamStmt.from_dict(param)
|
||||
elif param["param"] == "LN":
|
||||
yield LNParamStmt.from_dict(param)
|
||||
else:
|
||||
yield UnknownStmt(line)
|
||||
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# Region Mode
|
||||
(mode, r) = _match_one(self.REGION_MODE_STMT, line)
|
||||
if mode:
|
||||
yield RegionModeStmt.from_gerber(line)
|
||||
line = r
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# Quadrant Mode
|
||||
(mode, r) = _match_one(self.QUAD_MODE_STMT, line)
|
||||
if mode:
|
||||
yield QuadrantModeStmt.from_gerber(line)
|
||||
line = r
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# comment
|
||||
(comment, r) = _match_one(self.COMMENT_STMT, line)
|
||||
if comment:
|
||||
yield CommentStmt(comment["comment"])
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# deprecated codes
|
||||
(deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line)
|
||||
if deprecated_unit:
|
||||
stmt = MOParamStmt(param="MO", mo="inch" if "G70" in
|
||||
deprecated_unit["mode"] else "metric")
|
||||
self.settings.units = stmt.mode
|
||||
yield stmt
|
||||
line = r
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
(deprecated_format, r) = _match_one(self.DEPRECATED_FORMAT, line)
|
||||
if deprecated_format:
|
||||
yield DeprecatedStmt.from_gerber(line)
|
||||
line = r
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# eof
|
||||
(eof, r) = _match_one(self.EOF_STMT, line)
|
||||
if eof:
|
||||
yield EofStmt()
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
if line.find('*') > 0:
|
||||
yield UnknownStmt(line)
|
||||
did_something = True
|
||||
line = ""
|
||||
continue
|
||||
|
||||
oldline = line
|
||||
|
||||
def evaluate(self, stmt):
|
||||
""" Evaluate Gerber statement and update image accordingly.
|
||||
|
||||
This method is called once for each statement in the file as it
|
||||
is parsed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
statement : Statement
|
||||
Gerber/Excellon statement to evaluate.
|
||||
|
||||
"""
|
||||
if isinstance(stmt, CoordStmt):
|
||||
self._evaluate_coord(stmt)
|
||||
|
||||
elif isinstance(stmt, ParamStmt):
|
||||
self._evaluate_param(stmt)
|
||||
|
||||
elif isinstance(stmt, ApertureStmt):
|
||||
self._evaluate_aperture(stmt)
|
||||
|
||||
elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)):
|
||||
self._evaluate_mode(stmt)
|
||||
|
||||
elif isinstance(stmt, (CommentStmt, UnknownStmt, DeprecatedStmt, EofStmt)):
|
||||
return
|
||||
|
||||
else:
|
||||
raise Exception("Invalid statement to evaluate")
|
||||
|
||||
def _define_aperture(self, d, shape, modifiers):
|
||||
aperture = None
|
||||
if shape == 'C':
|
||||
diameter = modifiers[0][0]
|
||||
|
||||
hole_diameter = 0
|
||||
rectangular_hole = (0, 0)
|
||||
if len(modifiers[0]) == 2:
|
||||
hole_diameter = modifiers[0][1]
|
||||
elif len(modifiers[0]) == 3:
|
||||
rectangular_hole = modifiers[0][1:3]
|
||||
|
||||
aperture = Circle(position=None, diameter=diameter,
|
||||
hole_diameter=hole_diameter,
|
||||
hole_width=rectangular_hole[0],
|
||||
hole_height=rectangular_hole[1],
|
||||
units=self.settings.units)
|
||||
|
||||
elif shape == 'R':
|
||||
width = modifiers[0][0]
|
||||
height = modifiers[0][1]
|
||||
|
||||
hole_diameter = 0
|
||||
rectangular_hole = (0, 0)
|
||||
if len(modifiers[0]) == 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
elif len(modifiers[0]) == 4:
|
||||
rectangular_hole = modifiers[0][2:4]
|
||||
|
||||
aperture = Rectangle(position=None, width=width, height=height,
|
||||
hole_diameter=hole_diameter,
|
||||
hole_width=rectangular_hole[0],
|
||||
hole_height=rectangular_hole[1],
|
||||
units=self.settings.units)
|
||||
elif shape == 'O':
|
||||
width = modifiers[0][0]
|
||||
height = modifiers[0][1]
|
||||
|
||||
hole_diameter = 0
|
||||
rectangular_hole = (0, 0)
|
||||
if len(modifiers[0]) == 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
elif len(modifiers[0]) == 4:
|
||||
rectangular_hole = modifiers[0][2:4]
|
||||
|
||||
aperture = Obround(position=None, width=width, height=height,
|
||||
hole_diameter=hole_diameter,
|
||||
hole_width=rectangular_hole[0],
|
||||
hole_height=rectangular_hole[1],
|
||||
units=self.settings.units)
|
||||
elif shape == 'P':
|
||||
outer_diameter = modifiers[0][0]
|
||||
number_vertices = int(modifiers[0][1])
|
||||
if len(modifiers[0]) > 2:
|
||||
rotation = modifiers[0][2]
|
||||
else:
|
||||
rotation = 0
|
||||
|
||||
hole_diameter = 0
|
||||
rectangular_hole = (0, 0)
|
||||
if len(modifiers[0]) == 4:
|
||||
hole_diameter = modifiers[0][3]
|
||||
elif len(modifiers[0]) >= 5:
|
||||
rectangular_hole = modifiers[0][3:5]
|
||||
|
||||
aperture = Polygon(position=None, sides=number_vertices,
|
||||
radius=outer_diameter/2.0,
|
||||
hole_diameter=hole_diameter,
|
||||
hole_width=rectangular_hole[0],
|
||||
hole_height=rectangular_hole[1],
|
||||
rotation=rotation)
|
||||
else:
|
||||
aperture = self.macros[shape].build(modifiers)
|
||||
|
||||
aperture.units = self.settings.units
|
||||
self.apertures[d] = aperture
|
||||
|
||||
def _evaluate_mode(self, stmt):
|
||||
if stmt.type == 'RegionMode':
|
||||
if self.region_mode == 'on' and stmt.mode == 'off':
|
||||
# Sometimes we have regions that have no points. Skip those
|
||||
if self.current_region:
|
||||
self.primitives.append(Region(self.current_region,
|
||||
level_polarity=self.level_polarity, units=self.settings.units))
|
||||
|
||||
self.current_region = None
|
||||
self.region_mode = stmt.mode
|
||||
elif stmt.type == 'QuadrantMode':
|
||||
self.quadrant_mode = stmt.mode
|
||||
|
||||
def _evaluate_param(self, stmt):
|
||||
if stmt.param == "FS":
|
||||
self.settings.zero_suppression = stmt.zero_suppression
|
||||
self.settings.format = stmt.format
|
||||
self.settings.notation = stmt.notation
|
||||
elif stmt.param == "MO":
|
||||
self.settings.units = stmt.mode
|
||||
elif stmt.param == "IP":
|
||||
self.image_polarity = stmt.ip
|
||||
elif stmt.param == "LP":
|
||||
self.level_polarity = stmt.lp
|
||||
elif stmt.param == "AM":
|
||||
self.macros[stmt.name] = stmt
|
||||
elif stmt.param == "AD":
|
||||
self._define_aperture(stmt.d, stmt.shape, stmt.modifiers)
|
||||
|
||||
def _evaluate_coord(self, stmt):
|
||||
x = self.x if stmt.x is None else stmt.x
|
||||
y = self.y if stmt.y is None else stmt.y
|
||||
|
||||
if stmt.function in ("G01", "G1"):
|
||||
self.interpolation = 'linear'
|
||||
elif stmt.function in ('G02', 'G2', 'G03', 'G3'):
|
||||
self.interpolation = 'arc'
|
||||
self.direction = ('clockwise' if stmt.function in
|
||||
('G02', 'G2') else 'counterclockwise')
|
||||
|
||||
if stmt.only_function:
|
||||
# Sometimes we get a coordinate statement
|
||||
# that only sets the function. If so, don't
|
||||
# try futher otherwise that might draw/flash something
|
||||
return
|
||||
|
||||
if stmt.op:
|
||||
self.op = stmt.op
|
||||
else:
|
||||
# no implicit op allowed, force here if coord block doesn't have it
|
||||
stmt.op = self.op
|
||||
|
||||
if self.op == "D01" or self.op == "D1":
|
||||
start = (self.x, self.y)
|
||||
end = (x, y)
|
||||
|
||||
if self.interpolation == 'linear':
|
||||
if self.region_mode == 'off':
|
||||
self.primitives.append(Line(start, end,
|
||||
self.apertures[self.aperture],
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units))
|
||||
else:
|
||||
# from gerber spec revision J3, Section 4.5, page 55:
|
||||
# The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness.
|
||||
# The current aperture is associated with the region.
|
||||
# This has no graphical effect, but allows all its attributes to
|
||||
# be applied to the region.
|
||||
|
||||
if self.current_region is None:
|
||||
self.current_region = [Line(start, end,
|
||||
self.apertures.get(self.aperture,
|
||||
Circle((0, 0), 0)),
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units), ]
|
||||
else:
|
||||
self.current_region.append(Line(start, end,
|
||||
self.apertures.get(self.aperture,
|
||||
Circle((0, 0), 0)),
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units))
|
||||
else:
|
||||
i = 0 if stmt.i is None else stmt.i
|
||||
j = 0 if stmt.j is None else stmt.j
|
||||
center = self._find_center(start, end, (i, j))
|
||||
if self.region_mode == 'off':
|
||||
self.primitives.append(Arc(start, end, center, self.direction,
|
||||
self.apertures[self.aperture],
|
||||
quadrant_mode=self.quadrant_mode,
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units))
|
||||
else:
|
||||
if self.current_region is None:
|
||||
self.current_region = [Arc(start, end, center, self.direction,
|
||||
self.apertures.get(self.aperture, Circle((0,0), 0)),
|
||||
quadrant_mode=self.quadrant_mode,
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units),]
|
||||
else:
|
||||
self.current_region.append(Arc(start, end, center, self.direction,
|
||||
self.apertures.get(self.aperture, Circle((0,0), 0)),
|
||||
quadrant_mode=self.quadrant_mode,
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units))
|
||||
# Gerbv seems to reset interpolation mode in regions..
|
||||
# TODO: Make sure this is right.
|
||||
self.interpolation = 'linear'
|
||||
|
||||
elif self.op == "D02" or self.op == "D2":
|
||||
|
||||
if self.region_mode == "on":
|
||||
# D02 in the middle of a region finishes that region and starts a new one
|
||||
if self.current_region and len(self.current_region) > 1:
|
||||
self.primitives.append(Region(self.current_region,
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units))
|
||||
self.current_region = None
|
||||
|
||||
elif self.op == "D03" or self.op == "D3":
|
||||
primitive = copy.deepcopy(self.apertures[self.aperture])
|
||||
|
||||
if primitive is not None:
|
||||
|
||||
if not isinstance(primitive, AMParamStmt):
|
||||
primitive.position = (x, y)
|
||||
primitive.level_polarity = self.level_polarity
|
||||
primitive.units = self.settings.units
|
||||
self.primitives.append(primitive)
|
||||
else:
|
||||
# Aperture Macro
|
||||
for am_prim in primitive.primitives:
|
||||
renderable = am_prim.to_primitive((x, y),
|
||||
self.level_polarity,
|
||||
self.settings.units)
|
||||
if renderable is not None:
|
||||
self.primitives.append(renderable)
|
||||
self.x, self.y = x, y
|
||||
|
||||
def _find_center(self, start, end, offsets):
|
||||
"""
|
||||
In single quadrant mode, the offsets are always positive, which means
|
||||
there are 4 possible centers. The correct center is the only one that
|
||||
results in an arc with sweep angle of less than or equal to 90 degrees
|
||||
in the specified direction
|
||||
"""
|
||||
two_pi = 2 * math.pi
|
||||
if self.quadrant_mode == 'single-quadrant':
|
||||
# The Gerber spec says single quadrant only has one possible center,
|
||||
# and you can detect it based on the angle. But for real files, this
|
||||
# seems to work better - there is usually only one option that makes
|
||||
# sense for the center (since the distance should be the same
|
||||
# from start and end). We select the center with the least error in
|
||||
# radius from all the options with a valid sweep angle.
|
||||
|
||||
sqdist_diff_min = sys.maxsize
|
||||
center = None
|
||||
for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
|
||||
|
||||
test_center = (start[0] + offsets[0] * factors[0],
|
||||
start[1] + offsets[1] * factors[1])
|
||||
|
||||
# Find angle from center to start and end points
|
||||
start_angle = math.atan2(*reversed([_start - _center for _start, _center in zip(start, test_center)]))
|
||||
end_angle = math.atan2(*reversed([_end - _center for _end, _center in zip(end, test_center)]))
|
||||
|
||||
# Clamp angles to 0, 2pi
|
||||
theta0 = (start_angle + two_pi) % two_pi
|
||||
theta1 = (end_angle + two_pi) % two_pi
|
||||
|
||||
# Determine sweep angle in the current arc direction
|
||||
if self.direction == 'counterclockwise':
|
||||
sweep_angle = abs(theta1 - theta0)
|
||||
else:
|
||||
theta0 += two_pi
|
||||
sweep_angle = abs(theta0 - theta1) % two_pi
|
||||
|
||||
# Calculate the radius error
|
||||
sqdist_start = sq_distance(start, test_center)
|
||||
sqdist_end = sq_distance(end, test_center)
|
||||
sqdist_diff = abs(sqdist_start - sqdist_end)
|
||||
|
||||
# Take the option with the lowest radius error from the set of
|
||||
# options with a valid sweep angle
|
||||
# In some rare cases, the sweep angle is numerically (10**-14) above pi/2
|
||||
# So it is safer to compare the angles with some tolerance
|
||||
is_lowest_radius_error = sqdist_diff < sqdist_diff_min
|
||||
is_valid_sweep_angle = sweep_angle >= 0 and sweep_angle <= math.pi / 2.0 + 1e-6
|
||||
if is_lowest_radius_error and is_valid_sweep_angle:
|
||||
center = test_center
|
||||
sqdist_diff_min = sqdist_diff
|
||||
return center
|
||||
else:
|
||||
return (start[0] + offsets[0], start[1] + offsets[1])
|
||||
|
||||
def _evaluate_aperture(self, stmt):
|
||||
self.aperture = stmt.d
|
||||
|
||||
def _match_one(expr, data):
|
||||
match = expr.match(data)
|
||||
if match is None:
|
||||
return ({}, None)
|
||||
else:
|
||||
return (match.groupdict(), data[match.end(0):])
|
||||
|
||||
|
||||
def _match_one_from_many(exprs, data):
|
||||
for expr in exprs:
|
||||
match = expr.match(data)
|
||||
if match:
|
||||
return (match.groupdict(), data[match.end(0):])
|
||||
|
||||
return ({}, None)
|
Po Szerokość: | Wysokość: | Rozmiar: 9.9 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 46 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.3 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 5.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 3.4 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 4.0 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.7 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 70 KiB |