From 36956f93fe1773229b6b7bb1b851adf169f97f79 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sun, 25 Aug 2019 12:39:01 +0900 Subject: improve routing mode compatibility with excellon specification --- gerberex/excellon.py | 242 ++++++++++++++++++++++++----------- tests/data/ref_drill_inch.txt | 10 +- tests/data/ref_drill_metric.txt | 24 ++-- tests/expects/excellon_offset.txt | 8 +- tests/expects/excellon_rotate.txt | 8 +- tests/expects/excellon_save.txt | 8 +- tests/expects/excellon_to_inch.txt | 10 +- tests/expects/excellon_to_metric.txt | 8 +- 8 files changed, 213 insertions(+), 105 deletions(-) diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 04d10e9..7014a6e 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -5,8 +5,10 @@ import operator +import gerber.excellon from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot -from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \ +from gerber.excellon_statements import ExcellonStatement, UnitStmt, CoordinateStmt, UnknownStmt, \ + SlotStmt, DrillModeStmt, RouteModeStmt, LinearModeStmt, \ ToolSelectionStmt, ZAxisRoutPositionStmt, \ RetractWithClampingStmt, RetractWithoutClampingStmt, \ EndOfProgramStmt @@ -19,6 +21,8 @@ def loads(data, filename=None, settings=None, tools=None, format=None): settings = FileSettings(**detect_excellon_format(data)) if format: settings.format = format + gerber.excellon.CoordinateStmt = CoordinateStmtEx + gerber.excellon.UnitStmt = UnitStmtEx file = ExcellonParser(settings, tools).parse_raw(data, filename) return ExcellonFileEx.from_file(file) @@ -26,49 +30,78 @@ class ExcellonFileEx(ExcellonFile): @classmethod def from_file(cls, file): def correct_statements(): - lazy_stmt = None - modifier = None for stmt in file.statements: - modifier = lazy_stmt - lazy_stmt = None - if isinstance(stmt, UnitStmt): - yield UnitStmtEx.from_statement(stmt) - elif isinstance(stmt, CoordinateStmt): - new_stmt = CoordinateStmtEx.from_statement(stmt) - if modifier and new_stmt.mode is None: - new_stmt.mode = modifier - yield new_stmt - elif isinstance(stmt, UnknownStmt): + if isinstance(stmt, UnknownStmt): line = stmt.stmt.strip() - mode = None if line[:3] == 'G02': - mode = CoordinateStmtEx.MODE_CIRCULER_CW + yield CircularCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) elif line[:3] == 'G03': - mode = CoordinateStmtEx.MODE_CIRCULER_CCW + yield CircularCCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) + elif line[0] == 'X' or line[0] == 'Y' or line[0] == 'A' or line[0] == 'I': + yield CoordinateStmtEx.from_excellon(line, file.settings) else: yield stmt - continue - if len(line) == 3: - lazy_stmt = mode - continue - new_stmt = CoordinateStmtEx.from_excellon(line[3:], file.settings) - new_stmt.mode = mode - yield new_stmt else: yield stmt def generate_hits(statements): + class CoordinateCtx: + def __init__(self, notation): + self.notation = notation + self.x = 0. + self.y = 0. + self.radius = None + self.center_offset = None + + def update(self, x=None, y=None, radius=None, center_offset=None): + if self.notation == 'absolute': + if x is not None: + self.x = x + if y is not None: + self.y = y + else: + if x is not None: + self.x += x + if y is not None: + self.y += y + if radius is not None: + self.radius = radius + if center_offset is not None: + self.center_offset = center_offset + + def node(self, mode, center_offset): + radius, offset = None, None + if mode == DrillRout.MODE_CIRCULER_CW or mode == DrillRout.MODE_CIRCULER_CCW: + if center_offset is None: + radius = self.radius + offset = self.center_offset + else: + radius = None + offset = center_offset + return DrillRout.Node(mode, self.x, self.y, radius, offset) + STAT_DRILL = 0 STAT_ROUT_UP = 1 STAT_ROUT_DOWN = 2 + status = STAT_DRILL current_tool = None - rout_statements = [] + rout_mode = None + coordinate_ctx = CoordinateCtx(file.notation) + rout_nodes = [] + + last_position = (0., 0.) + last_radius = None + last_center_offset = None - def make_rout(status, statements): - if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None: + def make_rout(status, nodes): + if status != STAT_ROUT_DOWN or len(nodes) == 0 or current_tool is None: return None - return DrillRout.from_coordinates(current_tool, statements) + return DrillRout(current_tool, nodes) for stmt in statements: if isinstance(stmt, ToolSelectionStmt): @@ -79,31 +112,48 @@ class ExcellonFileEx(ExcellonFile): if rout is not None: yield rout status = STAT_DRILL + rout_mode = None + elif isinstance(stmt, RouteModeStmt): + if status == STAT_DRILL: + status = STAT_ROUT_UP + rout_mode = DrillRout.MODE_ROUT + else: + rout_mode = DrillRout.MODE_LINEAR + + elif isinstance(stmt, LinearModeStmt): + rout_mode = DrillRout.MODE_LINEAR + elif isinstance(stmt, CircularCWModeStmt): + rout_mode = DrillRout.MODE_CIRCULER_CW + elif isinstance(stmt, CircularCCWModeStmt): + rout_mode = DrillRout.MODE_CIRCULER_CCW elif isinstance(stmt, ZAxisRoutPositionStmt) and status == STAT_ROUT_UP: status = STAT_ROUT_DOWN elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt): - rout = make_rout(status, rout_statements) + rout = make_rout(status, rout_nodes) rout_statements = [] if rout is not None: yield rout status = STAT_ROUT_UP elif isinstance(stmt, SlotStmt): - yield DrillSlotEx(current_tool, (stmt.x_start, stmt.y_start), - (stmt.x_end, stmt.y_end), DrillSlotEx.TYPE_G85) + coordinate_ctx.update(stmt.x_start, stmt.y_start) + x_start = coordinate_ctx.x + y_start = coordinate_ctx.y + coordinate_ctx.update(stmt.x_end, stmt.y_end) + x_end = coordinate_ctx.x + y_end = coordinate_ctx.y + yield DrillSlotEx(current_tool, (x_start, y_start), + (x_end, y_end), DrillSlotEx.TYPE_G85) elif isinstance(stmt, CoordinateStmtEx): - if stmt.mode is None: - if status != STAT_DRILL: - raise Exception('invalid statement sequence') - yield DrillHitEx(current_tool, (stmt.x, stmt.y)) - else: - if stmt.mode == stmt.MODE_ROUT: - status = STAT_ROUT_UP - if status == STAT_ROUT_UP: - rout_statements = [stmt] + center_offset = (stmt.i, stmt.j) \ + if stmt.i is not None and stmt.j is not None else None + coordinate_ctx.update(stmt.x, stmt.y, stmt.radius, center_offset) + if stmt.x is not None or stmt.y is not None: + if status == STAT_DRILL: + yield DrillHitEx(current_tool, (coordinate_ctx.x, coordinate_ctx.y)) + elif status == STAT_ROUT_UP: + rout_nodes = [coordinate_ctx.node(DrillRout.MODE_ROUT, None)] elif status == STAT_ROUT_DOWN: - rout_statements.append(stmt) - else: - raise Exception('invalid statement sequence') + rout_nodes.append(coordinate_ctx.node(rout_mode, center_offset)) statements = [s for s in correct_statements()] hits = [h for h in generate_hits(statements)] @@ -188,28 +238,33 @@ class DrillSlotEx(DrillSlot): return SlotStmt(*self.start, *self.end).to_excellon(settings) class DrillRout(object): + MODE_ROUT = 'G00' + MODE_LINEAR = 'G01' + MODE_CIRCULER_CW = 'G02' + MODE_CIRCULER_CCW = 'G03' + class Node(object): - def __init__(self, mode, x, y, radius): + def __init__(self, mode, x, y, radius=None, center_offset=None): self.mode = mode self.position = (x, y) self.radius = radius + self.center_offset = center_offset - @classmethod - def from_coordinates(cls, tool, coordinates): - nodes = [cls.Node(c.mode, c.x, c.y, c.radius) for c in coordinates] - return cls(tool, nodes) + def to_excellon(self, settings): + center_offset = self.center_offset \ + if self.center_offset is not None else (None, None) + return self.mode + CoordinateStmtEx( + *self.position, self.radius, *center_offset).to_excellon(settings) def __init__(self, tool, nodes): self.tool = tool self.nodes = nodes + self.nodes[0].mode = self.MODE_ROUT def to_excellon(self, settings): - node = self.nodes[0] - excellon = CoordinateStmtEx(*node.position, node.radius, - CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n' + excellon = self.nodes[0].to_excellon(settings) + '\nM15\n' for node in self.nodes[1:]: - excellon += CoordinateStmtEx(*node.position, node.radius, - node.mode).to_excellon(settings) + '\n' + excellon += node.to_excellon(settings) + '\n' excellon += 'M16\nG05' return excellon @@ -218,12 +273,16 @@ class DrillRout(object): node.position = tuple(map(inch, node.position)) node.radius = inch( node.radius) if node.radius is not None else None + if node.center_offset is not None: + node.center_offset = tuple(map(inch, node.center_offset)) def to_metric(self): for node in self.nodes: node.position = tuple(map(metric, node.position)) node.radius = metric( node.radius) if node.radius is not None else None + if node.center_offset is not None: + node.center_offset = tuple(map(metric, node.center_offset)) def offset(self, x_offset=0, y_offset=0): for node in self.nodes: @@ -232,6 +291,8 @@ class DrillRout(object): def rotate(self, angle, center=(0, 0)): for node in self.nodes: node.position = rotate(*node.position, angle, center) + if node.center_offset is not None: + node.center_offset = rotate(*node.center_offset, angle, (0., 0.)) class UnitStmtEx(UnitStmt): @classmethod @@ -243,48 +304,69 @@ class UnitStmtEx(UnitStmt): def to_excellon(self, settings=None): format = settings.format if settings else self.format - stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC', - 'LZ' if self.zeros == 'leading' else 'TZ', - '0' * format[0], '0' * format[1]) + stmt = None + if self.units == 'inch' and format == (2, 4): + stmt = 'INCH,%s' % ('LZ' if self.zeros == 'leading' else 'TZ') + else: + stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC', + 'LZ' if self.zeros == 'leading' else 'TZ', + '0' * format[0], '0' * format[1]) return stmt -class CoordinateStmtEx(CoordinateStmt): - MODE_ROUT = 'ROUT' - MODE_LINEAR = 'LINEAR' - MODE_CIRCULER_CW = 'CW' - MODE_CIRCULER_CCW = 'CCW' +class CircularCWModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CircularCWModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G02' +class CircularCCWModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CircularCCWModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G02' + +class CoordinateStmtEx(CoordinateStmt): @classmethod def from_statement(cls, stmt): newStmt = cls(x=stmt.x, y=stmt.y) - newStmt.mode = stmt.mode newStmt.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None return newStmt @classmethod def from_excellon(cls, line, settings, **kwargs): - parts = line.split('A') - stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) - if len(parts) > 1: + stmt = None + if 'A' in line: + parts = line.split('A') + stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) \ + if parts[0] != '' else cls() stmt.radius = parse_gerber_value( parts[1], settings.format, settings.zero_suppression) + elif 'I' in line: + jparts = line.split('J') + iparts = jparts[0].split('I') + stmt = cls.from_statement(CoordinateStmt.from_excellon(iparts[0], settings)) \ + if iparts[0] != '' else cls() + stmt.i = parse_gerber_value( + iparts[1], settings.format, settings.zero_suppression) + stmt.j = parse_gerber_value( + jparts[1], settings.format, settings.zero_suppression) + else: + stmt = cls.from_statement(CoordinateStmt.from_excellon(line, settings)) + return stmt - def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs): + def __init__(self, x=None, y=None, radius=None, i=None, j=None, **kwargs): super(CoordinateStmtEx, self).__init__(x, y, **kwargs) - self.mode = mode self.radius = radius + self.i = i + self.j = j def to_excellon(self, settings): stmt = '' - if self.mode == self.MODE_ROUT: - stmt += "G00" - if self.mode == self.MODE_LINEAR: - stmt += "G01" - if self.mode == self.MODE_CIRCULER_CW: - stmt += "G02" - if self.mode == self.MODE_CIRCULER_CCW: - stmt += "G03" if self.x is not None: stmt += 'X%s' % write_gerber_value(self.x, settings.format, settings.zero_suppression) @@ -294,6 +376,11 @@ class CoordinateStmtEx(CoordinateStmt): if self.radius is not None: stmt += 'A%s' % write_gerber_value(self.radius, settings.format, settings.zero_suppression) + elif self.i is not None and self.j is not None: + stmt += 'I%sJ%s' % (write_gerber_value(self.i, settings.format, + settings.zero_suppression), + write_gerber_value(self.j, settings.format, + settings.zero_suppression)) return stmt def __str__(self): @@ -304,6 +391,9 @@ class CoordinateStmtEx(CoordinateStmt): coord_str += 'Y: %g ' % self.y if self.radius is not None: coord_str += 'A: %g ' % self.radius + if self.i is not None: + coord_str += 'I: %g ' % self.i + if self.j is not None: + coord_str += 'J: %g ' % self.j - return '' % \ - (coord_str, self.mode if self.mode else 'HIT') + return '' % (coord_str) diff --git a/tests/data/ref_drill_inch.txt b/tests/data/ref_drill_inch.txt index 6aea2e0..6af5494 100644 --- a/tests/data/ref_drill_inch.txt +++ b/tests/data/ref_drill_inch.txt @@ -1,7 +1,7 @@ M48 FMAT,2 ICI,OFF -INCH,TZ,00.0000 +INCH,TZ T01C0.0236 T02C0.0275 T03C0.0314 @@ -28,10 +28,12 @@ X1417Y2795 T04 G00X236Y669 M15 -G03X630Y276A394 +G02X630Y276I0J-394 +G03X1024Y-118A394 +G03X1417Y276A394 G01X2598Y276 -G01X2598Y1614 -G03X2205Y2008A394 +G01X2598Y1024 +G03X1614Y2008I-984J0 G01X236Y2008 G01X236Y669 M16 diff --git a/tests/data/ref_drill_metric.txt b/tests/data/ref_drill_metric.txt index 903fc48..3a50856 100644 --- a/tests/data/ref_drill_metric.txt +++ b/tests/data/ref_drill_metric.txt @@ -24,18 +24,24 @@ M15 G01X2140Y2925 M16 G05 - X3600Y7100 T04 -G00X600Y1700 +G00 +X600Y1700 M15 -G03X1600Y700A1000 -G01X6600Y700 -G01X6600Y4100 -G03X5600Y5100A1000 -G01X600Y5100 -G01X600Y1700 +I0J-1000 +G02X1600Y700 +G03 +A1000 +I-2000J0 +X2600Y-300 +X3600Y700 +G01X6600 +Y2600 +G03X4100Y5100I-2500J0 +G01 +X600 +Y1700 M16 G05 - M30 diff --git a/tests/expects/excellon_offset.txt b/tests/expects/excellon_offset.txt index 41dbf3e..9a0b430 100644 --- a/tests/expects/excellon_offset.txt +++ b/tests/expects/excellon_offset.txt @@ -28,10 +28,12 @@ X14600Y12100 T04 G00X11600Y6700 M15 -G03X12600Y5700A1000 +G02X12600Y5700I0J-1000 +G03X13600Y4700A1000 +G03X14600Y5700A1000 G01X17600Y5700 -G01X17600Y9100 -G03X16600Y10100A1000 +G01X17600Y7600 +G03X15100Y10100I-2500J0 G01X11600Y10100 G01X11600Y6700 M16 diff --git a/tests/expects/excellon_rotate.txt b/tests/expects/excellon_rotate.txt index 9983909..f33dcfd 100644 --- a/tests/expects/excellon_rotate.txt +++ b/tests/expects/excellon_rotate.txt @@ -28,10 +28,12 @@ X4978Y5086 T04 G00X4006Y-1014 M15 -G03X5287Y-1612A1000 +G02X5287Y-1612I342J-940 +G03X6569Y-2210A1000 +G03X7167Y-928A1000 G01X9986Y98 -G01X8823Y3293 -G03X7541Y3891A1000 +G01X9336Y1883 +G03X6132Y3378I-2349J-855 G01X2843Y2181 G01X4006Y-1014 M16 diff --git a/tests/expects/excellon_save.txt b/tests/expects/excellon_save.txt index 6a22a7d..18fdcc4 100644 --- a/tests/expects/excellon_save.txt +++ b/tests/expects/excellon_save.txt @@ -28,10 +28,12 @@ X3600Y7100 T04 G00X600Y1700 M15 -G03X1600Y700A1000 +G02X1600Y700I0J-1000 +G03X2600Y-300A1000 +G03X3600Y700A1000 G01X6600Y700 -G01X6600Y4100 -G03X5600Y5100A1000 +G01X6600Y2600 +G03X4100Y5100I-2500J0 G01X600Y5100 G01X600Y1700 M16 diff --git a/tests/expects/excellon_to_inch.txt b/tests/expects/excellon_to_inch.txt index 6aea2e0..6af5494 100644 --- a/tests/expects/excellon_to_inch.txt +++ b/tests/expects/excellon_to_inch.txt @@ -1,7 +1,7 @@ M48 FMAT,2 ICI,OFF -INCH,TZ,00.0000 +INCH,TZ T01C0.0236 T02C0.0275 T03C0.0314 @@ -28,10 +28,12 @@ X1417Y2795 T04 G00X236Y669 M15 -G03X630Y276A394 +G02X630Y276I0J-394 +G03X1024Y-118A394 +G03X1417Y276A394 G01X2598Y276 -G01X2598Y1614 -G03X2205Y2008A394 +G01X2598Y1024 +G03X1614Y2008I-984J0 G01X236Y2008 G01X236Y669 M16 diff --git a/tests/expects/excellon_to_metric.txt b/tests/expects/excellon_to_metric.txt index 1bb4287..20f698e 100644 --- a/tests/expects/excellon_to_metric.txt +++ b/tests/expects/excellon_to_metric.txt @@ -28,10 +28,12 @@ X3599Y7099 T04 G00X599Y1699 M15 -G03X1600Y701A1001 +G02X1600Y701I0J-1001 +G03X2601Y-300A1001 +G03X3599Y701A1001 G01X6599Y701 -G01X6599Y4100 -G03X5601Y5100A1001 +G01X6599Y2601 +G03X4100Y5100I-2499J0 G01X599Y5100 G01X599Y1699 M16 -- cgit