From 9febca7da6a730b3b3ca3a54129a9f88e5c44d14 Mon Sep 17 00:00:00 2001 From: opiopan Date: Thu, 21 Mar 2019 22:00:32 +0900 Subject: initial commit --- gerberex/excellon.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 gerberex/excellon.py (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py new file mode 100644 index 0000000..78e6e5f --- /dev/null +++ b/gerberex/excellon.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) +from gerber.excellon_statements import UnitStmt +from gerber.cam import FileSettings + +def loads(data, filename=None, settings=None, tools=None, format=None): + if not settings: + settings = FileSettings(**detect_excellon_format(data)) + if format: + settings.format = format + file = ExcellonParser(settings, tools).parse_raw(data, filename) + return ExcellonFileEx.from_file(file) + +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 __init__(self, statements, tools, hits, settings, filename=None): + super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename) + +class UnitStmtEx(UnitStmt): + @classmethod + def from_statement(cls, stmt): + return cls(units=stmt.units, zeros=stmt.zeros, format=stmt.format, id=stmt.id) + + def __init__(self, units='inch', zeros='leading', format=None, **kwargs): + super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs) + + def to_excellon(self, settings=None): + stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC', + 'LZ' if self.zeros == 'leading' else 'TZ', + '0' * self.format[0], '0' * self.format[1]) + return stmt -- cgit From 690df56bb71020901167605a87ec451081fa18d7 Mon Sep 17 00:00:00 2001 From: opiopan Date: Sat, 23 Mar 2019 21:59:13 +0900 Subject: add rotation fuction --- gerberex/excellon.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 78e6e5f..90d6742 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -6,6 +6,7 @@ from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) from gerber.excellon_statements import UnitStmt from gerber.cam import FileSettings +from gerberex.utility import rotate def loads(data, filename=None, settings=None, tools=None, format=None): if not settings: @@ -27,6 +28,12 @@ class ExcellonFileEx(ExcellonFile): def __init__(self, statements, tools, hits, settings, filename=None): super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename) + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + for hit in self.hits: + hit.position = rotate(hit.position[0], hit.position[1], angle, center) + class UnitStmtEx(UnitStmt): @classmethod def from_statement(cls, stmt): -- cgit From e3c59e39cf9bc64ce9d76c324b82956a65515f16 Mon Sep 17 00:00:00 2001 From: opiopan Date: Sun, 7 Apr 2019 22:22:33 +0900 Subject: expand test and fix many issues --- gerberex/excellon.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 90d6742..b72b95b 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -6,6 +6,7 @@ from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile) from gerber.excellon_statements import UnitStmt from gerber.cam import FileSettings +from gerber.utils import inch, metric from gerberex.utility import rotate def loads(data, filename=None, settings=None, tools=None, format=None): @@ -33,6 +34,19 @@ class ExcellonFileEx(ExcellonFile): return for hit in self.hits: hit.position = rotate(hit.position[0], hit.position[1], 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])) + + 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])) + class UnitStmtEx(UnitStmt): @classmethod @@ -43,7 +57,8 @@ class UnitStmtEx(UnitStmt): super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs) 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' * self.format[0], '0' * self.format[1]) + '0' * format[0], '0' * format[1]) return stmt -- cgit 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 --- gerberex/excellon.py | 251 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 240 insertions(+), 11 deletions(-) (limited to 'gerberex/excellon.py') 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 From 02258202793e9c70f212171fe4a92f6bab3a6f72 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sun, 18 Aug 2019 14:16:22 +0900 Subject: fix a bug that unit traslation of excellon fail --- gerberex/excellon.py | 48 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 657f02a..04d10e9 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -124,13 +124,19 @@ class ExcellonFileEx(ExcellonFile): def to_inch(self): if self.units == 'metric': - super(ExcellonFileEx, self).to_inch() + for stmt in self.statements: + stmt.to_inch() + for tool in self.tools: + self.tools[tool].to_inch() for hit in self.hits: hit.to_inch() def to_metric(self): if self.units == 'inch': - super(ExcellonFileEx, self).to_metric() + for stmt in self.statements: + stmt.to_metric() + for tool in self.tools: + self.tools[tool].to_metric() for hit in self.hits: hit.to_metric() @@ -153,13 +159,27 @@ class ExcellonFileEx(ExcellonFile): f.write(EndOfProgramStmt().to_excellon() + '\n') class DrillHitEx(DrillHit): - def rotate(self, angle, center=(0,0)): + def to_inch(self): + self.position = tuple(map(inch, self.position)) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + + 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 to_inch(self): + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + + def to_metric(self): + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + def rotate(self, angle, center=(0,0)): self.start = rotate(*self.start, angle, center) self.end = rotate(*self.end, angle, center) @@ -190,24 +210,20 @@ class DrillRout(object): for node in self.nodes[1:]: excellon += CoordinateStmtEx(*node.position, node.radius, node.mode).to_excellon(settings) + '\n' - excellon += 'M16\nG05\n' + excellon += 'M16\nG05' 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 + 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 + 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: -- cgit 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 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 166 insertions(+), 76 deletions(-) (limited to 'gerberex/excellon.py') 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) -- cgit From 13ab9db6e7571f3d0fcb406bfe6795eea9ce4e1c Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Sun, 25 Aug 2019 20:16:53 +0900 Subject: support incremental coordinate for excellon --- gerberex/excellon.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 7014a6e..a8c01c7 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -26,6 +26,13 @@ def loads(data, filename=None, settings=None, tools=None, format=None): file = ExcellonParser(settings, tools).parse_raw(data, filename) return ExcellonFileEx.from_file(file) +def write_excellon_header(file, settings, tools): + file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % + UnitStmtEx(settings.units, settings.zeros, settings.format).to_excellon(settings)) + for tool in tools: + file.write(tool.to_excellon(settings) + '\n') + file.write('%%\nG90\n%s\n' % ('M72' if settings.units == 'inch' else 'M71')) + class ExcellonFileEx(ExcellonFile): @classmethod def from_file(cls, file): @@ -180,6 +187,7 @@ class ExcellonFileEx(ExcellonFile): self.tools[tool].to_inch() for hit in self.hits: hit.to_inch() + self.units = 'inch' def to_metric(self): if self.units == 'inch': @@ -189,17 +197,12 @@ class ExcellonFileEx(ExcellonFile): self.tools[tool].to_metric() for hit in self.hits: hit.to_metric() + self.units = '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 - + write_excellon_header(f, self.settings, [self.tools[t] for t in self.tools]) for tool in iter(self.tools.values()): f.write(ToolSelectionStmt( tool.number).to_excellon(self.settings) + '\n') -- cgit From 2b1c751ff76ebd6901633235ee694cc93dabce81 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Mon, 9 Sep 2019 09:07:38 +0900 Subject: improve compatibility with RS-274x specification: - can merge multiple files having different file scope modifier, such as AS, MI, OF, SF, and IR - support modal coordinate notation --- gerberex/excellon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index a8c01c7..4f867be 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -27,7 +27,7 @@ def loads(data, filename=None, settings=None, tools=None, format=None): return ExcellonFileEx.from_file(file) def write_excellon_header(file, settings, tools): - file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % + file.write('M48\nFMAT,2\nICI,OFF\n%s\n' % UnitStmtEx(settings.units, settings.zeros, settings.format).to_excellon(settings)) for tool in tools: file.write(tool.to_excellon(settings) + '\n') -- cgit From d7a069324222bb8f69adc9b1c815fc9f3f6a29d6 Mon Sep 17 00:00:00 2001 From: Hiroshi Murayama Date: Mon, 30 Sep 2019 18:52:17 +0900 Subject: fix a issue that coordinate normalization for excellon is imperfect --- gerberex/excellon.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 4f867be..570f94e 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -200,6 +200,8 @@ class ExcellonFileEx(ExcellonFile): self.units = 'metric' def write(self, filename=None): + self.notation = 'absolute' + self.zeros = 'trailing' filename = filename if filename is not None else self.filename with open(filename, 'w') as f: write_excellon_header(f, self.settings, [self.tools[t] for t in self.tools]) -- cgit From 71c371ca680483aa9ef18d2998832460dd43abdf Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 6 Feb 2021 09:38:49 +0100 Subject: Fix mystery crashes in excellon parser I don't know this code and I don't get what was intended here, but it makes way more sense to me this way. --- gerberex/excellon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerberex/excellon.py') diff --git a/gerberex/excellon.py b/gerberex/excellon.py index 570f94e..f7787d3 100644 --- a/gerberex/excellon.py +++ b/gerberex/excellon.py @@ -114,8 +114,8 @@ class ExcellonFileEx(ExcellonFile): if isinstance(stmt, ToolSelectionStmt): current_tool = file.tools[stmt.tool] elif isinstance(stmt, DrillModeStmt): - rout = make_rout(status, rout_statements) - rout_statements = [] + rout = make_rout(status, rout_nodes) + rout_nodes = [] if rout is not None: yield rout status = STAT_DRILL @@ -137,7 +137,7 @@ class ExcellonFileEx(ExcellonFile): status = STAT_ROUT_DOWN elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt): rout = make_rout(status, rout_nodes) - rout_statements = [] + rout_nodes = [] if rout is not None: yield rout status = STAT_ROUT_UP -- cgit