Merge pull request #3 from garretfick/merge-curtacircuitos
Merge curtacircuitos
6
.gitignore
vendored
|
|
@ -37,10 +37,14 @@ nosetests.xml
|
|||
.idea/workspace.xml
|
||||
.idea/misc.xml
|
||||
.idea
|
||||
.settings
|
||||
|
||||
# Komodo Files
|
||||
*.komodoproject
|
||||
|
||||
# OS Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Virtual environment
|
||||
venv
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
# command to install dependencies
|
||||
install:
|
||||
install:
|
||||
- "pip install -r requirements.txt"
|
||||
- "pip install -r test-requirements.txt"
|
||||
- "pip install coveralls"
|
||||
|
||||
# command to run tests
|
||||
script:
|
||||
script:
|
||||
- make test-coverage
|
||||
|
||||
# Coveralls
|
||||
after_success:
|
||||
after_success:
|
||||
- coveralls
|
||||
|
|
|
|||
18
Makefile
|
|
@ -3,25 +3,33 @@ PYTHON ?= python
|
|||
NOSETESTS ?= nosetests
|
||||
|
||||
DOC_ROOT = doc
|
||||
EXAMPLES = examples
|
||||
|
||||
.PHONY: clean
|
||||
clean: doc-clean
|
||||
#$(PYTHON) setup.py clean
|
||||
find . -name '*.pyc' -delete
|
||||
rm -rf coverage .coverage
|
||||
rm -rf *.egg-info
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(NOSETESTS) -s -v gerber
|
||||
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage:
|
||||
rm -rf coverage .coverage
|
||||
$(NOSETESTS) -s -v --with-coverage --cover-package=gerber
|
||||
|
||||
|
||||
.PHONY: doc-html
|
||||
doc-html:
|
||||
(cd $(DOC_ROOT); make html)
|
||||
|
||||
|
||||
.PHONY: doc-clean
|
||||
doc-clean:
|
||||
(cd $(DOC_ROOT); make clean)
|
||||
|
||||
|
||||
.PHONY: examples
|
||||
examples:
|
||||
PYTHONPATH=. $(PYTHON) examples/cairo_example.py
|
||||
PYTHONPATH=. $(PYTHON) examples/pcb_example.py
|
||||
|
||||
|
|
|
|||
36
README.md
|
|
@ -1,21 +1,22 @@
|
|||
gerber-tools
|
||||
pcb-tools
|
||||
============
|
||||

|
||||
[](https://coveralls.io/r/hamiltonkibbe/gerber-tools?branch=master)
|
||||
[](https://travis-ci.org/curtacircuitos/pcb-tools)
|
||||
[](https://coveralls.io/r/curtacircuitos/pcb-tools?branch=master)
|
||||
[](https://readthedocs.org/projects/pcb-tools/?badge=latest)
|
||||
|
||||
Tools to handle Gerber and Excellon files in Python.
|
||||
|
||||
Useage Example:
|
||||
---------------
|
||||
import gerber
|
||||
from gerber.render import GerberSvgContext
|
||||
from gerber.render import GerberCairoContext
|
||||
|
||||
# Read gerber and Excellon files
|
||||
top_copper = gerber.read('example.GTL')
|
||||
nc_drill = gerber.read('example.txt')
|
||||
|
||||
# Rendering context
|
||||
ctx = GerberSvgContext()
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
# Create SVG image
|
||||
top_copper.render(ctx)
|
||||
|
|
@ -25,7 +26,26 @@ Useage Example:
|
|||
Rendering Examples:
|
||||
-------------------
|
||||
###Top Composite rendering
|
||||

|
||||

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

|
||||
Source code for this example can be found [here](examples/cairo_example.py).
|
||||
|
||||
|
||||
Documentation:
|
||||
--------------
|
||||
[PCB Tools Documentation](http://pcb-tools.readthedocs.org/en/latest/)
|
||||
|
||||
|
||||
Development and Testing:
|
||||
------------------------
|
||||
|
||||
Dependencies for developing and testing pcb-tools are listed in test-requirements.txt. Use of a virtual environment is strongly recommended.
|
||||
|
||||
$ virtualenv venv
|
||||
$ source venv/bin/activate
|
||||
(venv)$ pip install -r test-requirements.txt
|
||||
(venv)$ pip install -e .
|
||||
|
||||
We use nose to run pcb-tools's suite of unittests and doctests.
|
||||
|
||||
(venv)$ nosetests
|
||||
|
|
|
|||
4
doc-requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Doc requirements
|
||||
Sphinx==1.2.3
|
||||
numpydoc==0.5
|
||||
|
||||
40
doc/source/about.rst
Normal file
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
@ -47,8 +47,8 @@ source_suffix = '.rst'
|
|||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Gerber Tools'
|
||||
copyright = u'2014, Hamilton Kibbe'
|
||||
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
|
||||
|
|
@ -82,7 +82,7 @@ exclude_patterns = []
|
|||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
add_module_names = False
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
|
|
@ -180,7 +180,7 @@ html_static_path = ['_static']
|
|||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'GerberToolsdoc'
|
||||
htmlhelp_basename = 'PCBToolsdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
|
@ -200,7 +200,7 @@ latex_elements = {
|
|||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'GerberTools.tex', u'Gerber Tools Documentation',
|
||||
('index', 'PCBTools.tex', u'PCB Tools Documentation',
|
||||
u'Hamilton Kibbe', 'manual'),
|
||||
]
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ latex_documents = [
|
|||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'gerbertools', u'Gerber Tools Documentation',
|
||||
('index', 'pcbtools', u'PCB Tools Documentation',
|
||||
[u'Hamilton Kibbe'], 1)
|
||||
]
|
||||
|
||||
|
|
@ -244,8 +244,8 @@ man_pages = [
|
|||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'GerberTools', u'Gerber Tools Documentation',
|
||||
u'Hamilton Kibbe', 'GerberTools', 'One line description of project.',
|
||||
('index', 'PCBTools', u'PCB Tools Documentation',
|
||||
u'Hamilton Kibbe', 'PCBTools', 'Tools for working with PCB CAM files.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Gerber Tools Reference
|
||||
PCB Tools Reference
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
|
|
@ -6,6 +6,5 @@ Gerber Tools Reference
|
|||
|
||||
Gerber (RS-274X) Files <rs274x>
|
||||
Excellon Files <excellon>
|
||||
Operations <operations>
|
||||
Rendering <render>
|
||||
|
||||
|
||||
|
|
|
|||
24
doc/source/documentation/operations.rst
Normal file
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
@ -33,5 +33,5 @@ The :mod:`rs274x` module defines the following classes:
|
|||
.. autoclass:: gerber.rs274x.GerberFile
|
||||
:members:
|
||||
|
||||
.. autoclass:: gerber.rs274x.GerberParser
|
||||
.. autoclass:: gerber.rs274x.GerberParser
|
||||
:members:
|
||||
14
doc/source/features.rst
Normal file
|
|
@ -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
|
||||
============ ======== =========== ================ ====== ======= =======
|
||||
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
.. Gerber Tools documentation master file, created by
|
||||
.. 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.
|
||||
|
||||
Gerber-Tools!
|
||||
PCB-Tools
|
||||
========================================
|
||||
|
||||
Contents:
|
||||
|
|
@ -11,7 +11,8 @@ Contents:
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
intro
|
||||
about
|
||||
features
|
||||
documentation/index
|
||||
|
||||
Indices and tables
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
Gerber Tools Intro
|
||||
==================
|
||||
|
||||
PCB CAM (Gerber) Files
|
||||
------------
|
||||
|
||||
PCB design files (artwork) are most often stored in `Gerber` files. This is
|
||||
a generic term that may refer to `RS-274X (Gerber) <http://en.wikipedia.org/wiki/Gerber_format>`_,
|
||||
`ODB++ <http://en.wikipedia.org/wiki/ODB%2B%2B>`_, or `Excellon <http://en.wikipedia.org/wiki/Excellon_format>`_
|
||||
files.
|
||||
|
||||
|
||||
Gerber-Tools
|
||||
------------
|
||||
|
||||
The gerber-tools module provides tools for working with and rendering Gerber
|
||||
and Excellon files.
|
||||
|
||||
|
||||
BIN
examples/cairo_example.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
69
examples/cairo_example.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#! /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 read
|
||||
from gerber.render import GerberCairoContext, theme
|
||||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
|
||||
|
||||
|
||||
# Open the gerber files
|
||||
copper = read(os.path.join(GERBER_FOLDER, 'copper.GTL'))
|
||||
mask = read(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
|
||||
silk = read(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
|
||||
drill = read(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
# Set opacity and color for copper layer
|
||||
ctx.alpha = 1.0
|
||||
ctx.color = theme.COLORS['hasl copper']
|
||||
|
||||
# Draw the copper layer
|
||||
copper.render(ctx)
|
||||
|
||||
# Set opacity and color for soldermask layer
|
||||
ctx.alpha = 0.75
|
||||
ctx.color = theme.COLORS['green soldermask']
|
||||
|
||||
# Draw the soldermask layer
|
||||
mask.render(ctx, invert=True)
|
||||
|
||||
# Set opacity and color for silkscreen layer
|
||||
ctx.alpha = 1.0
|
||||
ctx.color = theme.COLORS['white']
|
||||
|
||||
# Draw the silkscreen layer
|
||||
silk.render(ctx)
|
||||
|
||||
# Set opacity for drill layer
|
||||
ctx.alpha = 1.0
|
||||
ctx.color = theme.COLORS['black']
|
||||
drill.render(ctx)
|
||||
|
||||
# Write output to png file
|
||||
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_example.png'))
|
||||
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 832 KiB |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 569 KiB |
90
examples/excellon_optimize.py
Normal file
|
|
@ -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()))
|
||||
|
||||
BIN
examples/excellon_optimize_after.PNG
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
examples/excellon_optimize_before.PNG
Normal file
|
After Width: | Height: | Size: 88 KiB |
1811
examples/gerbers/bottom_copper.GBL
Normal file
66
examples/gerbers/bottom_mask.GBS
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
G75*
|
||||
%MOIN*%
|
||||
%OFA0B0*%
|
||||
%FSLAX24Y24*%
|
||||
%IPPOS*%
|
||||
%LPD*%
|
||||
%AMOC8*
|
||||
5,1,8,0,0,1.08239X$1,22.5*
|
||||
%
|
||||
%ADD10C,0.0634*%
|
||||
%ADD11C,0.1360*%
|
||||
%ADD12C,0.0680*%
|
||||
%ADD13C,0.1340*%
|
||||
%ADD14C,0.0476*%
|
||||
D10*
|
||||
X017200Y009464D03*
|
||||
X018200Y009964D03*
|
||||
X018200Y010964D03*
|
||||
X017200Y010464D03*
|
||||
X017200Y011464D03*
|
||||
X018200Y011964D03*
|
||||
D11*
|
||||
X020700Y012714D03*
|
||||
X020700Y008714D03*
|
||||
D12*
|
||||
X018350Y016514D02*
|
||||
X018350Y017114D01*
|
||||
X017350Y017114D02*
|
||||
X017350Y016514D01*
|
||||
X007350Y016664D02*
|
||||
X007350Y017264D01*
|
||||
X006350Y017264D02*
|
||||
X006350Y016664D01*
|
||||
X005350Y016664D02*
|
||||
X005350Y017264D01*
|
||||
X001800Y012564D02*
|
||||
X001200Y012564D01*
|
||||
X001200Y011564D02*
|
||||
X001800Y011564D01*
|
||||
X001800Y010564D02*
|
||||
X001200Y010564D01*
|
||||
X001200Y009564D02*
|
||||
X001800Y009564D01*
|
||||
X001800Y008564D02*
|
||||
X001200Y008564D01*
|
||||
D13*
|
||||
X002350Y005114D03*
|
||||
X002300Y016064D03*
|
||||
X020800Y016064D03*
|
||||
X020800Y005064D03*
|
||||
D14*
|
||||
X015650Y006264D03*
|
||||
X013500Y006864D03*
|
||||
X012100Y005314D03*
|
||||
X009250Y004064D03*
|
||||
X015200Y004514D03*
|
||||
X013550Y008764D03*
|
||||
X013350Y010114D03*
|
||||
X013300Y011464D03*
|
||||
X011650Y013164D03*
|
||||
X010000Y015114D03*
|
||||
X006500Y013714D03*
|
||||
X004150Y011564D03*
|
||||
X014250Y014964D03*
|
||||
X015850Y009914D03*
|
||||
M02*
|
||||
3457
examples/gerbers/copper.GTL
Normal file
51
examples/gerbers/ncdrill.DRD
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
%
|
||||
M48
|
||||
M72
|
||||
T01C0.0236
|
||||
T02C0.0354
|
||||
T03C0.0400
|
||||
T04C0.1260
|
||||
T05C0.1280
|
||||
%
|
||||
T01
|
||||
X9250Y4064
|
||||
X12100Y5314
|
||||
X13500Y6864
|
||||
X15650Y6264
|
||||
X15200Y4514
|
||||
X13550Y8764
|
||||
X13350Y10114
|
||||
X13300Y11464
|
||||
X11650Y13164
|
||||
X10000Y15114
|
||||
X6500Y13714
|
||||
X4150Y11564
|
||||
X14250Y14964
|
||||
X15850Y9914
|
||||
T02
|
||||
X17200Y9464
|
||||
X18200Y9964
|
||||
X18200Y10964
|
||||
X17200Y10464
|
||||
X17200Y11464
|
||||
X18200Y11964
|
||||
T03
|
||||
X18350Y16814
|
||||
X17350Y16814
|
||||
X7350Y16964
|
||||
X6350Y16964
|
||||
X5350Y16964
|
||||
X1500Y12564
|
||||
X1500Y11564
|
||||
X1500Y10564
|
||||
X1500Y9564
|
||||
X1500Y8564
|
||||
T04
|
||||
X2350Y5114
|
||||
X2300Y16064
|
||||
X20800Y16064
|
||||
X20800Y5064
|
||||
T05
|
||||
X20700Y8714
|
||||
X20700Y12714
|
||||
M30
|
||||
2099
examples/gerbers/silkscreen.GTO
Normal file
162
examples/gerbers/soldermask.GTS
Normal file
|
|
@ -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*
|
||||
BIN
examples/pcb_bottom.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
40
examples/pcb_example.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#! /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 composite
|
||||
images using the PCB interface
|
||||
"""
|
||||
|
||||
import os
|
||||
from gerber import PCB
|
||||
from gerber.render import GerberCairoContext, theme
|
||||
|
||||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
|
||||
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
# Create a new PCB
|
||||
pcb = PCB.from_directory(GERBER_FOLDER)
|
||||
|
||||
# Render PCB
|
||||
ctx.render_layers(pcb.top_layers, os.path.join(os.path.dirname(__file__), 'pcb_top.png',), theme.THEMES['OSH Park'])
|
||||
ctx.render_layers(pcb.bottom_layers, os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'), theme.THEMES['OSH Park'])
|
||||
|
||||
BIN
examples/pcb_top.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
|
|
@ -23,4 +23,5 @@ gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon
|
|||
files in python.
|
||||
"""
|
||||
|
||||
from .common import read
|
||||
from .common import read, loads
|
||||
from .pcb import PCB
|
||||
|
|
|
|||
|
|
@ -16,24 +16,24 @@
|
|||
# the License.
|
||||
|
||||
if __name__ == '__main__':
|
||||
from .common import read
|
||||
from .render import GerberSvgContext
|
||||
from gerber.common import read
|
||||
from gerber.render import GerberCairoContext
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, "Usage: python -m gerber <filename> <filename>..."
|
||||
sys.stderr.write("Usage: python -m gerber <filename> <filename>...\n")
|
||||
sys.exit(1)
|
||||
|
||||
ctx = GerberSvgContext()
|
||||
ctx.set_alpha(0.95)
|
||||
ctx = GerberCairoContext()
|
||||
ctx.alpha = 0.95
|
||||
for filename in sys.argv[1:]:
|
||||
print "parsing %s" % filename
|
||||
print("parsing %s" % filename)
|
||||
if 'GTO' in filename or 'GBO' in filename:
|
||||
ctx.set_color((1, 1, 1))
|
||||
ctx.set_alpha(0.8)
|
||||
ctx.color = (1, 1, 1)
|
||||
ctx.alpha = 0.8
|
||||
elif 'GTS' in filename or 'GBS' in filename:
|
||||
ctx.set_color((0.2, 0.2, 0.75))
|
||||
ctx.set_alpha(0.8)
|
||||
ctx.color = (0.2, 0.2, 0.75)
|
||||
ctx.alpha = 0.8
|
||||
gerberfile = read(filename)
|
||||
gerberfile.render(ctx)
|
||||
|
||||
|
|
|
|||
109
gerber/am_eval.py
Normal file
|
|
@ -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 = []
|
||||
255
gerber/am_read.py
Normal file
|
|
@ -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)
|
||||
1060
gerber/am_statements.py
Normal file
121
gerber/cam.py
|
|
@ -27,9 +27,36 @@ 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='trailing', format=(2, 5)):
|
||||
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
|
||||
|
|
@ -38,15 +65,52 @@ class FileSettings(object):
|
|||
raise ValueError('Units must be either inch or metric')
|
||||
self.units = units
|
||||
|
||||
if zero_suppression not in ['leading', 'trailing']:
|
||||
raise ValueError('Zero suppression must be either leading or \
|
||||
trailling')
|
||||
self.zero_suppression = zero_suppression
|
||||
if 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
|
||||
|
|
@ -54,8 +118,12 @@ class FileSettings(object):
|
|||
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()
|
||||
|
||||
|
|
@ -69,22 +137,41 @@ class FileSettings(object):
|
|||
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)
|
||||
|
||||
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):
|
||||
|
|
@ -131,14 +218,17 @@ class CamFile(object):
|
|||
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 []
|
||||
self.primitives = primitives
|
||||
if primitives is not None:
|
||||
self.primitives = primitives
|
||||
self.filename = filename
|
||||
self.layer_name = layer_name
|
||||
|
||||
|
|
@ -156,11 +246,17 @@ class CamFile(object):
|
|||
|
||||
@property
|
||||
def bounds(self):
|
||||
""" File baundaries
|
||||
""" File boundaries
|
||||
"""
|
||||
pass
|
||||
|
||||
def render(self, ctx, filename=None):
|
||||
def to_inch(self):
|
||||
pass
|
||||
|
||||
def to_metric(self):
|
||||
pass
|
||||
|
||||
def render(self, ctx, invert=False, filename=None):
|
||||
""" Generate image of layer.
|
||||
|
||||
Parameters
|
||||
|
|
@ -172,7 +268,12 @@ class CamFile(object):
|
|||
If provided, save the rendered image to `filename`
|
||||
"""
|
||||
ctx.set_bounds(self.bounds)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@
|
|||
# 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.
|
||||
|
|
@ -30,13 +36,39 @@ def read(filename):
|
|||
CncFile object representing the file, either GerberFile or
|
||||
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
|
||||
"""
|
||||
import rs274x
|
||||
import excellon
|
||||
from utils import detect_file_format
|
||||
fmt = detect_file_format(filename)
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
fmt = detect_file_format(data)
|
||||
if fmt == 'rs274x':
|
||||
return rs274x.read(filename)
|
||||
elif fmt == 'excellon':
|
||||
return excellon.read(filename)
|
||||
elif fmt == 'ipc_d_356':
|
||||
return ipc356.read(filename)
|
||||
else:
|
||||
return None
|
||||
raise ParseError('Unable to detect file format')
|
||||
|
||||
|
||||
def loads(data):
|
||||
""" Read gerber or excellon file contents from a string and return a
|
||||
representative object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
gerber or excellon file contents as a string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : CncFile subclass
|
||||
CncFile object representing the file, either GerberFile or
|
||||
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
|
||||
"""
|
||||
|
||||
fmt = detect_file_format(data)
|
||||
if fmt == 'rs274x':
|
||||
return rs274x.loads(data)
|
||||
elif fmt == 'excellon':
|
||||
return excellon.loads(data)
|
||||
else:
|
||||
raise TypeError('Unable to detect file format')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
|
@ -13,8 +13,8 @@
|
|||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon File module
|
||||
====================
|
||||
|
|
@ -23,11 +23,22 @@ Excellon File module
|
|||
This module provides Excellon file classes and parsing utilities
|
||||
"""
|
||||
|
||||
|
||||
from .excellon_statements import *
|
||||
from .cam import CamFile, FileSettings
|
||||
from .primitives import Drill
|
||||
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
|
||||
|
|
@ -42,10 +53,108 @@ def read(filename):
|
|||
An ExcellonFile created from the specified file.
|
||||
|
||||
"""
|
||||
detected_settings = detect_excellon_format(filename)
|
||||
settings = FileSettings(**detected_settings)
|
||||
zeros = ''
|
||||
# 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, settings = None, tools = None):
|
||||
""" Read data from string and return an ExcellonFile
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon file contents
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
self.position = tuple(map(inch, self.position))
|
||||
|
||||
def to_metric(self):
|
||||
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, y_offset):
|
||||
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):
|
||||
self.start = tuple(map(inch, self.start))
|
||||
self.end = tuple(map(inch, self.end))
|
||||
|
||||
def to_metric(self):
|
||||
self.start = tuple(map(metric, self.start))
|
||||
self.end = tuple(map(metric, self.end))
|
||||
|
||||
@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, y_offset):
|
||||
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):
|
||||
|
|
@ -53,6 +162,9 @@ class ExcellonFile(CamFile):
|
|||
|
||||
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
|
||||
|
|
@ -72,39 +184,172 @@ class ExcellonFile(CamFile):
|
|||
either 'inch' or 'metric'.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, statements, tools, hits, settings, filename=None):
|
||||
super(ExcellonFile, self).__init__(statements=statements,
|
||||
settings=settings,
|
||||
filename=filename)
|
||||
self.tools = tools
|
||||
self.hits = hits
|
||||
self.primitives = [Drill(position, tool.diameter)
|
||||
for tool, position in self.hits]
|
||||
|
||||
@property
|
||||
def 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, hit, units=self.settings.units))
|
||||
elif isinstance(hit, DrillSlot):
|
||||
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
|
||||
else:
|
||||
raise ValueError('Unknown hit type')
|
||||
|
||||
return primitives
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
xmin = ymin = 100000000000
|
||||
xmax = ymax = -100000000000
|
||||
for tool, position in self.hits:
|
||||
radius = tool.diameter / 2.
|
||||
x = position[0]
|
||||
y = position[1]
|
||||
xmin = min(x - radius, xmin)
|
||||
xmax = max(x + radius, xmax)
|
||||
ymin = min(y - radius, ymin)
|
||||
ymax = max(y + radius, ymax)
|
||||
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):
|
||||
""" Print drill report
|
||||
def report(self, filename=None):
|
||||
""" Print or save drill report
|
||||
"""
|
||||
pass
|
||||
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):
|
||||
with open(filename, 'w') as f:
|
||||
def write(self, filename=None):
|
||||
filename = filename if filename is not None else self.filename
|
||||
with open(filename, 'w') as f:
|
||||
for statement in self.statements:
|
||||
f.write(statement.to_excellon() + '\n')
|
||||
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':
|
||||
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()
|
||||
|
||||
def to_metric(self):
|
||||
""" Convert units to metric
|
||||
"""
|
||||
if self.units != 'metric':
|
||||
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:
|
||||
primitive.to_metric()
|
||||
for hit in self.hits:
|
||||
hit.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)
|
||||
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):
|
||||
|
|
@ -113,26 +358,30 @@ class ExcellonParser(object):
|
|||
Parameters
|
||||
----------
|
||||
settings : FileSettings or dict-like
|
||||
Excellon file settings to use when interpreting the excellon file.
|
||||
"""
|
||||
def __init__(self, settings=None):
|
||||
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.zero_suppression = 'trailing'
|
||||
self.format = (2, 5)
|
||||
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.pos = [0., 0.]
|
||||
self.drill_down = False
|
||||
# Default for lated is None, which means we don't know
|
||||
self.plated = ExcellonTool.PLATED_UNKNOWN
|
||||
if settings is not None:
|
||||
self.units = settings.units
|
||||
self.zero_suppression = settings.zero_suppression
|
||||
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)]
|
||||
|
|
@ -159,18 +408,51 @@ class ExcellonParser(object):
|
|||
return len(self.hits)
|
||||
|
||||
def parse(self, filename):
|
||||
with open(filename, 'r') as f:
|
||||
for line in f:
|
||||
self._parse(line.strip())
|
||||
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(self, line):
|
||||
#line = line.strip()
|
||||
zs = self._settings().zero_suppression
|
||||
fmt = self._settings().format
|
||||
def _parse_line(self, line):
|
||||
# skip empty lines
|
||||
if not line.strip():
|
||||
return
|
||||
|
||||
if line[0] == ';':
|
||||
self.statements.append(CommentStmt.from_excellon(line))
|
||||
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())
|
||||
|
|
@ -180,30 +462,109 @@ class ExcellonParser(object):
|
|||
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)
|
||||
stmt = EndOfProgramStmt.from_excellon(line, self._settings())
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'G00':
|
||||
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':
|
||||
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) and
|
||||
('LZ' in line or 'TZ' in line)):
|
||||
elif 'INCH' in line or 'METRIC' in line:
|
||||
stmt = UnitStmt.from_excellon(line)
|
||||
self.units = stmt.units
|
||||
self.zero_suppression = stmt.zero_suppression
|
||||
self.zeros = stmt.zeros
|
||||
if stmt.format:
|
||||
self.format = stmt.format
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'M71' or line [:3] == 'M72':
|
||||
elif line[:3] == 'M71' or line[:3] == 'M72':
|
||||
stmt = MeasuringModeStmt.from_excellon(line)
|
||||
self.units = stmt.units
|
||||
self.statements.append(stmt)
|
||||
|
|
@ -220,52 +581,191 @@ class ExcellonParser(object):
|
|||
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':
|
||||
tool = ExcellonTool.from_excellon(line, self._settings())
|
||||
self.tools[tool.number] = tool
|
||||
self.statements.append(tool)
|
||||
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.active_tool = self.tools[stmt.tool]
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[0] in ['X', 'Y']:
|
||||
stmt = CoordinateStmt.from_excellon(line, fmt, zs)
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
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':
|
||||
self.hits.append((self.active_tool, tuple(self.pos)))
|
||||
# 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,
|
||||
zero_suppression=self.zero_suppression,
|
||||
notation=self.notation)
|
||||
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(filename):
|
||||
def detect_excellon_format(data=None, filename=None):
|
||||
""" Detect excellon file decimal format and zero-suppression settings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Name of the file to parse. This does not check if the file is actually
|
||||
an Excellon file, so do that before calling this.
|
||||
data : string
|
||||
String containing contents of Excellon file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -277,15 +777,21 @@ def detect_excellon_format(filename):
|
|||
results = {}
|
||||
detected_zeros = None
|
||||
detected_format = None
|
||||
zs_options = ('leading', 'trailing', )
|
||||
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(filename)
|
||||
p.parse_raw(data)
|
||||
|
||||
# Get zero_suppression from a unit statement
|
||||
zero_statements = [stmt.zero_suppression for stmt in p.statements
|
||||
zero_statements = [stmt.zeros for stmt in p.statements
|
||||
if isinstance(stmt, UnitStmt)]
|
||||
|
||||
# get format from altium comment
|
||||
|
|
@ -294,40 +800,40 @@ def detect_excellon_format(filename):
|
|||
and 'FILE_FORMAT' in stmt.comment]
|
||||
|
||||
detected_format = (tuple([int(val) for val in
|
||||
format_comment[0].split('=')[1].split(':')])
|
||||
format_comment[0].split('=')[1].split(':')])
|
||||
if len(format_comment) == 1 else None)
|
||||
detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None
|
||||
|
||||
# Bail out here if possible
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zero_suppression': detected_zeros}
|
||||
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:
|
||||
zs_options = (detected_zeros,)
|
||||
zeros_options = (detected_zeros,)
|
||||
|
||||
# Brute force all remaining options, and pick the best looking one...
|
||||
for zs in zs_options:
|
||||
for zeros in zeros_options:
|
||||
for fmt in format_options:
|
||||
key = (fmt, zs)
|
||||
settings = FileSettings(zero_suppression=zs, format=fmt)
|
||||
key = (fmt, zeros)
|
||||
settings = FileSettings(zeros=zeros, format=fmt)
|
||||
try:
|
||||
p = ExcellonParser(settings)
|
||||
p.parse(filename)
|
||||
size = tuple([t[1] - t[0] for t in p.bounds])
|
||||
p.parse_raw(data)
|
||||
size = tuple([t[0] - t[1] for t in p.bounds])
|
||||
hole_area = 0.0
|
||||
for hit in p.hits:
|
||||
tool = hit[0]
|
||||
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 results.iterkeys())
|
||||
zeros = set(key[1] for key in results.iterkeys())
|
||||
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:
|
||||
|
|
@ -335,7 +841,7 @@ def detect_excellon_format(filename):
|
|||
|
||||
# Bail out here if we got everything....
|
||||
if detected_format is not None and detected_zeros is not None:
|
||||
return {'format': detected_format, 'zero_suppression': detected_zeros}
|
||||
return {'format': detected_format, 'zeros': detected_zeros}
|
||||
|
||||
# Otherwise score each option and pick the best candidate
|
||||
else:
|
||||
|
|
@ -344,9 +850,9 @@ def detect_excellon_format(filename):
|
|||
size, count, diameter = results[key]
|
||||
scores[key] = _layer_size_score(size, count, diameter)
|
||||
minscore = min(scores.values())
|
||||
for key in scores.iterkeys():
|
||||
for key in iter(scores.keys()):
|
||||
if scores[key] == minscore:
|
||||
return {'format': key[0], 'zero_suppression': key[1]}
|
||||
return {'format': key[0], 'zeros': key[1]}
|
||||
|
||||
|
||||
def _layer_size_score(size, hole_count, hole_area):
|
||||
|
|
@ -354,7 +860,11 @@ def _layer_size_score(size, hole_count, hole_area):
|
|||
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
|
||||
size_score = (board_area - 8) ** 2
|
||||
return hole_score * size_score
|
||||
|
||||
25
gerber/excellon_report/excellon_drr.py
Normal file
|
|
@ -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
|
||||
"""
|
||||
|
||||
105
gerber/excellon_settings.py
Normal file
|
|
@ -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
|
||||
|
|
@ -21,26 +21,53 @@ Excellon Statements
|
|||
|
||||
"""
|
||||
|
||||
from .utils import parse_gerber_value, write_gerber_value, decimal_string
|
||||
import re
|
||||
import uuid
|
||||
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', 'UnknownStmt',
|
||||
]
|
||||
'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 to_excellon(self):
|
||||
pass
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
class ExcellonTool(ExcellonStatement):
|
||||
|
|
@ -86,9 +113,29 @@ class ExcellonTool(ExcellonStatement):
|
|||
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):
|
||||
def from_excellon(cls, line, settings, id=None, plated=None):
|
||||
""" Create a Tool from an excellon file tool definition line.
|
||||
|
||||
Parameters
|
||||
|
|
@ -107,6 +154,7 @@ class ExcellonTool(ExcellonStatement):
|
|||
commands = re.split('([BCFHSTZ])', line)[1:]
|
||||
commands = [(command, value) for command, value in pairwise(commands)]
|
||||
args = {}
|
||||
args['id'] = id
|
||||
nformat = settings.format
|
||||
zero_suppression = settings.zero_suppression
|
||||
for cmd, val in commands:
|
||||
|
|
@ -124,6 +172,10 @@ class ExcellonTool(ExcellonStatement):
|
|||
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
|
||||
args['plated'] = plated
|
||||
return cls(settings, **args)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -143,9 +195,11 @@ class ExcellonTool(ExcellonStatement):
|
|||
tool : ExcellonTool
|
||||
An ExcellonTool initialized with the parameters in tool_dict.
|
||||
"""
|
||||
return cls(settings, 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')
|
||||
|
|
@ -154,12 +208,16 @@ class ExcellonTool(ExcellonStatement):
|
|||
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):
|
||||
fmt = self.settings.format
|
||||
zs = self.settings.format
|
||||
stmt = 'T%d' % self.number
|
||||
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:
|
||||
|
|
@ -170,25 +228,55 @@ class ExcellonTool(ExcellonStatement):
|
|||
if self.rpm < 100000.:
|
||||
stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs)
|
||||
else:
|
||||
stmt += 'S%g' % self.rpm / 1000.
|
||||
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'
|
||||
return '<ExcellonTool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
|
||||
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):
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
""" Create a ToolSelectionStmt from an excellon file line.
|
||||
|
||||
Parameters
|
||||
|
|
@ -203,103 +291,335 @@ class ToolSelectionStmt(ExcellonStatement):
|
|||
"""
|
||||
line = line[1:]
|
||||
compensation_index = None
|
||||
tool = int(line[:2])
|
||||
if len(line) > 2:
|
||||
compensation_index = int(line[2:])
|
||||
return cls(tool, compensation_index)
|
||||
|
||||
def __init__(self, tool, compensation_index=None):
|
||||
# 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):
|
||||
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_excellon(cls, line, nformat=(2, 5), zero_suppression='trailing'):
|
||||
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], nformat,
|
||||
zero_suppression)
|
||||
x_coord = parse_gerber_value(splitline[0], settings.format,
|
||||
settings.zero_suppression)
|
||||
if len(splitline) == 2:
|
||||
y_coord = parse_gerber_value(splitline[1], nformat,
|
||||
zero_suppression)
|
||||
y_coord = parse_gerber_value(splitline[1], settings.format,
|
||||
settings.zero_suppression)
|
||||
else:
|
||||
y_coord = parse_gerber_value(line.strip(' Y'), nformat,
|
||||
zero_suppression)
|
||||
return cls(x_coord, y_coord)
|
||||
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):
|
||||
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):
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
return cls(line.lstrip(';'))
|
||||
|
||||
def __init__(self, comment):
|
||||
def __init__(self, comment, **kwargs):
|
||||
super(CommentStmt, self).__init__(**kwargs)
|
||||
self.comment = comment
|
||||
|
||||
def to_excellon(self):
|
||||
def to_excellon(self, settings=None):
|
||||
return ';%s' % self.comment
|
||||
|
||||
|
||||
class HeaderBeginStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, **kwargs):
|
||||
super(HeaderBeginStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self):
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M48'
|
||||
|
||||
|
||||
class HeaderEndStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, **kwargs):
|
||||
super(HeaderEndStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self):
|
||||
def to_excellon(self, settings=None):
|
||||
return 'M95'
|
||||
|
||||
|
||||
class RewindStopStmt(ExcellonStatement):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, **kwargs):
|
||||
super(RewindStopStmt, self).__init__(**kwargs)
|
||||
|
||||
def to_excellon(self):
|
||||
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):
|
||||
|
||||
def __init__(self, x=None, y=None):
|
||||
@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):
|
||||
def to_excellon(self, settings=None):
|
||||
stmt = 'M30'
|
||||
if self.x is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x)
|
||||
|
|
@ -307,119 +627,346 @@ class EndOfProgramStmt(ExcellonStatement):
|
|||
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):
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
units = 'inch' if 'INCH' in line else 'metric'
|
||||
zero_suppression = 'trailing' if 'LZ' in line else 'leading'
|
||||
return cls(units, zero_suppression)
|
||||
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', zero_suppression='trailing'):
|
||||
def __init__(self, units='inch', zeros='leading', format=None, **kwargs):
|
||||
super(UnitStmt, self).__init__(**kwargs)
|
||||
self.units = units.lower()
|
||||
self.zero_suppression = zero_suppression
|
||||
self.zeros = zeros
|
||||
self.format = format
|
||||
|
||||
def to_excellon(self):
|
||||
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.zero_suppression == 'trailing'
|
||||
'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):
|
||||
return cls('off') if 'OFF' in line else cls('on')
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs)
|
||||
|
||||
def __init__(self, mode='off'):
|
||||
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):
|
||||
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):
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
version = int(line.split(',')[1])
|
||||
return cls(version)
|
||||
return cls(version, **kwargs)
|
||||
|
||||
def __init__(self, version=1):
|
||||
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):
|
||||
def to_excellon(self, settings=None):
|
||||
return 'VER,%d' % self.version
|
||||
|
||||
|
||||
class FormatStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
fmt = int(line.split(',')[1])
|
||||
return cls(fmt)
|
||||
return cls(fmt, **kwargs)
|
||||
|
||||
def __init__(self, format=1):
|
||||
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):
|
||||
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):
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
linked = [int(tool) for tool in line.split('/')]
|
||||
return cls(linked)
|
||||
return cls(linked, **kwargs)
|
||||
|
||||
def __init__(self, linked_tools):
|
||||
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):
|
||||
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):
|
||||
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') if 'M72' in line else cls('metric')
|
||||
return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs)
|
||||
|
||||
def __init__(self, units='inch'):
|
||||
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):
|
||||
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):
|
||||
return cls(line)
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
return cls(line, **kwargs)
|
||||
|
||||
def __init__(self, stmt):
|
||||
def __init__(self, stmt, **kwargs):
|
||||
super(UnknownStmt, self).__init__(**kwargs)
|
||||
self.stmt = stmt
|
||||
|
||||
def to_excellon(self):
|
||||
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.
|
||||
|
|
@ -428,4 +975,4 @@ def pairwise(iterator):
|
|||
"""
|
||||
itr = iter(iterator)
|
||||
while True:
|
||||
yield tuple([itr.next() for i in range(2)])
|
||||
yield tuple([next(itr) for i in range(2)])
|
||||
|
|
|
|||
186
gerber/excellon_tool.py
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
#!/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
|
||||
36
gerber/exceptions.py
Normal file
|
|
@ -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
|
||||
461
gerber/ipc356.py
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
#! /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 IPC_D_356
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of file to parse
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.ipc356.IPC_D_356`
|
||||
An IPC_D_356 object created from the specified file.
|
||||
|
||||
"""
|
||||
# File object should use settings from source file by default.
|
||||
return IPC_D_356.from_file(filename)
|
||||
|
||||
|
||||
class IPC_D_356(CamFile):
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filename):
|
||||
parser = IPC_D_356_Parser()
|
||||
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 IPC_D_356_Parser(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:
|
||||
oldline = ''
|
||||
for line in f:
|
||||
# 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 IPC_D_356(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'}
|
||||
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
|
||||
245
gerber/layers.py
|
|
@ -15,40 +15,217 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
top_copper_ext = ['gtl', 'cmp', 'top', ]
|
||||
top_copper_name = ['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
|
||||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
bottom_copper_ext = ['gbl', 'sld', 'bot', 'sol', ]
|
||||
bottom_coppper_name = ['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
|
||||
|
||||
internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
|
||||
'g2', 'g3', 'g4', 'g5', 'g6', ]
|
||||
internal_layer_name = ['art', 'internal']
|
||||
|
||||
power_plane_name = ['pgp', 'pwr', ]
|
||||
ground_plane_name = ['gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', 'gnd',
|
||||
'ground', ]
|
||||
|
||||
top_silk_ext = ['gto', 'sst', 'plc', 'ts', 'skt', ]
|
||||
top_silk_name = ['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
|
||||
|
||||
bottom_silk_ext = ['gbo', 'ssb', 'pls', 'bs', 'skb', ]
|
||||
bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ]
|
||||
|
||||
top_mask_ext = ['gts', 'stc', 'tmk', 'smt', 'tr', ]
|
||||
top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
|
||||
'mst', ]
|
||||
|
||||
bottom_mask_ext = ['gbs', 'sts', 'bmk', 'smb', 'br', ]
|
||||
bottom_mask_name = ['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
|
||||
|
||||
top_paste_ext = ['gtp', 'tm']
|
||||
top_paste_name = ['sp01', 'toppaste', 'pst']
|
||||
|
||||
bottom_paste_ext = ['gbp', 'bm']
|
||||
bottom_paste_name = ['sp02', 'botpaste', 'psb']
|
||||
|
||||
board_outline_ext = ['gko']
|
||||
board_outline_name = ['BDR', 'border', 'out', ]
|
||||
from .excellon import ExcellonFile
|
||||
from .ipc356 import IPC_D_356
|
||||
|
||||
|
||||
Hint = namedtuple('Hint', 'layer ext name')
|
||||
|
||||
hints = [
|
||||
Hint(layer='top',
|
||||
ext=['gtl', 'cmp', 'top', ],
|
||||
name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
|
||||
),
|
||||
Hint(layer='bottom',
|
||||
ext=['gbl', 'sld', 'bot', 'sol', 'bottom', ],
|
||||
name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
|
||||
),
|
||||
Hint(layer='internal',
|
||||
ext=['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
|
||||
'g2', 'g3', 'g4', 'g5', 'g6', ],
|
||||
name=['art', 'internal', 'pgp', 'pwr', 'gp1', 'gp2', 'gp3', 'gp4',
|
||||
'gt5', 'gp6', 'gnd', 'ground', ]
|
||||
),
|
||||
Hint(layer='topsilk',
|
||||
ext=['gto', 'sst', 'plc', 'ts', 'skt', 'topsilk', ],
|
||||
name=['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
|
||||
),
|
||||
Hint(layer='bottomsilk',
|
||||
ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', ],
|
||||
name=['bsilk', 'ssb', 'botsilk', ]
|
||||
),
|
||||
Hint(layer='topmask',
|
||||
ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ],
|
||||
name=['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
|
||||
'mst', ]
|
||||
),
|
||||
Hint(layer='bottommask',
|
||||
ext=['gbs', 'sts', 'bmk', 'smb', 'br', 'bottommask', ],
|
||||
name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
|
||||
),
|
||||
Hint(layer='toppaste',
|
||||
ext=['gtp', 'tm', 'toppaste', ],
|
||||
name=['sp01', 'toppaste', 'pst']
|
||||
),
|
||||
Hint(layer='bottompaste',
|
||||
ext=['gbp', 'bm', 'bottompaste', ],
|
||||
name=['sp02', 'botpaste', 'psb']
|
||||
),
|
||||
Hint(layer='outline',
|
||||
ext=['gko', 'outline', ],
|
||||
name=['BDR', 'border', 'out', ]
|
||||
),
|
||||
Hint(layer='ipc_netlist',
|
||||
ext=['ipc'],
|
||||
name=[],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def guess_layer_class(filename):
|
||||
try:
|
||||
directory, name = os.path.split(filename)
|
||||
name, ext = os.path.splitext(name.lower())
|
||||
for hint in hints:
|
||||
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 sort_layers(layers):
|
||||
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
|
||||
'internal', 'bottom', 'bottommask', 'bottomsilk',
|
||||
'bottompaste', 'drill', ]
|
||||
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)
|
||||
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_gerber(cls, camfile):
|
||||
filename = camfile.filename
|
||||
layer_class = guess_layer_class(filename)
|
||||
if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'):
|
||||
return DrillLayer.from_gerber(camfile)
|
||||
elif layer_class == 'internal':
|
||||
return InternalLayer.from_gerber(camfile)
|
||||
if isinstance(camfile, IPC_D_356):
|
||||
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_gerber(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_gerber(cls, camfile):
|
||||
filename = camfile.filename
|
||||
try:
|
||||
order = int(re.search(r'\d+', filename).group())
|
||||
except:
|
||||
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)
|
||||
|
||||
|
||||
class LayerSet(object):
|
||||
|
||||
def __init__(self, name, layers, **kwargs):
|
||||
super(LayerSet, self).__init__(**kwargs)
|
||||
self.name = name
|
||||
self.layers = list(layers)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.layers)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.layers[item]
|
||||
|
||||
def to_render(self):
|
||||
return self.layers
|
||||
|
||||
def apply_theme(self, theme):
|
||||
pass
|
||||
|
|
|
|||
25
gerber/ncparam/allegro.py
Normal file
|
|
@ -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
|
||||
"""
|
||||
|
||||
126
gerber/operations.py
Normal file
|
|
@ -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
|
||||
101
gerber/pcb.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#! /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, LayerSet, sort_layers
|
||||
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_gerber(camfile)
|
||||
layers.append(layer)
|
||||
names.add(os.path.splitext(filename)[0])
|
||||
if verbose:
|
||||
print('Added {} layer <{}>'.format(layer.layer_class, filename))
|
||||
except ParseError:
|
||||
if verbose:
|
||||
print('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]
|
||||
return board_layers + drill_layers
|
||||
|
||||
@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]
|
||||
return board_layers + drill_layers
|
||||
|
||||
@property
|
||||
def drill_layers(self):
|
||||
return [l for l in self.layers if l.layer_class == 'drill']
|
||||
|
||||
@property
|
||||
def copper_layers(self):
|
||||
return [layer for layer in self.layers if layer.layer_class in
|
||||
('top', 'bottom', 'internal')]
|
||||
|
||||
@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
|
||||
1628
gerber/primitives.py
|
|
@ -24,5 +24,4 @@ SVG is the only supported format.
|
|||
"""
|
||||
|
||||
|
||||
from svgwrite_backend import GerberSvgContext
|
||||
from cairo_backend import GerberCairoContext
|
||||
from .cairo_backend import GerberCairoContext
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
gerber.render.apertures
|
||||
============
|
||||
**Gerber Aperture base classes**
|
||||
|
||||
This module provides base classes for gerber apertures. These are used by
|
||||
the rendering engine to draw the gerber file.
|
||||
"""
|
||||
import math
|
||||
|
||||
class Aperture(object):
|
||||
""" Gerber Aperture base class
|
||||
"""
|
||||
def draw(self, ctx, x, y):
|
||||
raise NotImplementedError('The draw method must be implemented \
|
||||
in an Aperture subclass.')
|
||||
|
||||
def flash(self, ctx, x, y):
|
||||
raise NotImplementedError('The flash method must be implemented \
|
||||
in an Aperture subclass.')
|
||||
|
||||
def _arc_params(self, startx, starty, x, y, i, j):
|
||||
center = (startx + i, starty + j)
|
||||
radius = math.sqrt(math.pow(center[0] - x, 2) +
|
||||
math.pow(center[1] - y, 2))
|
||||
delta_x0 = startx - center[0]
|
||||
delta_y0 = center[1] - starty
|
||||
delta_x1 = x - center[0]
|
||||
delta_y1 = center[1] - y
|
||||
start_angle = math.atan2(delta_y0, delta_x0)
|
||||
end_angle = math.atan2(delta_y1, delta_x1)
|
||||
return {'center': center, 'radius': radius,
|
||||
'start_angle': start_angle, 'end_angle': end_angle}
|
||||
|
||||
|
||||
class Circle(Aperture):
|
||||
""" Circular Aperture base class
|
||||
"""
|
||||
def __init__(self, diameter=0.0):
|
||||
self.diameter = diameter
|
||||
|
||||
|
||||
class Rect(Aperture):
|
||||
""" Rectangular Aperture base class
|
||||
"""
|
||||
def __init__(self, size=(0, 0)):
|
||||
self.size = size
|
||||
|
||||
|
||||
class Obround(Aperture):
|
||||
""" Obround Aperture base class
|
||||
"""
|
||||
def __init__(self, size=(0, 0)):
|
||||
self.size = size
|
||||
|
||||
|
||||
class Polygon(Aperture):
|
||||
""" Polygon Aperture base class
|
||||
"""
|
||||
pass
|
||||
|
|
@ -12,80 +12,517 @@
|
|||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .render import GerberContext
|
||||
from operator import mul
|
||||
import cairocffi as cairo
|
||||
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
try:
|
||||
import cairo
|
||||
except ImportError:
|
||||
import cairocffi as cairo
|
||||
|
||||
import math
|
||||
from operator import mul, div
|
||||
import tempfile
|
||||
|
||||
SCALE = 300.
|
||||
import cairocffi as cairo
|
||||
|
||||
from ..primitives import *
|
||||
from .render import GerberContext, RenderSettings
|
||||
from .theme import THEMES
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
|
||||
class GerberCairoContext(GerberContext):
|
||||
def __init__(self, surface=None, size=(1000, 1000)):
|
||||
GerberContext.__init__(self)
|
||||
if surface is None:
|
||||
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
|
||||
size[0], size[1])
|
||||
else:
|
||||
self.surface = surface
|
||||
self.ctx = cairo.Context(self.surface)
|
||||
self.size = size
|
||||
self.ctx.translate(0, self.size[1])
|
||||
self.scale = (SCALE,SCALE)
|
||||
self.ctx.scale(1, -1)
|
||||
self.apertures = {}
|
||||
self.background = False
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
xbounds, ybounds = bounds
|
||||
self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], SCALE * (xbounds[1]- xbounds[0]), SCALE * (ybounds[1] - ybounds[0]))
|
||||
self.ctx.set_source_rgb(0,0,0)
|
||||
self.ctx.fill()
|
||||
def __init__(self, scale=300):
|
||||
super(GerberCairoContext, self).__init__()
|
||||
self.scale = (scale, scale)
|
||||
self.surface = None
|
||||
self.ctx = None
|
||||
self.active_layer = None
|
||||
self.output_ctx = None
|
||||
self.bg = False
|
||||
self.mask = None
|
||||
self.mask_ctx = None
|
||||
self.origin_in_inch = None
|
||||
self.size_in_inch = None
|
||||
self._xform_matrix = None
|
||||
|
||||
@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
|
||||
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)
|
||||
self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
|
||||
self.output_ctx.scale(1, -1)
|
||||
self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]),
|
||||
(-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
|
||||
self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0,
|
||||
x0=-self.origin_in_pixels[0],
|
||||
y0=self.size_in_pixels[1] + self.origin_in_pixels[1])
|
||||
|
||||
def render_layers(self, layers, filename, theme=THEMES['default']):
|
||||
""" Render a set of layers
|
||||
"""
|
||||
self.set_bounds(layers[0].bounds, True)
|
||||
self._paint_background(True)
|
||||
|
||||
for layer in layers:
|
||||
self._render_layer(layer, theme)
|
||||
self.dump(filename)
|
||||
|
||||
def dump(self, filename):
|
||||
""" Save image as `filename`
|
||||
"""
|
||||
if filename and filename.lower().endswith(".svg"):
|
||||
self.surface.finish()
|
||||
self.surface_buffer.flush()
|
||||
with open(filename, "w") 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 string containing the rendered image.
|
||||
"""
|
||||
fobj = StringIO()
|
||||
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 _render_layer(self, layer, theme=THEMES['default']):
|
||||
settings = theme.get(layer.layer_class, RenderSettings())
|
||||
self.color = settings.color
|
||||
self.alpha = settings.alpha
|
||||
self.invert = settings.invert
|
||||
|
||||
# Get a new clean layer to render on
|
||||
self._new_render_layer()
|
||||
if settings.mirror:
|
||||
raise Warning('mirrored layers aren\'t supported yet...')
|
||||
for prim in layer.primitives:
|
||||
self.render(prim)
|
||||
# Add layer to image
|
||||
self._flatten()
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = map(mul, line.start, self.scale)
|
||||
end = map(mul, line.end, self.scale)
|
||||
self.ctx.set_source_rgb (*color)
|
||||
self.ctx.set_line_width(line.width * SCALE)
|
||||
start = [pos * scale for pos, scale in zip(line.start, self.scale)]
|
||||
end = [pos * scale for pos, scale in zip(line.end, self.scale)]
|
||||
if not self.invert:
|
||||
<<<<<<< HEAD
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if line.level_polarity == "dark"
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
=======
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if line.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
if isinstance(line.aperture, Circle):
|
||||
<<<<<<< HEAD
|
||||
width = line.aperture.diameter
|
||||
=======
|
||||
width = line.aperture.diameter
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
self.ctx.set_line_width(width * self.scale[0])
|
||||
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
self.ctx.move_to(*start)
|
||||
self.ctx.line_to(*end)
|
||||
<<<<<<< HEAD
|
||||
self.ctx.stroke()
|
||||
=======
|
||||
self.ctx.stroke()
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
elif isinstance(line.aperture, Rectangle):
|
||||
points = [self.scale_point(x) for x in line.vertices]
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.move_to(*points[0])
|
||||
for point in points[1:]:
|
||||
self.ctx.line_to(*point)
|
||||
self.ctx.fill()
|
||||
|
||||
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
|
||||
angle1 = arc.start_angle
|
||||
angle2 = arc.end_angle
|
||||
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)
|
||||
|
||||
if not self.invert:
|
||||
<<<<<<< HEAD
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if arc.level_polarity == "dark"\
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
=======
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if arc.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
self.ctx.set_line_width(width * self.scale[0])
|
||||
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
self.ctx.move_to(*start) # You actually have to do this...
|
||||
if arc.direction == 'counterclockwise':
|
||||
<<<<<<< HEAD
|
||||
self.ctx.arc(center[0], center[1], radius, angle1, angle2)
|
||||
else:
|
||||
self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2)
|
||||
=======
|
||||
self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2)
|
||||
else:
|
||||
self.ctx.arc_negative(*center, radius=radius,
|
||||
angle1=angle1, angle2=angle2)
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
self.ctx.move_to(*end) # ...lame
|
||||
|
||||
def _render_region(self, region, color):
|
||||
if not self.invert:
|
||||
<<<<<<< HEAD
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if region.level_polarity == "dark"
|
||||
=======
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if region.level_polarity == 'dark'
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
self.ctx.move_to(*self.scale_point(region.primitives[0].start))
|
||||
for prim in region.primitives:
|
||||
if isinstance(prim, Line):
|
||||
self.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':
|
||||
self.ctx.arc(*center, radius=radius,
|
||||
angle1=angle1, angle2=angle2)
|
||||
else:
|
||||
self.ctx.arc_negative(*center, radius=radius,
|
||||
angle1=angle1, angle2=angle2)
|
||||
<<<<<<< HEAD
|
||||
self.ctx.fill()
|
||||
def _render_circle(self, circle, color):
|
||||
center = self.scale_point(circle.position)
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if circle.level_polarity == "dark"
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
if circle.hole_diameter > 0:
|
||||
self.ctx.push_group()
|
||||
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
|
||||
if circle.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
|
||||
self.ctx.pop_group_to_source()
|
||||
self.ctx.paint_with_alpha(1)
|
||||
=======
|
||||
self.ctx.fill()
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
center = self.scale_point(circle.position)
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(
|
||||
cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.arc(*center, radius=circle.radius *
|
||||
self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
|
||||
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))])
|
||||
<<<<<<< HEAD
|
||||
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if rectangle.level_polarity == "dark"
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
if rectangle.rotation != 0:
|
||||
self.ctx.save()
|
||||
|
||||
center = map(mul, rectangle.position, self.scale)
|
||||
matrix = cairo.Matrix()
|
||||
matrix.translate(center[0], center[1])
|
||||
# For drawing, we already handles the translation
|
||||
lower_left[0] = lower_left[0] - center[0]
|
||||
lower_left[1] = lower_left[1] - center[1]
|
||||
matrix.rotate(rectangle.rotation)
|
||||
self.ctx.transform(matrix)
|
||||
|
||||
if rectangle.hole_diameter > 0:
|
||||
self.ctx.push_group()
|
||||
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.rectangle(lower_left[0], lower_left[1], width, height)
|
||||
self.ctx.fill()
|
||||
|
||||
if rectangle.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
center = map(mul, rectangle.position, self.scale)
|
||||
self.ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
|
||||
self.ctx.pop_group_to_source()
|
||||
self.ctx.paint_with_alpha(1)
|
||||
|
||||
if rectangle.rotation != 0:
|
||||
self.ctx.restore()
|
||||
=======
|
||||
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(
|
||||
cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.rectangle(*lower_left, width=width, height=height)
|
||||
self.ctx.fill()
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
if obround.hole_diameter > 0:
|
||||
self.ctx.push_group()
|
||||
|
||||
self._render_circle(obround.subshapes['circle1'], color)
|
||||
self._render_circle(obround.subshapes['circle2'], color)
|
||||
self._render_rectangle(obround.subshapes['rectangle'], color)
|
||||
|
||||
if obround.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
center = map(mul, obround.position, self.scale)
|
||||
self.ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
|
||||
self.ctx.pop_group_to_source()
|
||||
self.ctx.paint_with_alpha(1)
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
|
||||
# TODO Ths does not handle rotation of a polygon
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER if polygon.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
if polygon.hole_radius > 0:
|
||||
self.ctx.push_group()
|
||||
|
||||
vertices = polygon.vertices
|
||||
|
||||
self.ctx.set_line_width(0)
|
||||
self.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
|
||||
self.ctx.move_to(*map(mul, vertices[-1], self.scale))
|
||||
for v in vertices:
|
||||
self.ctx.line_to(*map(mul, v, self.scale))
|
||||
|
||||
self.ctx.fill()
|
||||
|
||||
if polygon.hole_radius > 0:
|
||||
# Render the center clear
|
||||
center = tuple(map(mul, polygon.position, self.scale))
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
|
||||
self.ctx.fill()
|
||||
|
||||
self.ctx.pop_group_to_source()
|
||||
self.ctx.paint_with_alpha(1)
|
||||
|
||||
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
|
||||
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
self.ctx.set_line_width(width * self.scale[0])
|
||||
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
self.ctx.move_to(*start)
|
||||
self.ctx.line_to(*end)
|
||||
self.ctx.stroke()
|
||||
|
||||
def _render_amgroup(self, amgroup, color):
|
||||
self.ctx.push_group()
|
||||
for primitive in amgroup.primitives:
|
||||
self.render(primitive)
|
||||
self.ctx.pop_group_to_source()
|
||||
self.ctx.paint_with_alpha(1)
|
||||
|
||||
def _render_region(self, region, color):
|
||||
points = [tuple(map(mul, point, self.scale)) for point in region.points]
|
||||
self.ctx.set_source_rgb (*color)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.move_to(*points[0])
|
||||
for point in points[1:]:
|
||||
self.ctx.move_to(*point)
|
||||
self.ctx.fill()
|
||||
def _render_test_record(self, primitive, color):
|
||||
position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)]
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
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_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(
|
||||
cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' 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 _render_circle(self, circle, color):
|
||||
center = map(mul, circle.position, self.scale)
|
||||
self.ctx.set_source_rgb (*color)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.arc(*center, radius=circle.radius * SCALE, angle1=0, angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
def _new_render_layer(self, color=None):
|
||||
size_in_pixels = self.scale_point(self.size_in_inch)
|
||||
layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
|
||||
ctx = cairo.Context(layer)
|
||||
ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
|
||||
ctx.scale(1, -1)
|
||||
ctx.translate(-(self.origin_in_inch[0] * self.scale[0]),
|
||||
(-self.origin_in_inch[1] * self.scale[0])
|
||||
- size_in_pixels[1])
|
||||
if self.invert:
|
||||
ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
ctx.set_source_rgba(*self.color, alpha=self.alpha)
|
||||
ctx.paint()
|
||||
self.ctx = ctx
|
||||
self.active_layer = layer
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
ll = map(mul, rectangle.lower_left, self.scale)
|
||||
width, height = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale)))
|
||||
self.ctx.set_source_rgb (*color)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.rectangle(*ll,width=width, height=height)
|
||||
self.ctx.fill()
|
||||
def _flatten(self):
|
||||
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
<<<<<<< HEAD
|
||||
ptn = cairo.SurfacePattern(self.active_layer)
|
||||
=======
|
||||
ptn = cairo.SurfacePattern(self.active_layer)
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
ptn.set_matrix(self._xform_matrix)
|
||||
self.output_ctx.set_source(ptn)
|
||||
self.output_ctx.paint()
|
||||
self.ctx = None
|
||||
self.active_layer = None
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
self._render_circle(obround.subshapes['circle1'], color)
|
||||
self._render_circle(obround.subshapes['circle2'], color)
|
||||
self._render_rectangle(obround.subshapes['rectangle'], color)
|
||||
def _paint_background(self, force=False):
|
||||
if (not self.bg) or force:
|
||||
self.bg = True
|
||||
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
<<<<<<< HEAD
|
||||
self.output_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0)
|
||||
self.output_ctx.paint()
|
||||
|
||||
def _render_drill(self, circle, color):
|
||||
self._render_circle(circle, color)
|
||||
def scale_point(self, point):
|
||||
return tuple([coord * scale for coord, scale in zip(point, self.scale)])
|
||||
=======
|
||||
self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0)
|
||||
self.output_ctx.paint()
|
||||
|
||||
def dump(self, filename):
|
||||
self.surface.write_to_png(filename)
|
||||
def scale_point(self, point):
|
||||
return tuple([coord * scale for coord, scale in zip(point, self.scale)])
|
||||
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
|
||||
|
|
|
|||
189
gerber/render/excellon_backend.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
|
||||
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):
|
||||
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
|
||||
|
||||
|
|
@ -23,12 +23,13 @@ Rendering
|
|||
Render Gerber and Excellon files to a variety of formats. The render module
|
||||
currently supports SVG rendering using the `svgwrite` library.
|
||||
"""
|
||||
from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt,
|
||||
CoordStmt, ApertureStmt, RegionModeStmt,
|
||||
QuadrantModeStmt,
|
||||
)
|
||||
|
||||
|
||||
from ..primitives import *
|
||||
from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt,
|
||||
CoordStmt, ApertureStmt, RegionModeStmt,
|
||||
QuadrantModeStmt,)
|
||||
|
||||
|
||||
class GerberContext(object):
|
||||
""" Gerber rendering context base class
|
||||
|
|
@ -41,7 +42,7 @@ class GerberContext(object):
|
|||
Attributes
|
||||
----------
|
||||
units : string
|
||||
Measurement units
|
||||
Measurement units. 'inch' or 'metric'
|
||||
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color used for rendering as a tuple of normalized (red, green, blue) values.
|
||||
|
|
@ -56,79 +57,89 @@ class GerberContext(object):
|
|||
alpha : float
|
||||
Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.)
|
||||
"""
|
||||
|
||||
def __init__(self, units='inch'):
|
||||
self.units = units
|
||||
self.color = (0.7215, 0.451, 0.200)
|
||||
self.drill_color = (0.25, 0.25, 0.25)
|
||||
self.background_color = (0.0, 0.0, 0.0)
|
||||
self.alpha = 1.0
|
||||
self._units = units
|
||||
self._color = (0.7215, 0.451, 0.200)
|
||||
self._background_color = (0.0, 0.0, 0.0)
|
||||
self._alpha = 1.0
|
||||
self._invert = False
|
||||
self.ctx = None
|
||||
|
||||
def set_units(self, units):
|
||||
""" Set context measurement units
|
||||
@property
|
||||
def units(self):
|
||||
return self._units
|
||||
|
||||
Parameters
|
||||
----------
|
||||
unit : string
|
||||
Measurement units. may be 'inch' or 'metric'
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If `unit` is not 'inch' or 'metric'
|
||||
"""
|
||||
@units.setter
|
||||
def units(self, units):
|
||||
if units not in ('inch', 'metric'):
|
||||
raise ValueError('Units may be "inch" or "metric"')
|
||||
self.units = units
|
||||
self._units = units
|
||||
|
||||
def set_color(self, color):
|
||||
""" Set rendering color.
|
||||
@property
|
||||
def color(self):
|
||||
return self._color
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color as a tuple of (red, green, blue) values. Each channel is
|
||||
represented as a float value in (0, 1)
|
||||
"""
|
||||
self.color = color
|
||||
@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
|
||||
|
||||
def set_drill_color(self, color):
|
||||
""" Set color used for rendering drill hits.
|
||||
@property
|
||||
def drill_color(self):
|
||||
return self._drill_color
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color as a tuple of (red, green, blue) values. Each channel is
|
||||
represented as a float value in (0, 1)
|
||||
"""
|
||||
self.drill_color = 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
|
||||
|
||||
def set_background_color(self, color):
|
||||
""" Set rendering background color
|
||||
@property
|
||||
def background_color(self):
|
||||
return self._background_color
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color as a tuple of (red, green, blue) values. Each channel is
|
||||
represented as a float value in (0, 1)
|
||||
"""
|
||||
self.background_color = color
|
||||
@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
|
||||
|
||||
def set_alpha(self, alpha):
|
||||
""" Set layer rendering opacity
|
||||
@property
|
||||
def alpha(self):
|
||||
return self._alpha
|
||||
|
||||
.. note::
|
||||
Not all backends/rendering devices support this parameter.
|
||||
@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
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha : float
|
||||
Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque)
|
||||
"""
|
||||
self.alpha = alpha
|
||||
@property
|
||||
def invert(self):
|
||||
return self._invert
|
||||
|
||||
@invert.setter
|
||||
def invert(self, invert):
|
||||
self._invert = invert
|
||||
|
||||
def render(self, primitive):
|
||||
color = (self.color if primitive.level_polarity == 'dark'
|
||||
else self.background_color)
|
||||
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):
|
||||
|
|
@ -142,11 +153,35 @@ class GerberContext(object):
|
|||
elif isinstance(primitive, Obround):
|
||||
self._render_obround(primitive, color)
|
||||
elif isinstance(primitive, Polygon):
|
||||
self._render_polygon(Polygon, color)
|
||||
self._render_polygon(primitive, color)
|
||||
elif isinstance(primitive, Drill):
|
||||
self._render_drill(primitive, self.drill_color)
|
||||
else:
|
||||
return
|
||||
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 _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
|
||||
|
|
@ -171,4 +206,21 @@ class GerberContext(object):
|
|||
|
||||
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
|
||||
|
|
|
|||
495
gerber/render/rs274x_backend.py
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
"""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):
|
||||
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)
|
||||
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):
|
||||
|
||||
self._select_aperture(line.aperture)
|
||||
|
||||
self._render_level_polarity(line)
|
||||
|
||||
# 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):
|
||||
|
||||
# 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)
|
||||
|
||||
# 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:
|
||||
|
||||
if isinstance(p, Line):
|
||||
self._render_line(p, color)
|
||||
else:
|
||||
self._render_arc(p, color)
|
||||
|
||||
if self.explicit_region_move_end:
|
||||
self.body.append(CoordStmt.move(None, None))
|
||||
|
||||
self.body.append(RegionModeStmt.off())
|
||||
|
||||
def _render_level_polarity(self, region):
|
||||
if region.level_polarity != self._level_polarity:
|
||||
self._level_polarity = region.level_polarity
|
||||
self.body.append(LPParamStmt.from_region(region))
|
||||
|
||||
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, dcode = None):
|
||||
'''Define a circlar aperture'''
|
||||
|
||||
aper = self._circles.get((diameter, hole_diameter), 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)
|
||||
self._circles[(diameter, hole_diameter)] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
|
||||
aper = self._get_circle(circle.diameter, circle.hole_diameter)
|
||||
self._render_flash(circle, aper)
|
||||
|
||||
def _get_rectangle(self, width, height, dcode = None):
|
||||
'''Get a rectanglar aperture. If it isn't defined, create it'''
|
||||
|
||||
key = (width, 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)
|
||||
self._rects[(width, height)] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
|
||||
aper = self._get_rectangle(rectangle.width, rectangle.height)
|
||||
self._render_flash(rectangle, aper)
|
||||
|
||||
def _get_obround(self, width, height, dcode = None):
|
||||
|
||||
key = (width, 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)
|
||||
self._obrounds[key] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
|
||||
aper = self._get_obround(obround.width, obround.height)
|
||||
self._render_flash(obround, aper)
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
|
||||
aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius)
|
||||
self._render_flash(polygon, aper)
|
||||
|
||||
def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None):
|
||||
|
||||
key = (radius, num_vertices, rotation, hole_radius)
|
||||
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_radius * 2)
|
||||
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
|
||||
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Based on render_svg.py by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .render import GerberContext
|
||||
from operator import mul
|
||||
import svgwrite
|
||||
|
||||
SCALE = 300
|
||||
|
||||
|
||||
def svg_color(color):
|
||||
color = tuple([int(ch * 255) for ch in color])
|
||||
return 'rgb(%d, %d, %d)' % color
|
||||
|
||||
|
||||
class GerberSvgContext(GerberContext):
|
||||
def __init__(self):
|
||||
GerberContext.__init__(self)
|
||||
self.scale = (SCALE, -SCALE)
|
||||
self.dwg = svgwrite.Drawing()
|
||||
self.background = False
|
||||
|
||||
def dump(self, filename):
|
||||
self.dwg.saveas(filename)
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
xbounds, ybounds = bounds
|
||||
size = (SCALE * (xbounds[1] - xbounds[0]),
|
||||
SCALE * (ybounds[1] - ybounds[0]))
|
||||
if not self.background:
|
||||
vbox = '%f, %f, %f, %f' % (SCALE * xbounds[0], -SCALE * ybounds[1],
|
||||
size[0], size[1])
|
||||
self.dwg = svgwrite.Drawing(viewBox=vbox)
|
||||
rect = self.dwg.rect(insert=(SCALE * xbounds[0],
|
||||
-SCALE * ybounds[1]),
|
||||
size=size,
|
||||
fill=svg_color(self.background_color))
|
||||
self.dwg.add(rect)
|
||||
self.background = True
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = map(mul, line.start, self.scale)
|
||||
end = map(mul, line.end, self.scale)
|
||||
aline = self.dwg.line(start=start, end=end,
|
||||
stroke=svg_color(color),
|
||||
stroke_width=SCALE * line.width,
|
||||
stroke_linecap='round')
|
||||
aline.stroke(opacity=self.alpha)
|
||||
self.dwg.add(aline)
|
||||
|
||||
def _render_region(self, region, color):
|
||||
points = [tuple(map(mul, point, self.scale)) for point in region.points]
|
||||
region_path = self.dwg.path(d='M %f, %f' % points[0],
|
||||
fill=svg_color(color),
|
||||
stroke='none')
|
||||
region_path.fill(opacity=self.alpha)
|
||||
for point in points[1:]:
|
||||
region_path.push('L %f, %f' % point)
|
||||
self.dwg.add(region_path)
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
center = map(mul, circle.position, self.scale)
|
||||
acircle = self.dwg.circle(center=center,
|
||||
r = SCALE * circle.radius,
|
||||
fill=svg_color(color))
|
||||
acircle.fill(opacity=self.alpha)
|
||||
self.dwg.add(acircle)
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
center = map(mul, rectangle.position, self.scale)
|
||||
size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale)))
|
||||
insert = center[0] - size[0] / 2., center[1] - size[1] / 2.
|
||||
arect = self.dwg.rect(insert=insert, size=size,
|
||||
fill=svg_color(color))
|
||||
arect.fill(opacity=self.alpha)
|
||||
self.dwg.add(arect)
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
x, y = tuple(map(mul, obround.position, self.scale))
|
||||
xsize, ysize = tuple(map(mul, (obround.width, obround.height),
|
||||
self.scale))
|
||||
xscale, yscale = self.scale
|
||||
|
||||
# Corner case...
|
||||
if xsize == ysize:
|
||||
circle = self.dwg.circle(center=(x, y),
|
||||
r = (xsize / 2.0),
|
||||
fill=svg_color(color))
|
||||
circle.fill(opacity=self.alpha)
|
||||
self.dwg.add(circle)
|
||||
|
||||
# Horizontal obround
|
||||
elif xsize > ysize:
|
||||
rectx = xsize - ysize
|
||||
recty = ysize
|
||||
c1 = self.dwg.circle(center=(x - (rectx / 2.0), y),
|
||||
r = (ysize / 2.0),
|
||||
fill=svg_color(color))
|
||||
|
||||
c2 = self.dwg.circle(center=(x + (rectx / 2.0), y),
|
||||
r = (ysize / 2.0),
|
||||
fill=svg_color(color))
|
||||
|
||||
rect = self.dwg.rect(insert=(x, y),
|
||||
size=(xsize, ysize),
|
||||
fill=svg_color(color))
|
||||
c1.fill(opacity=self.alpha)
|
||||
c2.fill(opacity=self.alpha)
|
||||
rect.fill(opacity=self.alpha)
|
||||
self.dwg.add(c1)
|
||||
self.dwg.add(c2)
|
||||
self.dwg.add(rect)
|
||||
|
||||
# Vertical obround
|
||||
else:
|
||||
rectx = xsize
|
||||
recty = ysize - xsize
|
||||
c1 = self.dwg.circle(center=(x, y - (recty / 2.)),
|
||||
r = (xsize / 2.),
|
||||
fill=svg_color(color))
|
||||
|
||||
c2 = self.dwg.circle(center=(x, y + (recty / 2.)),
|
||||
r = (xsize / 2.),
|
||||
fill=svg_color(color))
|
||||
|
||||
rect = self.dwg.rect(insert=(x, y),
|
||||
size=(xsize, ysize),
|
||||
fill=svg_color(color))
|
||||
c1.fill(opacity=self.alpha)
|
||||
c2.fill(opacity=self.alpha)
|
||||
rect.fill(opacity=self.alpha)
|
||||
self.dwg.add(c1)
|
||||
self.dwg.add(c2)
|
||||
self.dwg.add(rect)
|
||||
|
||||
def _render_drill(self, circle, color):
|
||||
center = map(mul, circle.position, self.scale)
|
||||
hit = self.dwg.circle(center=center, r=SCALE * circle.radius,
|
||||
fill=svg_color(color))
|
||||
self.dwg.add(hit)
|
||||
70
gerber/render/theme.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#! /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),
|
||||
'blue': (0.0, 0.0, 1.0),
|
||||
'fr-4': (0.290, 0.345, 0.0),
|
||||
'green soldermask': (0.0, 0.612, 0.396),
|
||||
'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.686, 0.525, 0.510),
|
||||
'hasl copper': (0.871, 0.851, 0.839)
|
||||
}
|
||||
|
||||
|
||||
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['black'], alpha=0.0))
|
||||
self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white']))
|
||||
self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white']))
|
||||
self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True))
|
||||
self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True))
|
||||
self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
|
||||
self.bottom = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
|
||||
self.drill = kwargs.get('drill', RenderSettings(COLORS['black']))
|
||||
self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red']))
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def get(self, key, noneval=None):
|
||||
val = getattr(self, key)
|
||||
return val if val is not None else noneval
|
||||
|
||||
|
||||
THEMES = {
|
||||
'default': Theme(),
|
||||
'OSH Park': Theme(name='OSH Park',
|
||||
top=RenderSettings(COLORS['enig copper']),
|
||||
bottom=RenderSettings(COLORS['enig copper']),
|
||||
topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True),
|
||||
bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=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)),
|
||||
}
|
||||
543
gerber/rs274x.py
|
|
@ -18,13 +18,21 @@
|
|||
""" This module provides an RS-274-X class and parser.
|
||||
"""
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
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
|
||||
|
|
@ -42,6 +50,22 @@ def read(filename):
|
|||
return GerberParser().parse(filename)
|
||||
|
||||
|
||||
def loads(data):
|
||||
""" Generate a GerberFile object from rs274x data in memory
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing gerber file contents
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.rs274x.GerberFile`
|
||||
A GerberFile created from the specified file.
|
||||
"""
|
||||
return GerberParser().parse_raw(data)
|
||||
|
||||
|
||||
class GerberFile(CamFile):
|
||||
""" A class representing a single gerber file
|
||||
|
||||
|
|
@ -71,9 +95,11 @@ class GerberFile(CamFile):
|
|||
`bounds` is stored as ((min x, max x), (min y, max y))
|
||||
|
||||
"""
|
||||
def __init__(self, statements, settings, primitives, filename=None):
|
||||
super(GerberFile, self).__init__(statements, settings, primitives, filename)
|
||||
|
||||
def __init__(self, statements, settings, primitives, apertures, filename=None):
|
||||
super(GerberFile, self).__init__(statements, settings, primitives, filename)
|
||||
|
||||
self.apertures = apertures
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
|
|
@ -87,29 +113,61 @@ class GerberFile(CamFile):
|
|||
|
||||
@property
|
||||
def bounds(self):
|
||||
xbounds = [0.0, 0.0]
|
||||
ybounds = [0.0, 0.0]
|
||||
for stmt in [stmt for stmt in self.statements
|
||||
if isinstance(stmt, CoordStmt)]:
|
||||
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:
|
||||
if stmt.x < xbounds[0]:
|
||||
xbounds[0] = stmt.x
|
||||
elif stmt.x > xbounds[1]:
|
||||
xbounds[1] = stmt.x
|
||||
min_x = min(stmt.x, min_x)
|
||||
max_x = max(stmt.x, max_x)
|
||||
if stmt.y is not None:
|
||||
if stmt.y < ybounds[0]:
|
||||
ybounds[0] = stmt.y
|
||||
elif stmt.y > ybounds[1]:
|
||||
ybounds[1] = stmt.y
|
||||
return (xbounds, ybounds)
|
||||
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)
|
||||
|
||||
def write(self, filename):
|
||||
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())
|
||||
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):
|
||||
|
|
@ -118,42 +176,50 @@ class GerberParser(object):
|
|||
NUMBER = r"[\+-]?\d+"
|
||||
DECIMAL = r"[\+-]?\d+([.]?\d+)?"
|
||||
STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+"
|
||||
NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+"
|
||||
FUNCTION = r"G\d{2}"
|
||||
NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+"
|
||||
|
||||
COORD_OP = r"D[0]?[123]"
|
||||
|
||||
FS = r"(?P<param>FS)(?P<zero>(L|T))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])"
|
||||
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))"
|
||||
IP = r"(?P<param>IP)(?P<ip>(POS|NEG))"
|
||||
LP = r"(?P<param>LP)(?P<lp>(D|C))"
|
||||
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,]*)?"
|
||||
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,]*)"
|
||||
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,]*)"
|
||||
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,]*)"
|
||||
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,]*)?".format(name=NAME)
|
||||
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>.*)".format(name=NAME)
|
||||
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)"
|
||||
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,%]*)"
|
||||
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,%]*)"
|
||||
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,%]*)"
|
||||
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)".format(name=NAME)
|
||||
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>[^%]*)".format(name=NAME)
|
||||
|
||||
# begin deprecated
|
||||
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
|
||||
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, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN)
|
||||
PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS]
|
||||
PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY,
|
||||
AD_MACRO, AM, AS, 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=FUNCTION, op=COORD_OP)))
|
||||
r"(?P<op>{op})?\*".format(number=NUMBER, function=COORD_FUNCTION, op=COORD_OP)))
|
||||
|
||||
APERTURE_STMT = re.compile(r"(G54)?D(?P<d>\d+)\*")
|
||||
APERTURE_STMT = re.compile(r"(?P<deprecated>(G54)|(G55))?D(?P<d>\d+)\*")
|
||||
|
||||
COMMENT_STMT = re.compile(r"G04(?P<comment>[^*]*)(\*)?")
|
||||
COMMENT_STMT = re.compile(r"G0?4(?P<comment>[^*]*)(\*)?")
|
||||
|
||||
EOF_STMT = re.compile(r"(?P<eof>M02)\*")
|
||||
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])\*')
|
||||
|
|
@ -163,10 +229,11 @@ class GerberParser(object):
|
|||
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'
|
||||
|
|
@ -176,31 +243,67 @@ class GerberParser(object):
|
|||
self.quadrant_mode = 'multi-quadrant'
|
||||
self.step_and_repeat = (1, 1, 0, 0)
|
||||
|
||||
|
||||
def parse(self, filename):
|
||||
fp = open(filename, "r")
|
||||
data = fp.readlines()
|
||||
with open(filename, "rU") as fp:
|
||||
data = fp.read()
|
||||
return self.parse_raw(data, filename)
|
||||
|
||||
for stmt in self._parse(data):
|
||||
def parse_raw(self, data, filename=None):
|
||||
for stmt in self._parse(self._split_commands(data)):
|
||||
self.evaluate(stmt)
|
||||
self.statements.append(stmt)
|
||||
|
||||
return GerberFile(self.statements, self.settings, self.primitives, filename)
|
||||
# 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):
|
||||
s = ""
|
||||
string = ""
|
||||
for stmt in self.statements:
|
||||
s += str(stmt) + "\n"
|
||||
return s
|
||||
string += str(stmt) + "\n"
|
||||
return string
|
||||
|
||||
def _parse(self, data):
|
||||
oldline = ''
|
||||
|
||||
for i, line in enumerate(data):
|
||||
for line in data:
|
||||
line = oldline + line.strip()
|
||||
|
||||
# skip empty lines
|
||||
|
|
@ -208,13 +311,87 @@ class GerberParser(object):
|
|||
continue
|
||||
|
||||
# deal with multi-line parameters
|
||||
if line.startswith("%") and not line.endswith("%"):
|
||||
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"] == "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)
|
||||
|
|
@ -232,23 +409,6 @@ class GerberParser(object):
|
|||
did_something = True
|
||||
continue
|
||||
|
||||
# coord
|
||||
(coord, r) = _match_one(self.COORD_STMT, line)
|
||||
if coord:
|
||||
yield CoordStmt.from_dict(coord, self.settings)
|
||||
line = r
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# aperture selection
|
||||
(aperture, r) = _match_one(self.APERTURE_STMT, line)
|
||||
if aperture:
|
||||
yield ApertureStmt(**aperture)
|
||||
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
||||
# comment
|
||||
(comment, r) = _match_one(self.COMMENT_STMT, line)
|
||||
if comment:
|
||||
|
|
@ -257,37 +417,22 @@ class GerberParser(object):
|
|||
line = r
|
||||
continue
|
||||
|
||||
# parameter
|
||||
(param, r) = _match_one_from_many(self.PARAM_STMT, line)
|
||||
if param:
|
||||
if param["param"] == "FS":
|
||||
stmt = FSParamStmt.from_dict(param)
|
||||
self.settings.zero_suppression = stmt.zero_suppression
|
||||
self.settings.format = stmt.format
|
||||
self.settings.notation = stmt.notation
|
||||
yield stmt
|
||||
elif param["param"] == "MO":
|
||||
stmt = MOParamStmt.from_dict(param)
|
||||
self.settings.units = stmt.mode
|
||||
yield stmt
|
||||
elif param["param"] == "IP":
|
||||
yield IPParamStmt.from_dict(param)
|
||||
elif param["param"] == "LP":
|
||||
yield LPParamStmt.from_dict(param)
|
||||
elif param["param"] == "AD":
|
||||
yield ADParamStmt.from_dict(param)
|
||||
elif param["param"] == "AM":
|
||||
yield AMParamStmt.from_dict(param)
|
||||
elif param["param"] == "OF":
|
||||
yield OFParamStmt.from_dict(param)
|
||||
elif param["param"] == "IN":
|
||||
yield INParamStmt.from_dict(param)
|
||||
elif param["param"] == "LN":
|
||||
yield LNParamStmt.from_dict(param)
|
||||
else:
|
||||
yield UnknownStmt(line)
|
||||
did_something = True
|
||||
# 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
|
||||
|
|
@ -298,14 +443,6 @@ class GerberParser(object):
|
|||
line = r
|
||||
continue
|
||||
|
||||
if False:
|
||||
print self.COORD_STMT.pattern
|
||||
print self.APERTURE_STMT.pattern
|
||||
print self.COMMENT_STMT.pattern
|
||||
print self.EOF_STMT.pattern
|
||||
for i in self.PARAM_STMT:
|
||||
print i.pattern
|
||||
|
||||
if line.find('*') > 0:
|
||||
yield UnknownStmt(line)
|
||||
did_something = True
|
||||
|
|
@ -338,32 +475,69 @@ class GerberParser(object):
|
|||
elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)):
|
||||
self._evaluate_mode(stmt)
|
||||
|
||||
elif isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)):
|
||||
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 = float(modifiers[0][0])
|
||||
aperture = Circle(position=None, diameter=diameter)
|
||||
diameter = modifiers[0][0]
|
||||
|
||||
if len(modifiers[0]) >= 2:
|
||||
hole_diameter = modifiers[0][1]
|
||||
else:
|
||||
hole_diameter = None
|
||||
|
||||
aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units)
|
||||
elif shape == 'R':
|
||||
width = float(modifiers[0][0])
|
||||
height = float(modifiers[0][1])
|
||||
aperture = Rectangle(position=None, width=width, height=height)
|
||||
width = modifiers[0][0]
|
||||
height = modifiers[0][1]
|
||||
|
||||
if len(modifiers[0]) >= 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
else:
|
||||
hole_diameter = None
|
||||
|
||||
aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
|
||||
elif shape == 'O':
|
||||
width = float(modifiers[0][0])
|
||||
height = float(modifiers[0][1])
|
||||
aperture = Obround(position=None, width=width, height=height)
|
||||
width = modifiers[0][0]
|
||||
height = modifiers[0][1]
|
||||
|
||||
if len(modifiers[0]) >= 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
else:
|
||||
hole_diameter = None
|
||||
|
||||
aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, 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
|
||||
|
||||
if len(modifiers[0]) > 3:
|
||||
hole_diameter = modifiers[0][3]
|
||||
else:
|
||||
hole_diameter = None
|
||||
aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation)
|
||||
else:
|
||||
aperture = self.macros[shape].build(modifiers)
|
||||
|
||||
self.apertures[d] = aperture
|
||||
|
||||
def _evaluate_mode(self, stmt):
|
||||
if stmt.type == 'RegionMode':
|
||||
if self.region_mode == 'on' and stmt.mode == 'off':
|
||||
self.primitives.append(Region(self.current_region, self.level_polarity))
|
||||
# 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':
|
||||
|
|
@ -380,6 +554,8 @@ class GerberParser(object):
|
|||
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)
|
||||
|
||||
|
|
@ -391,34 +567,131 @@ class GerberParser(object):
|
|||
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')
|
||||
self.direction = ('clockwise' if stmt.function in
|
||||
('G02', 'G2') else 'counterclockwise')
|
||||
|
||||
if stmt.op == "D01":
|
||||
if self.region_mode == 'on':
|
||||
if self.current_region is None:
|
||||
self.current_region = [(self.x, self.y), ]
|
||||
self.current_region.append((x, y,))
|
||||
else:
|
||||
start = (self.x, self.y)
|
||||
end = (x, y)
|
||||
width = self.apertures[self.aperture].stroke_width
|
||||
if self.interpolation == 'linear':
|
||||
self.primitives.append(Line(start, end, width, self.level_polarity))
|
||||
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:
|
||||
center = (start[0] + stmt.i, start[1] + stmt.j)
|
||||
self.primitives.append(Arc(start, end, center, self.direction, width, self.level_polarity))
|
||||
# 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.
|
||||
|
||||
elif stmt.op == "D02":
|
||||
pass
|
||||
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))
|
||||
|
||||
elif stmt.op == "D03":
|
||||
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])
|
||||
# XXX: temporary fix because there are no primitives for Macros and Polygon
|
||||
|
||||
|
||||
if primitive is not None:
|
||||
primitive.position = (x, y)
|
||||
primitive.level_polarity = self.level_polarity
|
||||
self.primitives.append(primitive)
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
if self.quadrant_mode == 'single-quadrant':
|
||||
|
||||
# The Gerber spec says single quadrant only has one possible center, and you can detect
|
||||
# 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). Find the center that makes the most sense
|
||||
sqdist_diff_min = sys.maxint
|
||||
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])
|
||||
|
||||
sqdist_start = sq_distance(start, test_center)
|
||||
sqdist_end = sq_distance(end, test_center)
|
||||
|
||||
if abs(sqdist_start - sqdist_end) < sqdist_diff_min:
|
||||
center = test_center
|
||||
sqdist_diff_min = abs(sqdist_start - sqdist_end)
|
||||
|
||||
return center
|
||||
else:
|
||||
return (start[0] + offsets[0], start[1] + offsets[1])
|
||||
|
||||
def _evaluate_aperture(self, stmt):
|
||||
self.aperture = stmt.d
|
||||
|
|
|
|||
BIN
gerber/tests/golden/example_am_exposure_modifier.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
gerber/tests/golden/example_coincident_hole.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
gerber/tests/golden/example_cutin_multiple.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gerber/tests/golden/example_flash_circle.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
gerber/tests/golden/example_flash_obround.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gerber/tests/golden/example_flash_polygon.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
gerber/tests/golden/example_flash_rectangle.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
gerber/tests/golden/example_fully_coincident.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
gerber/tests/golden/example_holes_dont_clear.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
gerber/tests/golden/example_not_overlapping_contour.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
gerber/tests/golden/example_not_overlapping_touching.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
gerber/tests/golden/example_overlapping_contour.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
gerber/tests/golden/example_overlapping_touching.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
gerber/tests/golden/example_simple_contour.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
gerber/tests/golden/example_single_contour.png
Normal file
|
After Width: | Height: | Size: 556 B |
BIN
gerber/tests/golden/example_single_contour_3.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
16
gerber/tests/golden/example_single_quadrant.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
%FSLAX23Y23*%
|
||||
%MOIN*%
|
||||
%ADD10C,0.01*%
|
||||
G74*
|
||||
D10*
|
||||
%LPD*%
|
||||
G01X1100Y600D02*
|
||||
G03X700Y1000I-400J0D01*
|
||||
G03X300Y600I0J-400D01*
|
||||
G03X700Y200I400J0D01*
|
||||
G03X1100Y600I0J400D01*
|
||||
G01X300D02*
|
||||
X1100D01*
|
||||
X700Y200D02*
|
||||
Y1000D01*
|
||||
M02*
|
||||
BIN
gerber/tests/golden/example_single_quadrant.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
16
gerber/tests/golden/example_two_square_boxes.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD10C,0.01*%
|
||||
D10*
|
||||
%LPD*%
|
||||
G01X0Y0D02*
|
||||
X500000D01*
|
||||
Y500000D01*
|
||||
X0D01*
|
||||
Y0D01*
|
||||
X600000D02*
|
||||
X1100000D01*
|
||||
Y500000D01*
|
||||
X600000D01*
|
||||
Y0D01*
|
||||
M02*
|
||||
BIN
gerber/tests/golden/example_two_square_boxes.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
16
gerber/tests/resources/example_am_exposure_modifier.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
G04 Umaco example for exposure modifier and clearing area*
|
||||
%FSLAX26Y26*%
|
||||
%MOIN*%
|
||||
%AMSQUAREWITHHOLE*
|
||||
21,0.1,1,1,0,0,0*
|
||||
1,0,0.5,0,0*%
|
||||
%ADD10SQUAREWITHHOLE*%
|
||||
%ADD11C,1*%
|
||||
G01*
|
||||
%LPD*%
|
||||
D11*
|
||||
X-1000000Y-250000D02*
|
||||
X1000000Y250000D01*
|
||||
D10*
|
||||
X0Y0D03*
|
||||
M02*
|
||||
24
gerber/tests/resources/example_coincident_hole.gbr
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
G04 ex2: overlapping*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%SRX1Y1I0.000J0.000*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
G04 first fully coincident linear segment*
|
||||
X10000D01*
|
||||
X50000Y10000D01*
|
||||
X90000Y50000D01*
|
||||
X50000Y90000D01*
|
||||
X10000Y50000D01*
|
||||
G04 second fully coincident linear segment*
|
||||
X0D01*
|
||||
G37*
|
||||
M02*
|
||||
18
gerber/tests/resources/example_cutin.gbr
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
G04 Umaco uut-in example*
|
||||
%FSLAX24Y24*%
|
||||
G75*
|
||||
G36*
|
||||
X20000Y100000D02*
|
||||
G01*
|
||||
X120000D01*
|
||||
Y20000D01*
|
||||
X20000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
G03*
|
||||
X50000Y60000I30000J0D01*
|
||||
G01*
|
||||
X20000D01*
|
||||
Y100000D01*
|
||||
G37*
|
||||
M02*
|
||||
28
gerber/tests/resources/example_cutin_multiple.gbr
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
G04 multiple cutins*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%SRX1Y1I0.000J0.000*%
|
||||
%ADD10C,1.00000*%
|
||||
%LPD*%
|
||||
G36*
|
||||
X1220000Y2570000D02*
|
||||
G01*
|
||||
Y2720000D01*
|
||||
X1310000D01*
|
||||
Y2570000D01*
|
||||
X1250000D01*
|
||||
Y2600000D01*
|
||||
X1290000D01*
|
||||
Y2640000D01*
|
||||
X1250000D01*
|
||||
Y2670000D01*
|
||||
X1290000D01*
|
||||
Y2700000D01*
|
||||
X1250000D01*
|
||||
Y2670000D01*
|
||||
Y2640000D01*
|
||||
Y2600000D01*
|
||||
Y2570000D01*
|
||||
X1220000D01*
|
||||
G37*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_circle.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of circular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,0.5*%
|
||||
%ADD11C,0.5X0.25*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_obround.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of rectangular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10O,0.46X0.26*%
|
||||
%ADD11O,0.46X0.26X0.19*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_polygon.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of rectangular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10P,.40X6*%
|
||||
%ADD11P,.40X6X0.0X0.19*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_rectangle.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of rectangular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10R,0.44X0.25*%
|
||||
%ADD11R,0.44X0.25X0.19*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
23
gerber/tests/resources/example_fully_coincident.gbr
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
G04 ex1: non overlapping*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
G04 first fully coincident linear segment*
|
||||
X-10000D01*
|
||||
X-50000Y10000D01*
|
||||
X-90000Y50000D01*
|
||||
X-50000Y90000D01*
|
||||
X-10000Y50000D01*
|
||||
G04 second fully coincident linear segment*
|
||||
X0D01*
|
||||
G37*
|
||||
M02*
|
||||
13
gerber/tests/resources/example_holes_dont_clear.gbr
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
G04 Demonstrates that apertures with holes do not clear the area - only the aperture hole*
|
||||
%FSLAX26Y26*%
|
||||
%MOIN*%
|
||||
%ADD10C,1X0.5*%
|
||||
%ADD11C,0.1*%
|
||||
G01*
|
||||
%LPD*%
|
||||
D11*
|
||||
X-1000000Y-250000D02*
|
||||
X1000000Y250000D01*
|
||||
D10*
|
||||
X0Y0D03*
|
||||
M02*
|
||||
39
gerber/tests/resources/example_level_holes.gbr
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
G04 This file illustrates how to use levels to create holes*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
G01*
|
||||
G04 First level: big square - dark polarity*
|
||||
%LPD*%
|
||||
G36*
|
||||
X250000Y250000D02*
|
||||
X1750000D01*
|
||||
Y1750000D01*
|
||||
X250000D01*
|
||||
Y250000D01*
|
||||
G37*
|
||||
G04 Second level: big circle - clear polarity*
|
||||
%LPC*%
|
||||
G36*
|
||||
G75*
|
||||
X500000Y1000000D02*
|
||||
G03*
|
||||
X500000Y1000000I500000J0D01*
|
||||
G37*
|
||||
G04 Third level: small square - dark polarity*
|
||||
%LPD*%
|
||||
G36*
|
||||
X750000Y750000D02*
|
||||
X1250000D01*
|
||||
Y1250000D01*
|
||||
X750000D01*
|
||||
Y750000D01*
|
||||
G37*
|
||||
G04 Fourth level: small circle - clear polarity*
|
||||
%LPC*%
|
||||
G36*
|
||||
G75*
|
||||
X1150000Y1000000D02*
|
||||
G03*
|
||||
X1150000Y1000000I250000J0D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_not_overlapping_contour.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Non-overlapping contours*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
X-10000D02*
|
||||
X-50000Y10000D01*
|
||||
X-90000Y50000D01*
|
||||
X-50000Y90000D01*
|
||||
X-10000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_not_overlapping_touching.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Non-overlapping and touching*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
D02*
|
||||
X-50000Y10000D01*
|
||||
X-90000Y50000D01*
|
||||
X-50000Y90000D01*
|
||||
X0Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_overlapping_contour.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Overlapping contours*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
X10000D02*
|
||||
X50000Y10000D01*
|
||||
X90000Y50000D01*
|
||||
X50000Y90000D01*
|
||||
X10000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_overlapping_touching.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Overlapping and touching*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
D02*
|
||||
X50000Y10000D01*
|
||||
X90000Y50000D01*
|
||||
X50000Y90000D01*
|
||||
X0Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
16
gerber/tests/resources/example_simple_contour.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
G04 Ucamco ex. 4.6.4: Simple contour*
|
||||
%FSLAX25Y25*%
|
||||
%MOIN*%
|
||||
%ADD10C,0.010*%
|
||||
G36*
|
||||
X200000Y300000D02*
|
||||
G01*
|
||||
X700000D01*
|
||||
Y100000D01*
|
||||
X1100000Y500000D01*
|
||||
X700000Y900000D01*
|
||||
Y700000D01*
|
||||
X200000D01*
|
||||
Y300000D01*
|
||||
G37*
|
||||
M02*
|
||||
15
gerber/tests/resources/example_single_contour_1.gbr
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
G04 Ucamco ex. 4.6.5: Single contour #1*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD11C,0.01*%
|
||||
G01*
|
||||
D11*
|
||||
X3000Y5000D01*
|
||||
G36*
|
||||
X50000Y50000D02*
|
||||
X60000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
Y50000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
15
gerber/tests/resources/example_single_contour_2.gbr
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
G04 Ucamco ex. 4.6.5: Single contour #2*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD11C,0.01*%
|
||||
G01*
|
||||
D11*
|
||||
X3000Y5000D01*
|
||||
X50000Y50000D02*
|
||||
G36*
|
||||
X60000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
Y50000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
15
gerber/tests/resources/example_single_contour_3.gbr
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
G04 Ucamco ex. 4.6.5: Single contour #2*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD11C,0.01*%
|
||||
G01*
|
||||
D11*
|
||||
X3000Y5000D01*
|
||||
X50000Y50000D01*
|
||||
G36*
|
||||
X60000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
Y50000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
18
gerber/tests/resources/example_single_quadrant.gbr
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
G04 Ucamco ex. 4.5.8: Single quadrant*
|
||||
%FSLAX23Y23*%
|
||||
%MOIN*%
|
||||
%ADD10C,0.010*%
|
||||
G74*
|
||||
D10*
|
||||
X1100Y600D02*
|
||||
G03*
|
||||
X700Y1000I400J0D01*
|
||||
X300Y600I0J400D01*
|
||||
X700Y200I400J0D01*
|
||||
X1100Y600I0J400D01*
|
||||
X300D02*
|
||||
G01*
|
||||
X1100D01*
|
||||
X700Y200D02*
|
||||
Y1000D01*
|
||||
M02*
|
||||