From af97dcf2a8200d9319e20d2789dbb0baa0611ba5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 7 Oct 2014 22:44:08 -0400 Subject: fix excellon render --- gerber/__main__.py | 13 +++-- gerber/excellon.py | 62 ++++++++++++------------ gerber/gerber.py | 2 +- gerber/gerber_statements.py | 34 +++++++++++--- gerber/tests/test_excellon_statements.py | 48 +++++++++++++++++-- gerber/tests/test_utils.py | 81 +++++++++++++++++++------------- gerber/utils.py | 2 +- 7 files changed, 157 insertions(+), 85 deletions(-) diff --git a/gerber/__main__.py b/gerber/__main__.py index 31b70f8..26f36e1 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -16,21 +16,20 @@ # the License. if __name__ == '__main__': - from .gerber import GerberFile - from .excellon import ExcellonParser + import gerber + import excellon from .render import GerberSvgContext #import sys # - #if len(sys.argv) < 2: + #if len(sys.argv) < 2:` # print >> sys.stderr, "Usage: python -m gerber ..." # sys.exit(1) # ##for filename in sys.argv[1]: ## print "parsing %s" % filename ctx = GerberSvgContext() - g = GerberFile.read('SCB.GTL') + g = gerber.read('examples/test.gtl') g.render('test.svg', ctx) - p = ExcellonParser(ctx) - p.parse('ncdrill.txt') - p.dump('testwithdrill.svg') + p = excellon.read('ncdrill.txt') + p.render('testwithdrill.svg', ctx) diff --git a/gerber/excellon.py b/gerber/excellon.py index 9a5ef22..66b9ea2 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -21,9 +21,9 @@ Excellon module This module provides Excellon file classes and parsing utilities """ -import re + + from .excellon_statements import * -from .utils import parse_gerber_value from .cnc import CncFile, FileSettings @@ -70,15 +70,18 @@ class ExcellonFile(CncFile): def render(self, filename, ctx): """ Generate image of file """ + count = 0 for tool, pos in self.hits: ctx.drill(pos[0], pos[1], tool.diameter) + count += 1 + print('Drilled %d hits' % count) 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 @@ -95,27 +98,21 @@ class ExcellonParser(object): self.hits = [] self.active_tool = None self.pos = [0., 0.] - if ctx is not None: - self.ctx.set_coord_format(zero_suppression='trailing', - format=(2, 5), notation='absolute') def parse(self, filename): with open(filename, 'r') as f: for line in f: self._parse(line) - return ExcellonFile(self.statements, self.tools, self.hits, self._settings(), filename) - - def dump(self, filename): - if self.ctx is not None: - self.ctx.dump(filename) + return ExcellonFile(self.statements, self.tools, self.hits, + self._settings(), filename) def _parse(self, line): + line = line.strip() zs = self._settings()['zero_suppression'] fmt = self._settings()['format'] - if line[0] == ';': self.statements.append(CommentStmt.from_excellon(line)) - + elif line[:3] == 'M48': self.statements.append(HeaderBeginStmt()) self.state = 'HEADER' @@ -130,29 +127,41 @@ class ExcellonParser(object): if self.state == 'HEADER': self.state = 'DRILL' + elif line[:3] == 'M30': + stmt = EndOfProgramStmt.from_excellon(line) + self.statements.append(stmt) + elif line[:3] == 'G00': self.state = 'ROUT' elif line[:3] == 'G05': self.state = 'DRILL' - - elif ('INCH' in line or 'METRIC' in line) and ('LZ' in line or 'TZ' in line): + + elif (('INCH' in line or 'METRIC' in line) and + ('LZ' in line or 'TZ' in line)): stmt = UnitStmt.from_excellon(line) self.units = stmt.units self.zero_suppression = stmt.zero_suppression self.statements.append(stmt) - + elif line[:3] == 'M71' or line [:3] == 'M72': stmt = MeasuringModeStmt.from_excellon(line) self.units = stmt.units self.statements.append(stmt) - + elif line[:3] == 'ICI': stmt = IncrementalModeStmt.from_excellon(line) self.notation = 'incremental' if stmt.mode == 'on' else 'absolute' self.statements.append(stmt) - # tool definition + elif line[:3] == 'VER': + stmt = VersionStmt.from_excellon(line) + self.statements.append(stmt) + + elif line[:4] == 'FMAT': + stmt = FormatStmt.from_excellon(line) + self.statements.append(stmt) + elif line[0] == 'T' and self.state == 'HEADER': tool = ExcellonTool.from_excellon(line, self._settings()) self.tools[tool.number] = tool @@ -161,7 +170,6 @@ 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[int(line.strip().split('T')[1])] self.statements.append(stmt) elif line[0] in ['X', 'Y']: @@ -169,15 +177,6 @@ class ExcellonParser(object): x = stmt.x y = stmt.y self.statements.append(stmt) - #x = None - #y = None - #if line[0] == 'X': - # splitline = line.strip('X').split('Y') - # x = parse_gerber_value(splitline[0].strip(), fmt, zs) - # if len(splitline) == 2: - # y = parse_gerber_value(splitline[1].strip(), fmt, zs) - #else: - # y = parse_gerber_value(line.strip(' Y'), fmt, zs) if self.notation == 'absolute': if x is not None: self.pos[0] = x @@ -189,11 +188,8 @@ class ExcellonParser(object): if y is not None: self.pos[1] += y if self.state == 'DRILL': - self.hits.append((self.active_tool, self.pos)) + self.hits.append((self.active_tool, tuple(self.pos))) self.active_tool._hit() - if self.ctx is not None: - self.ctx.drill(self.pos[0], self.pos[1], - self.active_tool.diameter) else: self.statements.append(UnknownStmt.from_excellon(line)) @@ -201,7 +197,7 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zero_suppression=self.zero_suppression, notation=self.notation) - + if __name__ == '__main__': p = ExcellonParser() diff --git a/gerber/gerber.py b/gerber/gerber.py index 0278b0d..220405c 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -256,7 +256,7 @@ class GerberParser(object): elif param["param"] == "IN": yield INParamStmt.from_dict(param) elif param["param"] == "LN": - yield LNParamStmtfrom_dict(param) + yield LNParamStmt.from_dict(param) else: yield UnknownStmt(line) did_something = True diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 5a9d046..2f58a37 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -16,8 +16,8 @@ __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', class Statement(object): - def __init__(self, type): - self.type = type + def __init__(self, stype): + self.type = stype def __str__(self): s = "<{0} ".format(self.__class__.__name__) @@ -47,8 +47,8 @@ class FSParamStmt(ParamStmt): zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' x = map(int, stmt_dict.get('x').strip()) - format = (x[0], x[1]) - return cls(param, zeros, notation, format) + fmt = (x[0], x[1]) + return cls(param, zeros, notation, fmt) def __init__(self, param, zero_suppression='leading', notation='absolute', format=(2, 4)): @@ -88,9 +88,9 @@ class FSParamStmt(ParamStmt): def to_gerber(self): zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' notation = 'A' if self.notation == 'absolute' else 'I' - format = ''.join(map(str, self.format)) + fmt = ''.join(map(str, self.format)) return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, - format, format) + fmt, fmt) def __str__(self): return ('' % @@ -588,6 +588,28 @@ class EofStmt(Statement): def __str__(self): return '' +class QuadrantModeStmt(Statement): + + @classmethod + def from_gerber(cls, line): + line = line.strip() + if 'G74' not in line and 'G75' not in line: + raise ValueError('%s is not a valid quadrant mode statement' + % line) + return (cls('single-quadrant') if line[:3] == 'G74' + else cls('multi-quadrant')) + + def __init__(self, mode): + super(QuadrantModeStmt, self).__init__('Quadrant Mode') + mode = mode.lower + if mode not in ['single-quadrant', 'multi-quadrant']: + raise ValueError('Quadrant mode must be "single-quadrant" \ + or "multi-quadrant"') + self.mode = mode + + def to_gerber(self): + return 'G74*' if self.mode == 'single-quadrant' else 'G75*' + class UnknownStmt(Statement): """ Unknown Statement diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 4fa2b35..5e5e8dc 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -3,7 +3,7 @@ # Author: Hamilton Kibbe -from .tests import * +from .tests import assert_equal, assert_raises from ..excellon_statements import * @@ -65,6 +65,8 @@ def test_toolselection_dump(): def test_coordinatestmt_factory(): + """ Test CoordinateStmt factory method + """ line = 'X0278207Y0065293' stmt = CoordinateStmt.from_excellon(line) assert_equal(stmt.x, 2.78207) @@ -80,6 +82,8 @@ def test_coordinatestmt_factory(): def test_coordinatestmt_dump(): + """ Test CoordinateStmt to_excellon() + """ lines = ['X0278207Y0065293', 'X0243795', 'Y0082528', 'Y0086028', 'X0251295Y0081528', 'X02525Y0078', 'X0255Y00575', 'Y0052', 'X02675', 'Y00575', 'X02425', 'Y0052', 'X023', ] @@ -89,6 +93,8 @@ def test_coordinatestmt_dump(): def test_commentstmt_factory(): + """ Test CommentStmt factory method + """ line = ';Layer_Color=9474304' stmt = CommentStmt.from_excellon(line) assert_equal(stmt.comment, line[1:]) @@ -103,6 +109,8 @@ def test_commentstmt_factory(): def test_commentstmt_dump(): + """ Test CommentStmt to_excellon() + """ lines = [';Layer_Color=9474304', ';FILE_FORMAT=2:5', ';TYPE=PLATED', ] for line in lines: stmt = CommentStmt.from_excellon(line) @@ -110,6 +118,8 @@ def test_commentstmt_dump(): def test_unitstmt_factory(): + """ Test UnitStmt factory method + """ line = 'INCH,LZ' stmt = UnitStmt.from_excellon(line) assert_equal(stmt.units, 'inch') @@ -122,6 +132,8 @@ def test_unitstmt_factory(): def test_unitstmt_dump(): + """ Test UnitStmt to_excellon() + """ lines = ['INCH,LZ', 'INCH,TZ', 'METRIC,LZ', 'METRIC,TZ', ] for line in lines: stmt = UnitStmt.from_excellon(line) @@ -129,6 +141,8 @@ def test_unitstmt_dump(): def test_incrementalmode_factory(): + """ Test IncrementalModeStmt factory method + """ line = 'ICI,ON' stmt = IncrementalModeStmt.from_excellon(line) assert_equal(stmt.mode, 'on') @@ -139,6 +153,8 @@ def test_incrementalmode_factory(): def test_incrementalmode_dump(): + """ Test IncrementalModeStmt to_excellon() + """ lines = ['ICI,ON', 'ICI,OFF', ] for line in lines: stmt = IncrementalModeStmt.from_excellon(line) @@ -146,10 +162,14 @@ def test_incrementalmode_dump(): def test_incrementalmode_validation(): + """ Test IncrementalModeStmt input validation + """ assert_raises(ValueError, IncrementalModeStmt, 'OFF-ISH') def test_versionstmt_factory(): + """ Test VersionStmt factory method + """ line = 'VER,1' stmt = VersionStmt.from_excellon(line) assert_equal(stmt.version, 1) @@ -160,16 +180,22 @@ def test_versionstmt_factory(): def test_versionstmt_dump(): + """ Test VersionStmt to_excellon() + """ lines = ['VER,1', 'VER,2', ] for line in lines: stmt = VersionStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) def test_versionstmt_validation(): + """ Test VersionStmt input validation + """ assert_raises(ValueError, VersionStmt, 3) def test_formatstmt_factory(): + """ Test FormatStmt factory method + """ line = 'FMAT,1' stmt = FormatStmt.from_excellon(line) assert_equal(stmt.format, 1) @@ -180,6 +206,8 @@ def test_formatstmt_factory(): def test_formatstmt_dump(): + """ Test FormatStmt to_excellon() + """ lines = ['FMAT,1', 'FMAT,2', ] for line in lines: stmt = FormatStmt.from_excellon(line) @@ -187,10 +215,14 @@ def test_formatstmt_dump(): def test_formatstmt_validation(): + """ Test FormatStmt input validation + """ assert_raises(ValueError, FormatStmt, 3) def test_linktoolstmt_factory(): + """ Test LinkToolStmt factory method + """ line = '1/2/3/4' stmt = LinkToolStmt.from_excellon(line) assert_equal(stmt.linked_tools, [1, 2, 3, 4]) @@ -201,13 +233,17 @@ def test_linktoolstmt_factory(): def test_linktoolstmt_dump(): + """ Test LinkToolStmt to_excellon() + """ lines = ['1/2/3/4', '5/6/7', ] for line in lines: stmt = LinkToolStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) -def test_measuringmodestmt_factory(): +def test_measmodestmt_factory(): + """ Test MeasuringModeStmt factory method + """ line = 'M72' stmt = MeasuringModeStmt.from_excellon(line) assert_equal(stmt.units, 'inch') @@ -217,13 +253,17 @@ def test_measuringmodestmt_factory(): assert_equal(stmt.units, 'metric') -def test_measuringmodestmt_dump(): +def test_measmodestmt_dump(): + """ Test MeasuringModeStmt to_excellon() + """ lines = ['M71', 'M72', ] for line in lines: stmt = MeasuringModeStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) -def test_measuringmodestmt_validation(): +def test_measmodestmt_validation(): + """ Test MeasuringModeStmt input validation + """ assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70') assert_raises(ValueError, MeasuringModeStmt, 'millimeters') diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py index 50e2403..001a32f 100644 --- a/gerber/tests/test_utils.py +++ b/gerber/tests/test_utils.py @@ -3,6 +3,7 @@ # Author: Hamilton Kibbe +from .tests import assert_equal from ..utils import decimal_string, parse_gerber_value, write_gerber_value @@ -10,59 +11,73 @@ def test_zero_suppression(): """ Test gerber value parser and writer handle zero suppression correctly. """ # Default format - format = (2, 5) - + fmt = (2, 5) + # Test leading zero suppression zero_suppression = 'leading' test_cases = [('1', 0.00001), ('10', 0.0001), ('100', 0.001), - ('1000', 0.01), ('10000', 0.1), ('100000', 1.0),('1000000', 10.0), - ('-1', -0.00001), ('-10', -0.0001), ('-100', -0.001), - ('-1000', -0.01), ('-10000', -0.1), ('-100000', -1.0),('-1000000', -10.0),] + ('1000', 0.01), ('10000', 0.1), ('100000', 1.0), + ('1000000', 10.0), ('-1', -0.00001), ('-10', -0.0001), + ('-100', -0.001), ('-1000', -0.01), ('-10000', -0.1), + ('-100000', -1.0), ('-1000000', -10.0), ] for string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) - + assert(value == parse_gerber_value(string, fmt, zero_suppression)) + assert(string == write_gerber_value(value, fmt, zero_suppression)) + # Test trailing zero suppression zero_suppression = 'trailing' test_cases = [('1', 10.0), ('01', 1.0), ('001', 0.1), ('0001', 0.01), - ('00001', 0.001), ('000001', 0.0001), ('0000001', 0.00001), - ('-1', -10.0), ('-01', -1.0), ('-001', -0.1), ('-0001', -0.01), - ('-00001', -0.001), ('-000001', -0.0001), ('-0000001', -0.00001)] + ('00001', 0.001), ('000001', 0.0001), + ('0000001', 0.00001), ('-1', -10.0), ('-01', -1.0), + ('-001', -0.1), ('-0001', -0.01), ('-00001', -0.001), + ('-000001', -0.0001), ('-0000001', -0.00001)] for string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) - + assert(value == parse_gerber_value(string, fmt, zero_suppression)) + assert(string == write_gerber_value(value, fmt, zero_suppression)) def test_format(): """ Test gerber value parser and writer handle format correctly """ zero_suppression = 'leading' - test_cases = [((2,7),'1',0.0000001), ((2,6),'1',0.000001), - ((2,5),'1',0.00001), ((2,4),'1',0.0001), ((2,3),'1',0.001), - ((2,2),'1',0.01), ((2,1),'1',0.1), ((2,7),'-1',-0.0000001), - ((2,6),'-1',-0.000001), ((2,5),'-1',-0.00001), ((2,4),'-1',-0.0001), - ((2,3),'-1',-0.001), ((2,2),'-1',-0.01), ((2,1),'-1',-0.1),] - for format, string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) - + test_cases = [((2, 7), '1', 0.0000001), ((2, 6), '1', 0.000001), + ((2, 5), '1', 0.00001), ((2, 4), '1', 0.0001), + ((2, 3), '1', 0.001), ((2, 2), '1', 0.01), + ((2, 1), '1', 0.1), ((2, 7), '-1', -0.0000001), + ((2, 6), '-1', -0.000001), ((2, 5), '-1', -0.00001), + ((2, 4), '-1', -0.0001), ((2, 3), '-1', -0.001), + ((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1), ] + for fmt, string, value in test_cases: + assert(value == parse_gerber_value(string, fmt, zero_suppression)) + assert(string == write_gerber_value(value, fmt, zero_suppression)) + zero_suppression = 'trailing' - test_cases = [((6, 5), '1' , 100000.0), ((5, 5), '1', 10000.0), - ((4, 5), '1', 1000.0), ((3, 5), '1', 100.0),((2, 5), '1', 10.0), - ((1, 5), '1', 1.0), ((6, 5), '-1' , -100000.0), - ((5, 5), '-1', -10000.0), ((4, 5), '-1', -1000.0), - ((3, 5), '-1', -100.0),((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0),] - for format, string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) + test_cases = [((6, 5), '1', 100000.0), ((5, 5), '1', 10000.0), + ((4, 5), '1', 1000.0), ((3, 5), '1', 100.0), + ((2, 5), '1', 10.0), ((1, 5), '1', 1.0), + ((6, 5), '-1', -100000.0), ((5, 5), '-1', -10000.0), + ((4, 5), '-1', -1000.0), ((3, 5), '-1', -100.0), + ((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0), ] + for fmt, string, value in test_cases: + assert(value == parse_gerber_value(string, fmt, zero_suppression)) + assert(string == write_gerber_value(value, fmt, zero_suppression)) def test_decimal_truncation(): - """ Test decimal string truncates value to the correct precision + """ Test decimal_string truncates value to the correct precision """ value = 1.123456789 for x in range(10): result = decimal_string(value, precision=x) calculated = '1.' + ''.join(str(y) for y in range(1,x+1)) - assert(result == calculated) \ No newline at end of file + assert(result == calculated) + + +def test_decimal_padding(): + """ Test decimal_string padding + """ + value = 1.123 + assert_equal(decimal_string(value, precision=3, padding=True), '1.123') + assert_equal(decimal_string(value, precision=4, padding=True), '1.1230') + assert_equal(decimal_string(value, precision=5, padding=True), '1.12300') + assert_equal(decimal_string(value, precision=6, padding=True), '1.123000') diff --git a/gerber/utils.py b/gerber/utils.py index 1721a7d..fce6369 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -113,7 +113,7 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): # Edge case... if value == 0: return '00' - + # negative sign affects padding, so deal with it at the end... negative = value < 0.0 if negative: -- cgit