Merge pull request #3 from garretfick/merge-curtacircuitos

Merge curtacircuitos
This commit is contained in:
Hamilton Kibbe 2016-11-05 21:11:09 -04:00 committed by GitHub
commit d2fe444166
122 changed files with 20471 additions and 4648 deletions

6
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,21 +1,22 @@
gerber-tools
pcb-tools
============
![Travis CI Build Status](https://travis-ci.org/hamiltonkibbe/gerber-tools.svg?branch=master)
[![Coverage Status](https://coveralls.io/repos/hamiltonkibbe/gerber-tools/badge.png?branch=master)](https://coveralls.io/r/hamiltonkibbe/gerber-tools?branch=master)
[![Travis CI Build Status](https://travis-ci.org/curtacircuitos/pcb-tools.svg?branch=master)](https://travis-ci.org/curtacircuitos/pcb-tools)
[![Coverage Status](https://coveralls.io/repos/curtacircuitos/pcb-tools/badge.png?branch=master)](https://coveralls.io/r/curtacircuitos/pcb-tools?branch=master)
[![Documentation Status](https://readthedocs.org/projects/pcb-tools/badge/?version=latest)](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
![Composite Top Image](examples/composite_top.png)
![Composite Top Image](examples/cairo_example.png)
###Bottom Composite rendering
![Composite Bottom Image](examples/composite_bottom.png)
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
View file

@ -0,0 +1,4 @@
# Doc requirements
Sphinx==1.2.3
numpydoc==0.5

40
doc/source/about.rst Normal file
View 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

View file

@ -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'),
]

View file

@ -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>

View 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

View file

@ -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
View 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
============ ======== =========== ================ ====== ======= =======

View file

@ -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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

69
examples/cairo_example.py Normal file
View 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'))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 569 KiB

View 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()))

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because it is too large Load diff

View 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

File diff suppressed because it is too large Load diff

View 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

File diff suppressed because it is too large Load diff

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

40
examples/pcb_example.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -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

View file

@ -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
View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -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')

View file

@ -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

View 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
View 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

View file

@ -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
View 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
View 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

File diff suppressed because it is too large Load diff

461
gerber/ipc356.py Normal file
View 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

View file

@ -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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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.

View 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

View file

@ -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

View 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

View file

@ -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
View 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)),
}

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View 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*

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,16 @@
%FSLAX25Y25*%
%MOMM*%
%ADD10C,0.01*%
D10*
%LPD*%
G01X0Y0D02*
X500000D01*
Y500000D01*
X0D01*
Y0D01*
X600000D02*
X1100000D01*
Y500000D01*
X600000D01*
Y0D01*
M02*

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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*

View 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*

View 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*

View 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*

View file

@ -0,0 +1,10 @@
G04 Flashes of circular apertures*
%FSLAX24Y24*%
%MOMM*%
%ADD10C,0.5*%
%ADD11C,0.5X0.25*%
D10*
X000000Y000000D03*
D11*
X010000D03*
M02*

View 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*

View file

@ -0,0 +1,10 @@
G04 Flashes of rectangular apertures*
%FSLAX24Y24*%
%MOMM*%
%ADD10P,.40X6*%
%ADD11P,.40X6X0.0X0.19*%
D10*
X000000Y000000D03*
D11*
X010000D03*
M02*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

View 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*

Some files were not shown because too many files have changed in this diff Show more