From 41a7b90dff19b69ef03fed4104ecfdcbfcb21641 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 07:55:43 -0500 Subject: Excellon update --- gerber/excellon.py | 45 ++++++------ gerber/excellon_statements.py | 66 +++++++++--------- gerber/excellon_tool.py | 70 ++++++++++--------- gerber/tests/test_excellon.py | 154 ++++++++++++++++++++++++++++++++++++++++-- 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 (, (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[0-9/.]+)\s+(?PP|N)\s+T(?P[0-9]{2})\s+(?P[0-9/.]+)\s+(?P[0-9/.]+)') allegro_comment_mils = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') allegro2_comment_mils = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') allegro_comment_mm = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+') allegro2_comment_mm = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(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 -- cgit