Add keys to statements for linking to primitives. Add some API features to ExcellonFile, such as getting a tool path length and changing tool parameters. Excellonfiles write method generates statements based on the drill hits in the hits member, so drill hits in a generated file can be re-ordered by re-ordering the drill hits in ExcellonFile.hits. see #30

This commit is contained in:
Hamilton Kibbe 2015-06-11 11:20:56 -04:00
parent 1a70064e10
commit 94f3976915
5 changed files with 185 additions and 80 deletions

View file

@ -220,7 +220,8 @@ class CamFile(object):
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

View file

@ -24,6 +24,7 @@ This module provides Excellon file classes and parsing utilities
"""
import math
import operator
from .excellon_statements import *
from .cam import CamFile, FileSettings
@ -49,6 +50,22 @@ def read(filename):
return ExcellonParser(settings).parse(filename)
class DrillHit(object):
def __init__(self, tool, position):
self.tool = tool
self.position = position
def to_inch(self):
if self.tool.units == 'metric':
self.tool.to_inch()
self.position = tuple(map(inch, self.position))
def to_metric(self):
if self.tool.units == 'inch':
self.tool.to_metric()
self.position = tuple(map(metric, self.position))
class ExcellonFile(CamFile):
""" A class representing a single excellon file
@ -81,17 +98,19 @@ class ExcellonFile(CamFile):
filename=filename)
self.tools = tools
self.hits = hits
self.primitives = [Drill(position, tool.diameter, units=settings.units)
for tool, position in self.hits]
@property
def primitives(self):
return [Drill(hit.position, hit.tool.diameter,units=self.settings.units) for hit in self.hits]
@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]
for hit in self.hits:
radius = hit.tool.diameter / 2.
x, y = hit.position
xmin = min(x - radius, xmin)
xmax = max(x + radius, xmax)
ymin = min(y - radius, ymin)
@ -101,20 +120,23 @@ class ExcellonFile(CamFile):
def report(self, filename=None):
""" Print or save drill report
"""
toolfmt = ' T%%02d %%%d.%df %%d\n' % self.settings.format
rprt = 'Excellon Drill Report\n\n'
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 += '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'
rprt += ' Code Size Hits\n'
rprt += ' --------------------------\n'
rprt += '\nTool List:\n----------\n\n'
rprt += ' Code Size Hits Path Length\n'
rprt += ' --------------------------------------\n'
for tool in self.tools.itervalues():
rprt += toolfmt % (tool.number, tool.diameter, tool.hit_count)
rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.tool_path_length(tool.number))
if filename is not None:
with open(filename, 'w') as f:
f.write(rprt)
@ -122,9 +144,22 @@ class ExcellonFile(CamFile):
def write(self, filename):
with open(filename, 'w') as f:
# Copy the header verbatim
for statement in self.statements:
f.write(statement.to_excellon(self.settings) + '\n')
print(statement)
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 self.tools.itervalues():
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
@ -137,8 +172,8 @@ class ExcellonFile(CamFile):
tool.to_inch()
for primitive in self.primitives:
primitive.to_inch()
self.hits = [(tool, tuple(map(inch, pos)))
for tool, pos in self.hits]
for hit in self.hits:
hit.position = tuple(map(inch, hit,position))
def to_metric(self):
@ -152,17 +187,52 @@ class ExcellonFile(CamFile):
tool.to_metric()
for primitive in self.primitives:
primitive.to_metric()
self.hits = [(tool, tuple(map(metric, pos)))
for tool, pos in self.hits]
for hit in self.hits:
hit.position = tuple(map(metric, hit.position))
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)
self.hits = [(tool, (pos[0] + x_offset, pos[1] + y_offset))
for tool, pos in self.hits]
for hit in self. hits:
hit.position = tuple(map(operator.add, hit.position, (x_offset, y_offset)))
def tool_path_length(self, tool_number):
""" Return the path length for a given tool
"""
length = 0.0
pos = (0, 0)
for hit in self.hits:
tool = hit.tool
if tool.number == tool_number:
length = length + math.hypot(*tuple(map(operator.sub, pos, hit.position)))
pos = hit.position
return length
def update_tool(self, tool_number, **kwargs):
""" Change parameters of a tool
"""
if kwargs.get('feed_rate') is not None:
self.tools[tool_number].feed_rate = kwargs.get('feed_rate')
if kwargs.get('retract_rate') is not None:
self.tools[tool_number].retract_rate = kwargs.get('retract_rate')
if kwargs.get('rpm') is not None:
self.tools[tool_number].rpm = kwargs.get('rpm')
if kwargs.get('diameter') is not None:
self.tools[tool_number].diameter = kwargs.get('diameter')
if kwargs.get('max_hit_count') is not None:
self.tools[tool_number].max_hit_count = kwargs.get('max_hit_count')
if kwargs.get('depth_offset') is not None:
self.tools[tool_number].depth_offset = kwargs.get('depth_offset')
# Update drill hits
newtool = self.tools[tool_number]
for hit in self.hits:
if hit.tool.number == newtool.number:
hit.tool = newtool
class ExcellonParser(object):
""" Excellon File Parser
@ -248,6 +318,8 @@ class ExcellonParser(object):
self.statements.append(RewindStopStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
elif self.state == 'INIT':
self.state = 'HEADER'
elif line[:3] == 'M95':
self.statements.append(HeaderEndStmt())
@ -312,7 +384,7 @@ class ExcellonParser(object):
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((self.active_tool, tuple(self.pos)))
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
elif line[0] in ['X', 'Y']:
@ -331,7 +403,7 @@ class ExcellonParser(object):
if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
self.hits.append((self.active_tool, tuple(self.pos)))
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
else:
self.statements.append(UnknownStmt.from_excellon(line))
@ -402,7 +474,7 @@ def detect_excellon_format(filename):
size = tuple([t[1] - t[0] 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:

View file

@ -22,7 +22,7 @@ Excellon Statements
"""
import re
import uuid
from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
inch, metric)
@ -40,13 +40,15 @@ class ExcellonStatement(object):
""" Excellon Statement abstract base class
"""
units = 'inch'
@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')
@ -107,7 +109,7 @@ class ExcellonTool(ExcellonStatement):
"""
@classmethod
def from_excellon(cls, line, settings):
def from_excellon(cls, line, settings, id=None):
""" Create a Tool from an excellon file tool definition line.
Parameters
@ -126,6 +128,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:
@ -165,6 +168,8 @@ class ExcellonTool(ExcellonStatement):
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')
@ -221,7 +226,7 @@ class ExcellonTool(ExcellonStatement):
class ToolSelectionStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
""" Create a ToolSelectionStmt from an excellon file line.
Parameters
@ -244,9 +249,10 @@ class ToolSelectionStmt(ExcellonStatement):
tool = int(line[:2])
compensation_index = int(line[2:])
return cls(tool, compensation_index)
return cls(tool, compensation_index, **kwargs)
def __init__(self, tool, compensation_index=None):
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)
@ -263,7 +269,7 @@ class ToolSelectionStmt(ExcellonStatement):
class CoordinateStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings):
def from_excellon(cls, line, settings, **kwargs):
x_coord = None
y_coord = None
if line[0] == 'X':
@ -276,11 +282,12 @@ class CoordinateStmt(ExcellonStatement):
else:
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
settings.zero_suppression)
c = cls(x_coord, y_coord)
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
@ -329,7 +336,7 @@ class CoordinateStmt(ExcellonStatement):
class RepeatHoleStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings):
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()
@ -340,11 +347,12 @@ class RepeatHoleStmt(ExcellonStatement):
ydelta = (parse_gerber_value(stmt['ydelta'], settings.format,
settings.zero_suppression)
if stmt['ydelta'] is not '' else None)
c = cls(count, xdelta, ydelta)
c = cls(count, xdelta, ydelta, **kwargs)
c.units = settings.units
return c
def __init__(self, count, xdelta=0.0, ydelta=0.0):
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
@ -385,10 +393,11 @@ class RepeatHoleStmt(ExcellonStatement):
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, settings=None):
@ -397,8 +406,8 @@ class CommentStmt(ExcellonStatement):
class HeaderBeginStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(HeaderBeginStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M48'
@ -406,8 +415,8 @@ class HeaderBeginStmt(ExcellonStatement):
class HeaderEndStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(HeaderEndStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M95'
@ -415,8 +424,8 @@ class HeaderEndStmt(ExcellonStatement):
class RewindStopStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(RewindStopStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return '%'
@ -425,7 +434,7 @@ class RewindStopStmt(ExcellonStatement):
class EndOfProgramStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings):
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()
@ -435,11 +444,12 @@ class EndOfProgramStmt(ExcellonStatement):
y = (parse_gerber_value(stmt['y'], settings.format,
settings.zero_suppression)
if stmt['y'] is not '' else None)
c = cls(x, y)
c = cls(x, y, **kwargs)
c.units = settings.units
return c
def __init__(self, x=None, y=None):
def __init__(self, x=None, y=None, **kwargs):
super(EndOfProgramStmt, self).__init__(**kwargs)
self.x = x
self.y = y
@ -476,12 +486,13 @@ class EndOfProgramStmt(ExcellonStatement):
class UnitStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
units = 'inch' if 'INCH' in line else 'metric'
zeros = 'leading' if 'LZ' in line else 'trailing'
return cls(units, zeros)
return cls(units, zeros, **kwargs)
def __init__(self, units='inch', zeros='leading'):
def __init__(self, units='inch', zeros='leading', **kwargs):
super(UnitStmt, self).__init__(**kwargs)
self.units = units.lower()
self.zeros = zeros
@ -500,10 +511,11 @@ class UnitStmt(ExcellonStatement):
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
@ -515,11 +527,12 @@ class IncrementalModeStmt(ExcellonStatement):
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')
@ -532,11 +545,12 @@ class VersionStmt(ExcellonStatement):
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')
@ -549,11 +563,12 @@ class FormatStmt(ExcellonStatement):
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, settings=None):
@ -563,12 +578,13 @@ class LinkToolStmt(ExcellonStatement):
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"')
@ -585,8 +601,8 @@ class MeasuringModeStmt(ExcellonStatement):
class RouteModeStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(RouteModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G00'
@ -594,8 +610,8 @@ class RouteModeStmt(ExcellonStatement):
class DrillModeStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(DrillModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G05'
@ -603,8 +619,8 @@ class DrillModeStmt(ExcellonStatement):
class AbsoluteModeStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(AbsoluteModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G90'
@ -613,10 +629,11 @@ class AbsoluteModeStmt(ExcellonStatement):
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, settings=None):

View file

@ -36,11 +36,13 @@ class Primitive(object):
Rotation of a primitive about its origin in degrees. Positive rotation
is counter-clockwise as viewed from the board top.
"""
def __init__(self, level_polarity='dark', rotation=0, units=None):
def __init__(self, level_polarity='dark', rotation=0, units=None, id=None, statement_id=None):
self.level_polarity = level_polarity
self.rotation = rotation
self.units = units
self._to_convert = list()
self.id = id
self.statement_id = statement_id
def bounding_box(self):
""" Calculate bounding box

View file

@ -24,6 +24,17 @@ def test_read():
ncdrill = read(NCDRILL_FILE)
assert(isinstance(ncdrill, ExcellonFile))
def test_write():
ncdrill = read(NCDRILL_FILE)
ncdrill.write('test.ncd')
with open(NCDRILL_FILE) as src:
srclines = src.readlines()
with open('test.ncd') as res:
for idx, line in enumerate(res):
assert_equal(line.strip(), srclines[idx].strip())
os.remove('test.ncd')
def test_read_settings():
ncdrill = read(NCDRILL_FILE)
assert_equal(ncdrill.settings['format'], (2, 4))
@ -47,9 +58,11 @@ def test_conversion():
ncdrill.to_metric()
assert_equal(ncdrill.settings.units, 'metric')
inch_primitives = ncdrill_inch.primitives
for tool in iter(ncdrill_inch.tools.values()):
tool.to_metric()
for primitive in ncdrill_inch.primitives:
for primitive in inch_primitives:
primitive.to_metric()
for statement in ncdrill_inch.statements:
statement.to_metric()
@ -57,7 +70,7 @@ def test_conversion():
for m_tool, i_tool in zip(iter(ncdrill.tools.values()), iter(ncdrill_inch.tools.values())):
assert_equal(i_tool, m_tool)
for m, i in zip(ncdrill.primitives,ncdrill_inch.primitives):
for m, i in zip(ncdrill.primitives,inch_primitives):
assert_equal(m, i)