summaryrefslogtreecommitdiff
path: root/gerber
diff options
context:
space:
mode:
Diffstat (limited to 'gerber')
-rwxr-xr-xgerber/excellon.py45
-rw-r--r--gerber/excellon_statements.py66
-rw-r--r--gerber/excellon_tool.py70
-rw-r--r--gerber/tests/test_excellon.py154
4 files changed, 242 insertions, 93 deletions
diff --git a/gerber/excellon.py b/gerber/excellon.py
index 9825c5a..c3de948 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -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
diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py
index ac9c528..bcf35e4 100644
--- a/gerber/excellon_statements.py
+++ b/gerber/excellon_statements.py
@@ -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
diff --git a/gerber/excellon_tool.py b/gerber/excellon_tool.py
index bd76e54..a9ac450 100644
--- a/gerber/excellon_tool.py
+++ b/gerber/excellon_tool.py
@@ -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 \ No newline at end of file
+
+ tool = ExcellonTool(None, number=toolid, diameter=size,
+ plated=plated, feed_rate=feedrate,
+ rpm=speed)
+
+ self.tools[tool.number] = tool
diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py
index 1402938..6cddb60 100644
--- a/gerber/tests/test_excellon.py
+++ b/gerber/tests/test_excellon.py
@@ -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