Excellon update

This commit is contained in:
Hamilton Kibbe 2016-11-18 07:55:43 -05:00
parent ca2143380f
commit 41a7b90dff
4 changed files with 242 additions and 93 deletions

View file

@ -100,12 +100,12 @@ class DrillHit(object):
self.position = position
def to_inch(self):
if self.tool.units == 'metric':
if self.tool.settings.units == 'metric':
self.tool.to_inch()
self.position = tuple(map(inch, self.position))
def to_metric(self):
if self.tool.units == 'inch':
if self.tool.settings.units == 'inch':
self.tool.to_metric()
self.position = tuple(map(metric, self.position))
@ -120,7 +120,7 @@ class DrillHit(object):
max_y = position[1] + radius
return ((min_x, max_x), (min_y, max_y))
def offset(self, x_offset, y_offset):
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(operator.add, self.position, (x_offset, y_offset)))
def __str__(self):
@ -141,13 +141,13 @@ class DrillSlot(object):
self.slot_type = slot_type
def to_inch(self):
if self.tool.units == 'metric':
if self.tool.settings.units == 'metric':
self.tool.to_inch()
self.start = tuple(map(inch, self.start))
self.end = tuple(map(inch, self.end))
def to_metric(self):
if self.tool.units == 'inch':
if self.tool.settings.units == 'inch':
self.tool.to_metric()
self.start = tuple(map(metric, self.start))
self.end = tuple(map(metric, self.end))
@ -163,7 +163,7 @@ class DrillSlot(object):
max_y = max(start[1], end[1]) + radius
return ((min_x, max_x), (min_y, max_y))
def offset(self, x_offset, y_offset):
def offset(self, x_offset=0, y_offset=0):
self.start = tuple(map(operator.add, self.start, (x_offset, y_offset)))
self.end = tuple(map(operator.add, self.end, (x_offset, y_offset)))
@ -183,6 +183,7 @@ class ExcellonFile(CamFile):
hits : list of tuples
list of drill hits as (<Tool>, (x, y))
settings : dict
Dictionary of gerber file settings
@ -211,16 +212,17 @@ class ExcellonFile(CamFile):
primitives = []
for hit in self.hits:
if isinstance(hit, DrillHit):
primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units))
primitives.append(Drill(hit.position, hit.tool.diameter,
units=self.settings.units))
elif isinstance(hit, DrillSlot):
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter,
units=self.settings.units))
else:
raise ValueError('Unknown hit type')
return primitives
@property
def bounds(self):
def bounding_box(self):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
for hit in self.hits:
@ -282,29 +284,31 @@ class ExcellonFile(CamFile):
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()
#for primitive in self.primitives:
# primitive.to_inch()
#for hit in self.hits:
# hit.to_inch()
self.units = '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 primitive in self.primitives:
# print("Converting to metric: {}".format(primitive))
# primitive.to_metric()
# print(primitive)
for hit in self.hits:
hit.to_metric()
self.units = 'metric'
def offset(self, x_offset=0, y_offset=0):
for statement in self.statements:
@ -663,7 +667,8 @@ class ExcellonParser(object):
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
# 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
@ -835,7 +840,7 @@ def detect_excellon_format(data=None, filename=None):
try:
p = ExcellonParser(settings)
ef = p.parse_raw(data)
size = tuple([t[0] - t[1] for t in ef.bounds])
size = tuple([t[0] - t[1] for t in ef.bounding_box])
hole_area = 0.0
for hit in p.hits:
tool = hit.tool

View file

@ -113,16 +113,16 @@ 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
@ -131,7 +131,7 @@ class ExcellonTool(ExcellonStatement):
args['plated'] = tool.plated
args['retract_rate'] = tool.retract_rate
args['rpm'] = tool.rpm
return cls(None, **args)
@classmethod
@ -172,9 +172,9 @@ 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
# Sometimees we can can parse the plating status
args['plated'] = plated
return cls(settings, **args)
@ -209,7 +209,7 @@ class ExcellonTool(ExcellonStatement):
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, settings=None):
@ -249,15 +249,15 @@ class ExcellonTool(ExcellonStatement):
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
@ -314,12 +314,12 @@ class ToolSelectionStmt(ExcellonStatement):
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.
@ -329,10 +329,10 @@ class NextToolSelectionStmt(ExcellonStatement):
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
@ -651,11 +651,11 @@ class EndOfProgramStmt(ExcellonStatement):
class UnitStmt(ExcellonStatement):
@classmethod
def from_settings(cls, settings):
"""Create the unit statement from the FileSettings"""
return cls(settings.units, settings.zeros)
@classmethod
@ -742,7 +742,7 @@ class FormatStmt(ExcellonStatement):
def to_excellon(self, settings=None):
return 'FMAT,%d' % self.format
@property
def format_tuple(self):
return (self.format, 6 - self.format)
@ -844,38 +844,38 @@ class UnknownStmt(ExcellonStatement):
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
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,
@ -886,7 +886,7 @@ class SlotStmt(ExcellonStatement):
else:
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
settings.zero_suppression)
return (x_coord, y_coord)
@ -907,16 +907,16 @@ class SlotStmt(ExcellonStatement):
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):
@ -959,7 +959,7 @@ class SlotStmt(ExcellonStatement):
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

View file

@ -28,9 +28,9 @@ 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
@ -52,13 +52,13 @@ class ExcellonToolDefinitionParser(object):
----------
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'),
@ -66,34 +66,34 @@ class ExcellonToolDefinitionParser(object):
(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':
@ -102,19 +102,20 @@ class ExcellonToolDefinitionParser(object):
plated = ExcellonTool.PLATED_OPTIONAL
else:
plated = ExcellonTool.PLATED_UNKNOWN
tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated)
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
@ -137,34 +138,35 @@ def loads_rep(data, settings=None):
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.
# 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()
@ -180,7 +182,9 @@ class ExcellonReportParser(object):
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
tool = ExcellonTool(None, number=toolid, diameter=size,
plated=plated, feed_rate=feedrate,
rpm=speed)
self.tools[tool.number] = tool

View file

@ -6,6 +6,7 @@ import os
from ..cam import FileSettings
from ..excellon import read, detect_excellon_format, ExcellonFile, ExcellonParser
from ..excellon import DrillHit, DrillSlot
from ..excellon_statements import ExcellonTool
from .tests import *
@ -50,29 +51,28 @@ def test_read_settings():
assert_equal(ncdrill.settings['zeros'], 'trailing')
def test_bounds():
def test_bounding_box():
ncdrill = read(NCDRILL_FILE)
xbound, ybound = ncdrill.bounds
xbound, ybound = ncdrill.bounding_box
assert_array_almost_equal(xbound, (0.1300, 2.1430))
assert_array_almost_equal(ybound, (0.3946, 1.7164))
def test_report():
ncdrill = read(NCDRILL_FILE)
rprt = ncdrill.report()
def test_conversion():
import copy
ncdrill = read(NCDRILL_FILE)
assert_equal(ncdrill.settings.units, 'inch')
ncdrill_inch = copy.deepcopy(ncdrill)
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 inch_primitives:
primitive.to_metric()
for statement in ncdrill_inch.statements:
statement.to_metric()
@ -80,7 +80,8 @@ def test_conversion():
iter(ncdrill_inch.tools.values())):
assert_equal(i_tool, m_tool)
for m, i in zip(ncdrill.primitives, inch_primitives):
for m, i in zip(ncdrill.primitives, ncdrill_inch.primitives):
assert_equal(m.position, i.position, '%s not equal to %s' % (m, i))
assert_equal(m.diameter, i.diameter, '%s not equal to %s' % (m, i))
@ -197,3 +198,142 @@ def test_parse_unknown():
p = ExcellonParser(FileSettings())
p._parse_line('Not A Valid Statement')
assert_equal(p.statements[0].stmt, 'Not A Valid Statement')
def test_drill_hit_units_conversion():
""" Test unit conversion for drill hits
"""
# Inch hit
settings = FileSettings(units='inch')
tool = ExcellonTool(settings, diameter=1.0)
hit = DrillHit(tool, (1.0, 1.0))
assert_equal(hit.tool.settings.units, 'inch')
assert_equal(hit.tool.diameter, 1.0)
assert_equal(hit.position, (1.0, 1.0))
# No Effect
hit.to_inch()
assert_equal(hit.tool.settings.units, 'inch')
assert_equal(hit.tool.diameter, 1.0)
assert_equal(hit.position, (1.0, 1.0))
# Should convert
hit.to_metric()
assert_equal(hit.tool.settings.units, 'metric')
assert_equal(hit.tool.diameter, 25.4)
assert_equal(hit.position, (25.4, 25.4))
# No Effect
hit.to_metric()
assert_equal(hit.tool.settings.units, 'metric')
assert_equal(hit.tool.diameter, 25.4)
assert_equal(hit.position, (25.4, 25.4))
# Convert back to inch
hit.to_inch()
assert_equal(hit.tool.settings.units, 'inch')
assert_equal(hit.tool.diameter, 1.0)
assert_equal(hit.position, (1.0, 1.0))
def test_drill_hit_offset():
TEST_VECTORS = [
((0.0 ,0.0), (0.0, 1.0), (0.0, 1.0)),
((0.0, 0.0), (1.0, 1.0), (1.0, 1.0)),
((1.0, 1.0), (0.0, -1.0), (1.0, 0.0)),
((1.0, 1.0), (-1.0, -1.0), (0.0, 0.0)),
]
for position, offset, expected in TEST_VECTORS:
settings = FileSettings(units='inch')
tool = ExcellonTool(settings, diameter=1.0)
hit = DrillHit(tool, position)
assert_equal(hit.position, position)
hit.offset(offset[0], offset[1])
assert_equal(hit.position, expected)
def test_drill_slot_units_conversion():
""" Test unit conversion for drill hits
"""
# Inch hit
settings = FileSettings(units='inch')
tool = ExcellonTool(settings, diameter=1.0)
hit = DrillSlot(tool, (1.0, 1.0), (10.0, 10.0), DrillSlot.TYPE_ROUT)
assert_equal(hit.tool.settings.units, 'inch')
assert_equal(hit.tool.diameter, 1.0)
assert_equal(hit.start, (1.0, 1.0))
assert_equal(hit.end, (10.0, 10.0))
# No Effect
hit.to_inch()
assert_equal(hit.tool.settings.units, 'inch')
assert_equal(hit.tool.diameter, 1.0)
assert_equal(hit.start, (1.0, 1.0))
assert_equal(hit.end, (10.0, 10.0))
# Should convert
hit.to_metric()
assert_equal(hit.tool.settings.units, 'metric')
assert_equal(hit.tool.diameter, 25.4)
assert_equal(hit.start, (25.4, 25.4))
assert_equal(hit.end, (254.0, 254.0))
# No Effect
hit.to_metric()
assert_equal(hit.tool.settings.units, 'metric')
assert_equal(hit.tool.diameter, 25.4)
assert_equal(hit.start, (25.4, 25.4))
assert_equal(hit.end, (254.0, 254.0))
# Convert back to inch
hit.to_inch()
assert_equal(hit.tool.settings.units, 'inch')
assert_equal(hit.tool.diameter, 1.0)
assert_equal(hit.start, (1.0, 1.0))
assert_equal(hit.end, (10.0, 10.0))
def test_drill_slot_offset():
TEST_VECTORS = [
((0.0 ,0.0), (1.0, 1.0), (0.0, 0.0), (0.0, 0.0), (1.0, 1.0)),
((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (1.0, 0.0), (2.0, 1.0)),
((0.0, 0.0), (1.0, 1.0), (1.0, 1.0), (1.0, 1.0), (2.0, 2.0)),
((0.0, 0.0), (1.0, 1.0), (-1.0, 1.0), (-1.0, 1.0), (0.0, 2.0)),
]
for start, end, offset, expected_start, expected_end in TEST_VECTORS:
settings = FileSettings(units='inch')
tool = ExcellonTool(settings, diameter=1.0)
slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT)
assert_equal(slot.start, start)
assert_equal(slot.end, end)
slot.offset(offset[0], offset[1])
assert_equal(slot.start, expected_start)
assert_equal(slot.end, expected_end)
def test_drill_slot_bounds():
TEST_VECTORS = [
((0.0, 0.0), (1.0, 1.0), 1.0, ((-0.5, 1.5), (-0.5, 1.5))),
((0.0, 0.0), (1.0, 1.0), 0.5, ((-0.25, 1.25), (-0.25, 1.25))),
]
for start, end, diameter, expected, in TEST_VECTORS:
settings = FileSettings(units='inch')
tool = ExcellonTool(settings, diameter=diameter)
slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT)
assert_equal(slot.bounding_box, expected)
#def test_exce