add excellon file write
This commit is contained in:
parent
08253b40f6
commit
22a6f87e94
3 changed files with 239 additions and 223 deletions
|
|
@ -74,6 +74,11 @@ class ExcellonFile(CncFile):
|
|||
ctx.drill(pos[0], pos[1], tool.diameter)
|
||||
ctx.dump(filename)
|
||||
|
||||
def write(self, filename):
|
||||
with open(filename, 'w') as f:
|
||||
for statement in self.statements:
|
||||
f.write(statement.to_excellon() + '\n')
|
||||
|
||||
|
||||
class ExcellonParser(object):
|
||||
""" Excellon File Parser
|
||||
|
|
@ -155,9 +160,9 @@ class ExcellonParser(object):
|
|||
|
||||
elif line[0] == 'T' and self.state != 'HEADER':
|
||||
stmt = ToolSelectionStmt.from_excellon(line)
|
||||
self.active_tool self.tools[stmt.tool]
|
||||
self.active_tool = self.tools[stmt.tool]
|
||||
#self.active_tool = self.tools[int(line.strip().split('T')[1])]
|
||||
self.statements.append(statement)
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[0] in ['X', 'Y']:
|
||||
stmt = CoordinateStmt.from_excellon(line, fmt, zs)
|
||||
|
|
@ -197,18 +202,8 @@ class ExcellonParser(object):
|
|||
return FileSettings(units=self.units, format=self.format,
|
||||
zero_suppression=self.zero_suppression,
|
||||
notation=self.notation)
|
||||
|
||||
|
||||
def pairwise(iterator):
|
||||
""" Iterate over list taking two elements at a time.
|
||||
|
||||
e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)]
|
||||
"""
|
||||
itr = iter(iterator)
|
||||
while True:
|
||||
yield tuple([itr.next() for i in range(2)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = parser()
|
||||
p.parse('examples/ncdrill.txt')
|
||||
p = ExcellonParser()
|
||||
parsed = p.parse('examples/ncdrill.txt')
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .utils import write_gerber_value
|
||||
|
||||
from .utils import parse_gerber_value, write_gerber_value, decimal_string
|
||||
import re
|
||||
|
||||
__all__ = ['ExcellonTool', 'ToolSelectionStatment', 'CoordinateStmt',
|
||||
__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
|
||||
'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt',
|
||||
'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt',
|
||||
'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt',
|
||||
|
|
@ -138,20 +139,20 @@ class ExcellonTool(ExcellonStatement):
|
|||
fmt = self.settings['format']
|
||||
zs = self.settings['zero_suppression']
|
||||
stmt = 'T%d' % self.number
|
||||
if self.retract_rate:
|
||||
if self.retract_rate is not None:
|
||||
stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs)
|
||||
if self.diameter:
|
||||
stmt += 'C%s' % write_gerber_value(self.diameter, fmt, zs)
|
||||
if self.feed_rate:
|
||||
if self.feed_rate is not None:
|
||||
stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs)
|
||||
if self.max_hit_count:
|
||||
if self.max_hit_count is not None:
|
||||
stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs)
|
||||
if self.rpm:
|
||||
if self.rpm is not None:
|
||||
if self.rpm < 100000.:
|
||||
stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs)
|
||||
else:
|
||||
stmt += 'S%g' % self.rpm / 1000.
|
||||
if self.depth_offset:
|
||||
if self.diameter is not None:
|
||||
stmt += 'C%s' % decimal_string(self.diameter, 5, True)
|
||||
if self.depth_offset is not None:
|
||||
stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs)
|
||||
return stmt
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ class ExcellonTool(ExcellonStatement):
|
|||
return '<ExcellonTool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
|
||||
|
||||
|
||||
class ToolSelectionStatment(ExcellonStatement):
|
||||
class ToolSelectionStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
|
|
@ -189,6 +190,7 @@ class ToolSelectionStatment(ExcellonStatement):
|
|||
|
||||
class CoordinateStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, format=(2, 5), zero_suppression='trailing'):
|
||||
x = None
|
||||
y = None
|
||||
|
|
@ -208,22 +210,23 @@ class CoordinateStmt(ExcellonStatement):
|
|||
def to_excellon(self):
|
||||
stmt = ''
|
||||
if self.x is not None:
|
||||
stmt.append('X%s' % write_gerber_value(self.x))
|
||||
stmt += 'X%s' % write_gerber_value(self.x)
|
||||
if self.y is not None:
|
||||
stmt.append('Y%s' % write_gerber_value(self.y))
|
||||
stmt += 'Y%s' % write_gerber_value(self.y)
|
||||
return stmt
|
||||
|
||||
|
||||
class CommentStmt(ExcellonStatement):
|
||||
|
||||
def from_excellon(self, line):
|
||||
@classmethod
|
||||
def from_excellon(cls, line):
|
||||
return cls(line.strip().lstrip(';'))
|
||||
|
||||
def __init__(self, comment):
|
||||
self.comment = comment
|
||||
|
||||
def to_excellon(self):
|
||||
return ';%s' % comment
|
||||
return ';%s' % self.comment
|
||||
|
||||
|
||||
class HeaderBeginStmt(ExcellonStatement):
|
||||
|
|
@ -265,7 +268,7 @@ class EndOfProgramStmt(ExcellonStatement):
|
|||
stmt += 'X%s' % write_gerber_value(self.x)
|
||||
if self.y is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y)
|
||||
|
||||
return stmt
|
||||
|
||||
class UnitStmt(ExcellonStatement):
|
||||
|
||||
|
|
@ -281,8 +284,9 @@ class UnitStmt(ExcellonStatement):
|
|||
|
||||
def to_excellon(self):
|
||||
stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
|
||||
'LZ' if self.zero_suppression == 'trailing' else 'TZ')
|
||||
|
||||
'LZ' if self.zero_suppression == 'trailing'
|
||||
else 'TZ')
|
||||
return stmt
|
||||
|
||||
class IncrementalModeStmt(ExcellonStatement):
|
||||
|
||||
|
|
@ -292,7 +296,7 @@ class IncrementalModeStmt(ExcellonStatement):
|
|||
|
||||
def __init__(self, mode='off'):
|
||||
if mode.lower() not in ['on', 'off']:
|
||||
raise ValueError('Mode may be "on" or "off")
|
||||
raise ValueError('Mode may be "on" or "off"')
|
||||
self.mode = 'off'
|
||||
|
||||
def to_excellon(self):
|
||||
|
|
@ -309,7 +313,7 @@ class VersionStmt(ExcellonStatement):
|
|||
def __init__(self, version=1):
|
||||
version = int(version)
|
||||
if version not in [1, 2]:
|
||||
raise ValueError('Valid versions are 1 or 2'
|
||||
raise ValueError('Valid versions are 1 or 2')
|
||||
self.version = version
|
||||
|
||||
def to_excellon(self):
|
||||
|
|
@ -374,3 +378,15 @@ class UnknownStmt(ExcellonStatement):
|
|||
|
||||
def to_excellon(self):
|
||||
return self.stmt
|
||||
|
||||
|
||||
|
||||
|
||||
def pairwise(iterator):
|
||||
""" Iterate over list taking two elements at a time.
|
||||
|
||||
e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)]
|
||||
"""
|
||||
itr = iter(iterator)
|
||||
while True:
|
||||
yield tuple([itr.next() for i in range(2)])
|
||||
385
gerber/utils.py
385
gerber/utils.py
|
|
@ -1,190 +1,195 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
gerber.utils
|
||||
============
|
||||
**Gerber and Excellon file handling utilities**
|
||||
|
||||
This module provides utility functions for working with Gerber and Excellon
|
||||
files.
|
||||
"""
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# License:
|
||||
|
||||
|
||||
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
|
||||
""" Convert gerber/excellon formatted string to floating-point number
|
||||
|
||||
.. note::
|
||||
Format and zero suppression are configurable. Note that the Excellon
|
||||
and Gerber formats use opposite terminology with respect to leading
|
||||
and trailing zeros. The Gerber format specifies which zeros are
|
||||
suppressed, while the Excellon format specifies which zeros are
|
||||
included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use
|
||||
`zero_suppression='trailing'`
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def detect_file_format(filename):
|
||||
""" Determine format of a file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of the file to read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
format : string
|
||||
File format. either 'excellon' or 'rs274x'
|
||||
"""
|
||||
|
||||
# Read the first 20 lines
|
||||
with open(filename, 'r') as f:
|
||||
lines = [next(f) for x in xrange(20)]
|
||||
|
||||
# Look for
|
||||
for line in lines:
|
||||
if 'M48' in line:
|
||||
return 'excellon'
|
||||
elif '%FS' in line:
|
||||
return'rs274x'
|
||||
return 'unknown'
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
gerber.utils
|
||||
============
|
||||
**Gerber and Excellon file handling utilities**
|
||||
|
||||
This module provides utility functions for working with Gerber and Excellon
|
||||
files.
|
||||
"""
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# License:
|
||||
|
||||
|
||||
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
|
||||
""" Convert gerber/excellon formatted string to floating-point number
|
||||
|
||||
.. note::
|
||||
Format and zero suppression are configurable. Note that the Excellon
|
||||
and Gerber formats use opposite terminology with respect to leading
|
||||
and trailing zeros. The Gerber format specifies which zeros are
|
||||
suppressed, while the Excellon format specifies which zeros are
|
||||
included. This function uses the Gerber-file convention, so an
|
||||
Excellon file in LZ (leading zeros) mode would use
|
||||
`zero_suppression='trailing'`
|
||||
|
||||
|
||||
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')
|
||||
|
||||
# Edge case...
|
||||
if value == 0:
|
||||
return '00'
|
||||
|
||||
# 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, padding=False):
|
||||
""" 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.10g' % 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]
|
||||
elif padding:
|
||||
decimal = decimal + (precision - len(decimal)) * '0'
|
||||
if integer or decimal:
|
||||
return ''.join([integer, '.', decimal])
|
||||
else:
|
||||
return int(floatstr)
|
||||
|
||||
|
||||
def detect_file_format(filename):
|
||||
""" Determine format of a file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
Filename of the file to read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
format : string
|
||||
File format. either 'excellon' or 'rs274x'
|
||||
"""
|
||||
|
||||
# Read the first 20 lines
|
||||
with open(filename, 'r') as f:
|
||||
lines = [next(f) for x in xrange(20)]
|
||||
|
||||
# Look for
|
||||
for line in lines:
|
||||
if 'M48' in line:
|
||||
return 'excellon'
|
||||
elif '%FS' in line:
|
||||
return'rs274x'
|
||||
return 'unknown'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue