Merge pull request #3 from hamiltonkibbe/excellon_support

Add Excellon support
This commit is contained in:
Paulo Henrique Silva 2014-09-28 20:02:30 -03:00
commit 14e71e2c0c
6 changed files with 421 additions and 49 deletions

4
.gitignore vendored
View file

@ -37,3 +37,7 @@ nosetests.xml
.idea/workspace.xml
.idea/misc.xml
.idea
# OS Files
.DS_Store
Thumbs.db

180
gerber/excellon.py Executable file
View file

@ -0,0 +1,180 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from itertools import tee, izip
from .utils import parse_gerber_value
class Tool(object):
@classmethod
def from_line(cls, line, settings):
commands = re.split('([BCFHSTZ])', line)[1:]
commands = [(command, value) for command, value in pairwise(commands)]
args = {}
format = settings['format']
zero_suppression = settings['zero_suppression']
for cmd, val in commands:
if cmd == 'B':
args['retract_rate'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'C':
args['diameter'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'F':
args['feed_rate'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'H':
args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression)
elif cmd == 'S':
args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression)
elif cmd == 'T':
args['number'] = int(val)
elif cmd == 'Z':
args['depth_offset'] = parse_gerber_value(val, format, zero_suppression)
return cls(settings, **args)
def __init__(self, settings, **kwargs):
self.number = kwargs.get('number')
self.feed_rate = kwargs.get('feed_rate')
self.retract_rate = kwargs.get('retract_rate')
self.rpm = kwargs.get('rpm')
self.diameter = kwargs.get('diameter')
self.max_hit_count = kwargs.get('max_hit_count')
self.depth_offset = kwargs.get('depth_offset')
self.units = settings.get('units', 'inch')
def __repr__(self):
unit = 'in.' if self.units == 'inch' else 'mm'
return '<Tool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
class ExcellonParser(object):
def __init__(self, ctx=None):
self.ctx=ctx
self.notation = 'absolute'
self.units = 'inch'
self.zero_suppression = 'trailing'
self.format = (2,5)
self.state = 'INIT'
self.tools = {}
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
if ctx is not None:
zeros = 'L' if self.zero_suppression == 'leading' else 'T'
x = self.format
y = self.format
self.ctx.set_coord_format(zeros, x, y)
def parse(self, filename):
with open(filename, 'r') as f:
for line in f:
self._parse(line)
def dump(self, filename='teste.svg'):
if self.ctx is not None:
self.ctx.dump(filename)
def _parse(self, line):
if 'M48' in line:
self.state = 'HEADER'
if 'G00' in line:
self.state = 'ROUT'
if 'G05' in line:
self.state = 'DRILL'
elif line[0] == '%' and self.state == 'HEADER':
self.state = 'DRILL'
if 'INCH' in line or line.strip() == 'M72':
self.units = 'inch'
elif 'METRIC' in line or line.strip() == 'M71':
self.units = 'metric'
if 'LZ' in line:
self.zero_suppression = 'trailing'
elif 'TZ' in line:
self.zero_suppression = 'leading'
if 'ICI' in line and 'ON' in line or line.strip() == 'G91':
self.notation = 'incremental'
if 'ICI' in line and 'OFF' in line or line.strip() == 'G90':
self.notation = 'absolute'
zs = self._settings()['zero_suppression']
fmt = self._settings()['format']
# tool definition
if line[0] == 'T' and self.state == 'HEADER':
tool = Tool.from_line(line,self._settings())
self.tools[tool.number] = tool
elif line[0] == 'T' and self.state != 'HEADER':
self.active_tool = self.tools[int(line.strip().split('T')[1])]
if line[0] in ['X', 'Y']:
x = None
y = None
if line[0] == 'X':
splitline = line.strip('X').split('Y')
x = parse_gerber_value(splitline[0].strip(), fmt, zs)
if len(splitline) == 2:
y = parse_gerber_value(splitline[1].strip(), fmt,zs)
else:
y = parse_gerber_value(line.strip(' Y'), fmt,zs)
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, self.pos))
if self.ctx is not None:
self.ctx.drill(self.pos[0], self.pos[1],
self.active_tool.diameter)
def _settings(self):
return {'units':self.units, 'zero_suppression':self.zero_suppression,
'format': self.format}
def pairwise(iterator):
itr = iter(iterator)
while True:
yield tuple([itr.next() for i in range(2)])
if __name__ == '__main__':
from .render_svg import GerberSvgContext
tools = []
p = ExcellonParser(GerberSvgContext())
p.parse('examples/ncdrill.txt')
p.dump('excellon.svg')

View file

@ -242,7 +242,7 @@ class GerberParser(object):
for stmt in self._parse(data):
self.statements.append(stmt)
if self.ctx:
self._evaluate(stmt)
self.ctx.evaluate(stmt)
def dump_json(self):
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
@ -361,49 +361,3 @@ class GerberParser(object):
return (match.groupdict(), data[match.end(0):])
return ({}, None)
# really this all belongs in another class - the GerberContext class
def _evaluate(self, stmt):
if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)):
return
elif isinstance(stmt, ParamStmt):
self._evaluate_param(stmt)
elif isinstance(stmt, CoordStmt):
self._evaluate_coord(stmt)
elif isinstance(stmt, ApertureStmt):
self._evaluate_aperture(stmt)
else:
raise Exception("Invalid statement to evaluate")
def _evaluate_param(self, stmt):
if stmt.param == "FS":
self.ctx.set_coord_format(stmt.zero, stmt.x, stmt.y)
self.ctx.set_coord_notation(stmt.notation)
elif stmt.param == "MO:":
self.ctx.set_coord_unit(stmt.mo)
elif stmt.param == "IP:":
self.ctx.set_image_polarity(stmt.ip)
elif stmt.param == "LP:":
self.ctx.set_level_polarity(stmt.lp)
elif stmt.param == "AD":
self.ctx.define_aperture(stmt.d, stmt.shape, stmt.modifiers)
def _evaluate_coord(self, stmt):
if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"):
self.ctx.set_interpolation(stmt.function)
if stmt.op == "D01":
self.ctx.stroke(stmt.x, stmt.y)
elif stmt.op == "D02":
self.ctx.move(stmt.x, stmt.y)
elif stmt.op == "D03":
self.ctx.flash(stmt.x, stmt.y)
def _evaluate_aperture(self, stmt):
self.ctx.set_aperture(stmt.d)

View file

@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .parser import CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt
IMAGE_POLARITY_POSITIVE = 1
IMAGE_POLARITY_NEGATIVE = 2
@ -138,3 +139,50 @@ class GerberContext(object):
def flash(self, x, y):
pass
def drill(self, x, y, diameter):
pass
def evaluate(self, stmt):
if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)):
return
elif isinstance(stmt, ParamStmt):
self._evaluate_param(stmt)
elif isinstance(stmt, CoordStmt):
self._evaluate_coord(stmt)
elif isinstance(stmt, ApertureStmt):
self._evaluate_aperture(stmt)
else:
raise Exception("Invalid statement to evaluate")
def _evaluate_param(self, stmt):
if stmt.param == "FS":
self.set_coord_format(stmt.zero, stmt.x, stmt.y)
self.set_coord_notation(stmt.notation)
elif stmt.param == "MO:":
self.set_coord_unit(stmt.mo)
elif stmt.param == "IP:":
self.set_image_polarity(stmt.ip)
elif stmt.param == "LP:":
self.set_level_polarity(stmt.lp)
elif stmt.param == "AD":
self.define_aperture(stmt.d, stmt.shape, stmt.modifiers)
def _evaluate_coord(self, stmt):
if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"):
self.set_interpolation(stmt.function)
if stmt.op == "D01":
self.stroke(stmt.x, stmt.y)
elif stmt.op == "D02":
self.move(stmt.x, stmt.y)
elif stmt.op == "D03":
self.flash(stmt.x, stmt.y)
def _evaluate_aperture(self, stmt):
self.set_aperture(stmt.d)

View file

@ -44,6 +44,9 @@ class Rect(Shape):
stroke_width=2, stroke_linecap="butt")
def flash(self, ctx, x, y):
# Center the rectange on x,y
x -= (self.size[0] / 2.0)
y -= (self.size[0] / 2.0)
return ctx.dwg.rect(insert=(300*x, 300*y), size=(300*float(self.size[0]), 300*float(self.size[1])),
fill="rgb(184, 115, 51)")
@ -102,5 +105,10 @@ class GerberSvgContext(GerberContext):
self.move(x, y, resolve=False)
def dump(self):
self.dwg.saveas("teste.svg")
def drill(self, x, y, diameter):
hit = self.dwg.circle(center=(x*300, y*300), r=300*(diameter/2.0), fill="gray")
self.dwg.add(hit)
def dump(self,filename='teste.svg'):
self.dwg.saveas(filename)

178
gerber/utils.py Normal file
View file

@ -0,0 +1,178 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
gerber.utils
============
**Gerber and Excellon file handling utilities**
This module provides utility functions for working with Gerber and Excellon
files.
"""
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
""" Convert gerber/excellon formatted string to floating-point number
.. note::
Format and zero suppression are configurable. Note that the Excellon
and Gerber formats use opposite terminology with respect to leading
and trailing zeros. The Gerber format specifies which zeros are
suppressed, while the Excellon format specifies which zeros are
included. This function uses the Gerber-file convention, so an
Excellon file in LZ (leading zeros) mode would use
`zero_suppression='trailing'`
Parameters
----------
value : string
A Gerber/Excellon-formatted string representing a numerical value.
format : tuple (int,int)
Gerber/Excellon precision format expressed as a tuple containing:
(number of integer-part digits, number of decimal-part digits)
zero_suppression : string
Zero-suppression mode. May be 'leading' or 'trailing'
Returns
-------
value : float
The specified value as a floating-point number.
"""
# Format precision
integer_digits, decimal_digits = format
MAX_DIGITS = integer_digits + decimal_digits
# Absolute maximum number of digits supported. This will handle up to
# 6:7 format, which is somewhat supported, even though the gerber spec
# only allows up to 6:6
if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7:
raise ValueError('Parser only supports precision up to 6:7 format')
# Remove extraneous information
value = value.strip()
value = value.strip(' +')
negative = '-' in value
if negative:
value = value.strip(' -')
# Handle excellon edge case with explicit decimal. "That was easy!"
if '.' in value:
return float(value)
digits = [digit for digit in '0' * MAX_DIGITS]
offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value))
for i, digit in enumerate(value):
digits[i + offset] = digit
result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:]))
return -1.0 * result if negative else result
def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
""" Convert a floating point number to a Gerber/Excellon-formatted string.
.. note::
Format and zero suppression are configurable. Note that the Excellon
and Gerber formats use opposite terminology with respect to leading
and trailing zeros. The Gerber format specifies which zeros are
suppressed, while the Excellon format specifies which zeros are
included. This function uses the Gerber-file convention, so an
Excellon file in LZ (leading zeros) mode would use
`zero_suppression='trailing'`
Parameters
----------
value : float
A floating point value.
format : tuple (n=2)
Gerber/Excellon precision format expressed as a tuple containing:
(number of integer-part digits, number of decimal-part digits)
zero_suppression : string
Zero-suppression mode. May be 'leading' or 'trailing'
Returns
-------
value : string
The specified value as a Gerber/Excellon-formatted string.
"""
# Format precision
integer_digits, decimal_digits = format
MAX_DIGITS = integer_digits + decimal_digits
if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7:
raise ValueError('Parser only supports precision up to 6:7 format')
# negative sign affects padding, so deal with it at the end...
negative = value < 0.0
if negative:
value = -1.0 * value
# Format string for padding out in both directions
fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits)
digits = [val for val in fmtstring % value if val != '.']
# Suppression...
if zero_suppression == 'trailing':
while digits[-1] == '0':
digits.pop()
else:
while digits[0] == '0':
digits.pop(0)
return ''.join(digits) if not negative else ''.join(['-'] + digits)
def decimal_string(value, precision=6):
""" Convert float to string with limited precision
Parameters
----------
value : float
A floating point value.
precision :
Maximum number of decimal places to print
Returns
-------
value : string
The specified value as a string.
"""
floatstr = '%0.20g' % value
integer = None
decimal = None
if '.' in floatstr:
integer, decimal = floatstr.split('.')
elif ',' in floatstr:
integer, decimal = floatstr.split(',')
if len(decimal) > precision:
decimal = decimal[:precision]
if integer or decimal:
return ''.join([integer, '.', decimal])
else:
return int(floatstr)