From 4eb0e063bcd34c21b737023aa6ed5baed80658d1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 13 Jun 2021 15:00:17 +0200 Subject: Repo re-org, make gerberex tests run --- gerbonara/gerber/panelize/excellon.py | 404 ++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 gerbonara/gerber/panelize/excellon.py (limited to 'gerbonara/gerber/panelize/excellon.py') diff --git a/gerbonara/gerber/panelize/excellon.py b/gerbonara/gerber/panelize/excellon.py new file mode 100644 index 0000000..ae0b68e --- /dev/null +++ b/gerbonara/gerber/panelize/excellon.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2019 Hiroshi Murayama + +import operator + +from .. import excellon +from ..excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot +from ..excellon_statements import ExcellonStatement, UnitStmt, CoordinateStmt, UnknownStmt, \ + SlotStmt, DrillModeStmt, RouteModeStmt, LinearModeStmt, \ + ToolSelectionStmt, ZAxisRoutPositionStmt, \ + RetractWithClampingStmt, RetractWithoutClampingStmt, \ + EndOfProgramStmt +from ..cam import FileSettings +from ..utils import inch, metric, write_gerber_value, parse_gerber_value +from .utility import rotate + +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 + excellon.CoordinateStmt = CoordinateStmtEx + excellon.UnitStmt = UnitStmtEx + 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): + def correct_statements(): + for stmt in file.statements: + if isinstance(stmt, UnknownStmt): + line = stmt.stmt.strip() + if line[:3] == 'G02': + yield CircularCWModeStmt() + if len(line) > 3: + yield CoordinateStmtEx.from_excellon(line[3:], file.settings) + elif line[:3] == 'G03': + 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 + 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_mode = None + coordinate_ctx = CoordinateCtx(file.notation) + rout_nodes = [] + + last_position = (0., 0.) + last_radius = None + last_center_offset = None + + def make_rout(status, nodes): + if status != STAT_ROUT_DOWN or len(nodes) == 0 or current_tool is None: + return None + return DrillRout(current_tool, nodes) + + for stmt in statements: + if isinstance(stmt, ToolSelectionStmt): + current_tool = file.tools[stmt.tool] + elif isinstance(stmt, DrillModeStmt): + rout = make_rout(status, rout_nodes) + rout_nodes = [] + 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_nodes) + rout_nodes = [] + if rout is not None: + yield rout + status = STAT_ROUT_UP + elif isinstance(stmt, SlotStmt): + 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): + 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_nodes.append(coordinate_ctx.node(rout_mode, center_offset)) + + 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) + + def rotate(self, angle, center=(0,0)): + if angle % 360 == 0: + return + for hit in self.hits: + hit.rotate(angle, center) + + def to_inch(self): + if self.units == 'metric': + 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() + self.units = 'inch' + + def to_metric(self): + if self.units == 'inch': + 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() + 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]) + 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 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) + + def to_excellon(self, settings): + 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=None, center_offset=None): + self.mode = mode + self.position = (x, y) + self.radius = radius + self.center_offset = center_offset + + 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): + excellon = self.nodes[0].to_excellon(settings) + '\nM15\n' + for node in self.nodes[1:]: + excellon += node.to_excellon(settings) + '\n' + excellon += 'M16\nG05' + return excellon + + def to_inch(self): + 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 + 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: + 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) + if node.center_offset is not None: + node.center_offset = rotate(*node.center_offset, angle, (0., 0.)) + +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): + format = settings.format if settings else self.format + 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 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.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None + return newStmt + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + 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, i=None, j=None, **kwargs): + super(CoordinateStmtEx, self).__init__(x, y, **kwargs) + self.radius = radius + self.i = i + self.j = j + + def to_excellon(self, settings): + stmt = '' + 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) + 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): + 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 + 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) -- cgit