From 22f4c8a3f5bdce243908f3787216344b200902df Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sat, 17 Aug 2019 23:38:30 +0900 Subject: router mode and G85 slot in excellon file is supported --- README.md | 8 ++ gerberex/composition.py | 5 +- gerberex/excellon.py | 251 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 249 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0429e54..d688edf 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,18 @@ pcb-tools-extension adds following function to pcb-tools. Only RS-274x format and Excellon drill format data can be handled by current version of this library. ## Installation +You can install a stable version by following step. + ```shell $ pip install pcb-tools-extension ``` +If you have a intention to try latest developing version, please install as follows. + +```shell +$ pip install git+https://github.com/opiopan/pcb-tools-extension.git +``` + ## How to panelize Following code is a example to panelize two top metal layer files. diff --git a/gerberex/composition.py b/gerberex/composition.py index 29725ba..7f691f5 100644 --- a/gerberex/composition.py +++ b/gerberex/composition.py @@ -150,10 +150,7 @@ class DrillComposition(Composition): yield ToolSelectionStmt(t.number).to_excellon(self.settings) for h in self.hits: if h.tool.number == t.number: - if type(h) == DrillSlot: - yield SlotStmt(*h.start, *h.end).to_excellon(self.settings) - elif type(h) == DrillHit: - yield CoordinateStmt(*h.position).to_excellon(self.settings) + yield h.to_excellon(self.settings) for num, statement in self.dxf_statements: if num == t.number: yield statement.to_excellon(self.settings) diff --git a/gerberex/excellon.py b/gerberex/excellon.py index b72b95b..657f02a 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -3,10 +3,15 @@ # Copyright 2019 Hiroshi Murayama -from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) -from gerber.excellon_statements import UnitStmt +import operator + +from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot +from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \ + ToolSelectionStmt, ZAxisRoutPositionStmt, \ + RetractWithClampingStmt, RetractWithoutClampingStmt, \ + EndOfProgramStmt from gerber.cam import FileSettings -from gerber.utils import inch, metric +from gerber.utils import inch, metric, write_gerber_value, parse_gerber_value from gerberex.utility import rotate def loads(data, filename=None, settings=None, tools=None, format=None): @@ -20,11 +25,93 @@ def loads(data, filename=None, settings=None, tools=None, format=None): class ExcellonFileEx(ExcellonFile): @classmethod def from_file(cls, file): - statements = [ - UnitStmtEx.from_statement(s) if isinstance(s, UnitStmt) else s \ - for s in file.statements - ] - return cls(statements, file.tools, file.hits, file.settings, file.filename) + 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): + line = stmt.stmt.strip() + mode = None + if line[:3] == 'G02': + mode = CoordinateStmtEx.MODE_CIRCULER_CW + elif line[:3] == 'G03': + mode = CoordinateStmtEx.MODE_CIRCULER_CCW + 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): + STAT_DRILL = 0 + STAT_ROUT_UP = 1 + STAT_ROUT_DOWN = 2 + status = STAT_DRILL + current_tool = None + rout_statements = [] + + def make_rout(status, statements): + if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None: + return None + return DrillRout.from_coordinates(current_tool, statements) + + for stmt in statements: + if isinstance(stmt, ToolSelectionStmt): + current_tool = file.tools[stmt.tool] + elif isinstance(stmt, DrillModeStmt): + rout = make_rout(status, rout_statements) + rout_statements = [] + if rout is not None: + yield rout + status = STAT_DRILL + 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_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) + 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] + elif status == STAT_ROUT_DOWN: + rout_statements.append(stmt) + else: + raise Exception('invalid statement sequence') + + statements = [s for s in correct_statements()] + hits = [h for h in generate_hits(statements)] + return cls(statements, file.tools, hits, file.settings, file.filename) + + @property + def primitives(self): + return [] def __init__(self, statements, tools, hits, settings, filename=None): super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename) @@ -33,20 +120,102 @@ class ExcellonFileEx(ExcellonFile): if angle % 360 == 0: return for hit in self.hits: - hit.position = rotate(hit.position[0], hit.position[1], angle, center) + hit.rotate(angle, center) def to_inch(self): if self.units == 'metric': super(ExcellonFileEx, self).to_inch() for hit in self.hits: - hit.position = (inch(hit.position[0]), inch(hit.position[1])) + hit.to_inch() def to_metric(self): if self.units == 'inch': super(ExcellonFileEx, self).to_metric() for hit in self.hits: - hit.position = (metric(hit.position[0]), metric(hit.position[1])) + hit.to_metric() + + def write(self, filename=None): + filename = filename if filename is not None else self.filename + with open(filename, 'w') as f: + + for statement in self.statements: + if not isinstance(statement, ToolSelectionStmt): + f.write(statement.to_excellon(self.settings) + '\n') + else: + break + + for tool in iter(self.tools.values()): + f.write(ToolSelectionStmt( + tool.number).to_excellon(self.settings) + '\n') + for hit in self.hits: + if hit.tool.number == tool.number: + f.write(hit.to_excellon(self.settings) + '\n') + f.write(EndOfProgramStmt().to_excellon() + '\n') + +class DrillHitEx(DrillHit): + def rotate(self, angle, center=(0,0)): + self.position = rotate(*self.position, angle, center) + + def to_excellon(self, settings): + return CoordinateStmtEx(*self.position).to_excellon(settings) + +class DrillSlotEx(DrillSlot): + def rotate(self, angle, center=(0,0)): + self.start = rotate(*self.start, angle, center) + self.end = rotate(*self.end, angle, center) + + def to_excellon(self, settings): + return SlotStmt(*self.start, *self.end).to_excellon(settings) + +class DrillRout(object): + class Node(object): + def __init__(self, mode, x, y, radius): + self.mode = mode + self.position = (x, y) + self.radius = radius + + @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 __init__(self, tool, nodes): + self.tool = tool + self.nodes = nodes + def to_excellon(self, settings): + node = self.nodes[0] + excellon = CoordinateStmtEx(*node.position, node.radius, + CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n' + for node in self.nodes[1:]: + excellon += CoordinateStmtEx(*node.position, node.radius, + node.mode).to_excellon(settings) + '\n' + excellon += 'M16\nG05\n' + return excellon + + def to_inch(self): + if self.tool.settings.units == 'metric': + self.tool.to_inch() + for node in self.nodes: + node.position = tuple(map(inch, node.position)) + node.radius = inch( + node.radius) if node.radius is not None else None + + def to_metric(self): + if self.tool.settings.units == 'inch': + self.tool.to_metric() + 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 + + def offset(self, x_offset=0, y_offset=0): + for node in self.nodes: + node.position = tuple(map(operator.add, node.position, (x_offset, y_offset))) + + def rotate(self, angle, center=(0, 0)): + for node in self.nodes: + node.position = rotate(*node.position, angle, center) class UnitStmtEx(UnitStmt): @classmethod @@ -62,3 +231,63 @@ class UnitStmtEx(UnitStmt): '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' + + @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.radius = parse_gerber_value( + parts[1], settings.format, settings.zero_suppression) + return stmt + + def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs): + super(CoordinateStmtEx, self).__init__(x, y, **kwargs) + self.mode = mode + self.radius = radius + + 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) + if self.y is not None: + stmt += 'Y%s' % write_gerber_value(self.y, settings.format, + settings.zero_suppression) + if self.radius is not None: + stmt += 'A%s' % write_gerber_value(self.radius, settings.format, + settings.zero_suppression) + return stmt + + def __str__(self): + coord_str = '' + if self.x is not None: + coord_str += 'X: %g ' % self.x + if self.y is not None: + coord_str += 'Y: %g ' % self.y + if self.radius is not None: + coord_str += 'A: %g ' % self.radius + + return '' % \ + (coord_str, self.mode if self.mode else 'HIT') -- cgit