summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/panelize/excellon.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber/panelize/excellon.py')
-rw-r--r--gerbonara/gerber/panelize/excellon.py404
1 files changed, 404 insertions, 0 deletions
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 <opiopan@gmail.com>
+
+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 '<Coordinate Statement: %s>' % (coord_str)