summaryrefslogtreecommitdiff
path: root/gerber/excellon_statements.py
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2021-06-06 13:25:45 +0200
committerjaseg <git@jaseg.de>2021-06-06 13:25:45 +0200
commit5a5ba2b709f01b2100cd767a25a41737541ad53c (patch)
tree6362ca960945e08d4a77b7f059e971e6099217c9 /gerber/excellon_statements.py
parent8bad573131e4c91782425d81a141dd656b622d7b (diff)
parent72257258edf16cbda691483ef1fa722192ac0d38 (diff)
downloadgerbonara-5a5ba2b709f01b2100cd767a25a41737541ad53c.tar.gz
gerbonara-5a5ba2b709f01b2100cd767a25a41737541ad53c.tar.bz2
gerbonara-5a5ba2b709f01b2100cd767a25a41737541ad53c.zip
Graft pcb-tools upstream onto gerbonara tree
Diffstat (limited to 'gerber/excellon_statements.py')
-rw-r--r--gerber/excellon_statements.py979
1 files changed, 979 insertions, 0 deletions
diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py
new file mode 100644
index 0000000..2c50ef9
--- /dev/null
+++ b/gerber/excellon_statements.py
@@ -0,0 +1,979 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Excellon Statements
+====================
+**Excellon file statement classes**
+
+"""
+
+import re
+import uuid
+import itertools
+from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
+ inch, metric)
+
+
+__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
+ 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt',
+ 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt',
+ 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt',
+ 'MeasuringModeStmt', 'RouteModeStmt', 'LinearModeStmt', 'DrillModeStmt',
+ 'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt',
+ 'ExcellonStatement', 'ZAxisRoutPositionStmt',
+ 'RetractWithClampingStmt', 'RetractWithoutClampingStmt',
+ 'CutterCompensationOffStmt', 'CutterCompensationLeftStmt',
+ 'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt',
+ 'NextToolSelectionStmt', 'SlotStmt']
+
+
+class ExcellonStatement(object):
+ """ Excellon Statement abstract base class
+ """
+
+ @classmethod
+ def from_excellon(cls, line):
+ raise NotImplementedError('from_excellon must be implemented in a '
+ 'subclass')
+
+ def __init__(self, unit='inch', id=None):
+ self.units = unit
+ self.id = uuid.uuid4().int if id is None else id
+
+ def to_excellon(self, settings=None):
+ raise NotImplementedError('to_excellon must be implemented in a '
+ 'subclass')
+
+ def to_inch(self):
+ self.units = 'inch'
+
+ def to_metric(self):
+ self.units = 'metric'
+
+ def offset(self, x_offset=0, y_offset=0):
+ pass
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+
+class ExcellonTool(ExcellonStatement):
+ """ Excellon Tool class
+
+ Parameters
+ ----------
+ settings : FileSettings (dict-like)
+ File-wide settings.
+
+ kwargs : dict-like
+ Tool settings from the excellon statement. Valid keys are:
+ - `diameter` : Tool diameter [expressed in file units]
+ - `rpm` : Tool RPM
+ - `feed_rate` : Z-axis tool feed rate
+ - `retract_rate` : Z-axis tool retraction rate
+ - `max_hit_count` : Number of hits allowed before a tool change
+ - `depth_offset` : Offset of tool depth from tip of tool.
+
+ Attributes
+ ----------
+ number : integer
+ Tool number from the excellon file
+
+ diameter : float
+ Tool diameter in file units
+
+ rpm : float
+ Tool RPM
+
+ feed_rate : float
+ Tool Z-axis feed rate.
+
+ retract_rate : float
+ Tool Z-axis retract rate
+
+ depth_offset : float
+ Offset of depth measurement from tip of tool
+
+ max_hit_count : integer
+ Maximum number of tool hits allowed before a tool change
+
+ hit_count : integer
+ Number of tool hits in excellon file.
+ """
+
+ PLATED_UNKNOWN = None
+ PLATED_YES = 'plated'
+ PLATED_NO = 'nonplated'
+ PLATED_OPTIONAL = 'optional'
+
+ @classmethod
+ def from_tool(cls, tool):
+ args = {}
+
+ args['depth_offset'] = tool.depth_offset
+ args['diameter'] = tool.diameter
+ args['feed_rate'] = tool.feed_rate
+ args['max_hit_count'] = tool.max_hit_count
+ args['number'] = tool.number
+ args['plated'] = tool.plated
+ args['retract_rate'] = tool.retract_rate
+ args['rpm'] = tool.rpm
+
+ return cls(None, **args)
+
+ @classmethod
+ def from_excellon(cls, line, settings, id=None, plated=None):
+ """ Create a Tool from an excellon file tool definition line.
+
+ Parameters
+ ----------
+ line : string
+ Tool definition line from an excellon file.
+
+ settings : FileSettings (dict-like)
+ Excellon file-wide settings
+
+ Returns
+ -------
+ tool : Tool
+ An ExcellonTool representing the tool defined in `line`
+ """
+ commands = pairwise(re.split('([BCFHSTZ])', line)[1:])
+ args = {}
+ args['id'] = id
+ nformat = settings.format
+ zero_suppression = settings.zero_suppression
+ for cmd, val in commands:
+ if cmd == 'B':
+ args['retract_rate'] = parse_gerber_value(val, nformat, zero_suppression)
+ elif cmd == 'C':
+ args['diameter'] = parse_gerber_value(val, nformat, zero_suppression)
+ elif cmd == 'F':
+ args['feed_rate'] = parse_gerber_value(val, nformat, zero_suppression)
+ elif cmd == 'H':
+ args['max_hit_count'] = parse_gerber_value(val, nformat, zero_suppression)
+ elif cmd == 'S':
+ args['rpm'] = 1000 * parse_gerber_value(val, nformat, zero_suppression)
+ elif cmd == 'T':
+ args['number'] = int(val)
+ elif cmd == 'Z':
+ args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression)
+
+ if plated != ExcellonTool.PLATED_UNKNOWN:
+ # Sometimees we can can parse the plating status
+ args['plated'] = plated
+ return cls(settings, **args)
+
+ @classmethod
+ def from_dict(cls, settings, tool_dict):
+ """ Create an ExcellonTool from a dict.
+
+ Parameters
+ ----------
+ settings : FileSettings (dict-like)
+ Excellon File-wide settings
+
+ tool_dict : dict
+ Excellon tool parameters as a dict
+
+ Returns
+ -------
+ tool : ExcellonTool
+ An ExcellonTool initialized with the parameters in tool_dict.
+ """
+ return cls(settings, **tool_dict)
+
+ def __init__(self, settings, **kwargs):
+ if kwargs.get('id') is not None:
+ super(ExcellonTool, self).__init__(id=kwargs.get('id'))
+ self.settings = settings
+ self.number = kwargs.get('number')
+ self.feed_rate = kwargs.get('feed_rate')
+ self.retract_rate = kwargs.get('retract_rate')
+ self.rpm = kwargs.get('rpm')
+ self.diameter = kwargs.get('diameter')
+ self.max_hit_count = kwargs.get('max_hit_count')
+ self.depth_offset = kwargs.get('depth_offset')
+ self.plated = kwargs.get('plated')
+
+ self.hit_count = 0
+
+ def to_excellon(self, settings=None):
+ if self.settings and not settings:
+ settings = self.settings
+ fmt = settings.format
+ zs = settings.zero_suppression
+ stmt = 'T%02d' % self.number
+ if self.retract_rate is not None:
+ stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs)
+ if self.feed_rate is not None:
+ stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs)
+ if self.max_hit_count is not None:
+ stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs)
+ if self.rpm is not None:
+ if self.rpm < 100000.:
+ stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs)
+ else:
+ stmt += 'S%g' % (self.rpm / 1000.)
+ if self.diameter is not None:
+ stmt += 'C%s' % decimal_string(self.diameter, fmt[1], True)
+ if self.depth_offset is not None:
+ stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs)
+ return stmt
+
+ def to_inch(self):
+ if self.settings.units != 'inch':
+ self.settings.units = 'inch'
+ if self.diameter is not None:
+ self.diameter = inch(self.diameter)
+
+ def to_metric(self):
+ if self.settings.units != 'metric':
+ self.settings.units = 'metric'
+ if self.diameter is not None:
+ self.diameter = metric(self.diameter)
+
+ def _hit(self):
+ self.hit_count += 1
+
+ def equivalent(self, other):
+ """
+ Is the other tool equal to this, ignoring the tool number, and other file specified properties
+ """
+
+ if type(self) != type(other):
+ return False
+
+ return (self.diameter == other.diameter
+ and self.feed_rate == other.feed_rate
+ and self.retract_rate == other.retract_rate
+ and self.rpm == other.rpm
+ and self.depth_offset == other.depth_offset
+ and self.max_hit_count == other.max_hit_count
+ and self.plated == other.plated
+ and self.settings.units == other.settings.units)
+
+ def __repr__(self):
+ unit = 'in.' if self.settings.units == 'inch' else 'mm'
+ fmtstr = '<ExcellonTool %%02d: %%%d.%dg%%s dia.>' % self.settings.format
+ return fmtstr % (self.number, self.diameter, unit)
+
+
+class ToolSelectionStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ """ Create a ToolSelectionStmt from an excellon file line.
+
+ Parameters
+ ----------
+ line : string
+ Line from an Excellon file
+
+ Returns
+ -------
+ tool_statement : ToolSelectionStmt
+ ToolSelectionStmt representation of `line.`
+ """
+ line = line[1:]
+ compensation_index = None
+
+ # up to 3 characters for tool number (Frizting uses that)
+ if len(line) <= 3:
+ tool = int(line)
+ else:
+ tool = int(line[:2])
+ compensation_index = int(line[2:])
+
+ return cls(tool, compensation_index, **kwargs)
+
+ def __init__(self, tool, compensation_index=None, **kwargs):
+ super(ToolSelectionStmt, self).__init__(**kwargs)
+ tool = int(tool)
+ compensation_index = (int(compensation_index) if compensation_index
+ is not None else None)
+ self.tool = tool
+ self.compensation_index = compensation_index
+
+ def to_excellon(self, settings=None):
+ stmt = 'T%02d' % self.tool
+ if self.compensation_index is not None:
+ stmt += '%02d' % self.compensation_index
+ return stmt
+
+class NextToolSelectionStmt(ExcellonStatement):
+
+ # TODO the statement exists outside of the context of the file,
+ # so it is imposible to know that it is really the next tool
+
+ def __init__(self, cur_tool, next_tool, **kwargs):
+ """
+ Select the next tool in the wheel.
+ Parameters
+ ----------
+ cur_tool : the tool that is currently selected
+ next_tool : the that that is now selected
+ """
+ super(NextToolSelectionStmt, self).__init__(**kwargs)
+
+ self.cur_tool = cur_tool
+ self.next_tool = next_tool
+
+ def to_excellon(self, settings=None):
+ stmt = 'M00'
+ return stmt
+
+class ZAxisInfeedRateStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ """ Create a ZAxisInfeedRate from an excellon file line.
+
+ Parameters
+ ----------
+ line : string
+ Line from an Excellon file
+
+ Returns
+ -------
+ z_axis_infeed_rate : ToolSelectionStmt
+ ToolSelectionStmt representation of `line.`
+ """
+ rate = int(line[1:])
+
+ return cls(rate, **kwargs)
+
+ def __init__(self, rate, **kwargs):
+ super(ZAxisInfeedRateStmt, self).__init__(**kwargs)
+ self.rate = rate
+
+ def to_excellon(self, settings=None):
+ return 'F%02d' % self.rate
+
+
+class CoordinateStmt(ExcellonStatement):
+
+ @classmethod
+ def from_point(cls, point, mode=None):
+
+ stmt = cls(point[0], point[1])
+ if mode:
+ stmt.mode = mode
+ return stmt
+
+ @classmethod
+ def from_excellon(cls, line, settings, **kwargs):
+ x_coord = None
+ y_coord = None
+ if line[0] == 'X':
+ splitline = line.strip('X').split('Y')
+ x_coord = parse_gerber_value(splitline[0], settings.format,
+ settings.zero_suppression)
+ if len(splitline) == 2:
+ y_coord = parse_gerber_value(splitline[1], settings.format,
+ settings.zero_suppression)
+ else:
+ y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
+ settings.zero_suppression)
+ c = cls(x_coord, y_coord, **kwargs)
+ c.units = settings.units
+ return c
+
+ def __init__(self, x=None, y=None, **kwargs):
+ super(CoordinateStmt, self).__init__(**kwargs)
+ self.x = x
+ self.y = y
+ self.mode = None
+
+ def to_excellon(self, settings):
+ stmt = ''
+ if self.mode == "ROUT":
+ stmt += "G00"
+ if self.mode == "LINEAR":
+ stmt += "G01"
+ 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)
+ return stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.x is not None:
+ self.x = inch(self.x)
+ if self.y is not None:
+ self.y = inch(self.y)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.x is not None:
+ self.x = metric(self.x)
+ if self.y is not None:
+ self.y = metric(self.y)
+
+ def offset(self, x_offset=0, y_offset=0):
+ if self.x is not None:
+ self.x += x_offset
+ if self.y is not None:
+ self.y += y_offset
+
+ 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
+
+ return '<Coordinate Statement: %s>' % coord_str
+
+
+class RepeatHoleStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, settings, **kwargs):
+ match = re.compile(r'R(?P<rcount>[0-9]*)X?(?P<xdelta>[+\-]?\d*\.?\d*)?Y?'
+ '(?P<ydelta>[+\-]?\d*\.?\d*)?').match(line)
+ stmt = match.groupdict()
+ count = int(stmt['rcount'])
+ xdelta = (parse_gerber_value(stmt['xdelta'], settings.format,
+ settings.zero_suppression)
+ if stmt['xdelta'] is not '' else None)
+ ydelta = (parse_gerber_value(stmt['ydelta'], settings.format,
+ settings.zero_suppression)
+ if stmt['ydelta'] is not '' else None)
+ c = cls(count, xdelta, ydelta, **kwargs)
+ c.units = settings.units
+ return c
+
+ def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs):
+ super(RepeatHoleStmt, self).__init__(**kwargs)
+ self.count = count
+ self.xdelta = xdelta
+ self.ydelta = ydelta
+
+ def to_excellon(self, settings):
+ stmt = 'R%d' % self.count
+ if self.xdelta is not None and self.xdelta != 0.0:
+ stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format,
+ settings.zero_suppression)
+ if self.ydelta is not None and self.ydelta != 0.0:
+ stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format,
+ settings.zero_suppression)
+ return stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.xdelta is not None:
+ self.xdelta = inch(self.xdelta)
+ if self.ydelta is not None:
+ self.ydelta = inch(self.ydelta)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.xdelta is not None:
+ self.xdelta = metric(self.xdelta)
+ if self.ydelta is not None:
+ self.ydelta = metric(self.ydelta)
+
+ def __str__(self):
+ return '<Repeat Hole: %d times, offset X: %g Y: %g>' % (
+ self.count,
+ self.xdelta if self.xdelta is not None else 0,
+ self.ydelta if self.ydelta is not None else 0)
+
+
+class CommentStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ return cls(line.lstrip(';'))
+
+ def __init__(self, comment, **kwargs):
+ super(CommentStmt, self).__init__(**kwargs)
+ self.comment = comment
+
+ def to_excellon(self, settings=None):
+ return ';%s' % self.comment
+
+
+class HeaderBeginStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(HeaderBeginStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'M48'
+
+
+class HeaderEndStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(HeaderEndStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'M95'
+
+
+class RewindStopStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(RewindStopStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return '%'
+
+
+class ZAxisRoutPositionStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(ZAxisRoutPositionStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'M15'
+
+
+class RetractWithClampingStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(RetractWithClampingStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'M16'
+
+
+class RetractWithoutClampingStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(RetractWithoutClampingStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'M17'
+
+
+class CutterCompensationOffStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(CutterCompensationOffStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G40'
+
+
+class CutterCompensationLeftStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(CutterCompensationLeftStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G41'
+
+
+class CutterCompensationRightStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(CutterCompensationRightStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G42'
+
+
+class EndOfProgramStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, settings, **kwargs):
+ match = re.compile(r'M30X?(?P<x>\d*\.?\d*)?Y?'
+ '(?P<y>\d*\.?\d*)?').match(line)
+ stmt = match.groupdict()
+ x = (parse_gerber_value(stmt['x'], settings.format,
+ settings.zero_suppression)
+ if stmt['x'] is not '' else None)
+ y = (parse_gerber_value(stmt['y'], settings.format,
+ settings.zero_suppression)
+ if stmt['y'] is not '' else None)
+ c = cls(x, y, **kwargs)
+ c.units = settings.units
+ return c
+
+ def __init__(self, x=None, y=None, **kwargs):
+ super(EndOfProgramStmt, self).__init__(**kwargs)
+ self.x = x
+ self.y = y
+
+ def to_excellon(self, settings=None):
+ stmt = 'M30'
+ if self.x is not None:
+ stmt += 'X%s' % write_gerber_value(self.x)
+ if self.y is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y)
+ return stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.x is not None:
+ self.x = inch(self.x)
+ if self.y is not None:
+ self.y = inch(self.y)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.x is not None:
+ self.x = metric(self.x)
+ if self.y is not None:
+ self.y = metric(self.y)
+
+ def offset(self, x_offset=0, y_offset=0):
+ if self.x is not None:
+ self.x += x_offset
+ if self.y is not None:
+ self.y += y_offset
+
+
+class UnitStmt(ExcellonStatement):
+
+ @classmethod
+ def from_settings(cls, settings):
+ """Create the unit statement from the FileSettings"""
+
+ return cls(settings.units, settings.zeros)
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ units = 'inch' if 'INCH' in line else 'metric'
+ zeros = 'leading' if 'LZ' in line else 'trailing'
+ if '0000.00' in line:
+ format = (4, 2)
+ elif '000.000' in line:
+ format = (3, 3)
+ elif '00.0000' in line:
+ format = (2, 4)
+ else:
+ format = None
+ return cls(units, zeros, format, **kwargs)
+
+ def __init__(self, units='inch', zeros='leading', format=None, **kwargs):
+ super(UnitStmt, self).__init__(**kwargs)
+ self.units = units.lower()
+ self.zeros = zeros
+ self.format = format
+
+ def to_excellon(self, settings=None):
+ # TODO This won't export the invalid format statement if it exists
+ stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
+ 'LZ' if self.zeros == 'leading'
+ else 'TZ')
+ return stmt
+
+ def to_inch(self):
+ self.units = 'inch'
+
+ def to_metric(self):
+ self.units = 'metric'
+
+
+class IncrementalModeStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs)
+
+ def __init__(self, mode='off', **kwargs):
+ super(IncrementalModeStmt, self).__init__(**kwargs)
+ if mode.lower() not in ['on', 'off']:
+ raise ValueError('Mode may be "on" or "off"')
+ self.mode = mode
+
+ def to_excellon(self, settings=None):
+ return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON')
+
+
+class VersionStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ version = int(line.split(',')[1])
+ return cls(version, **kwargs)
+
+ def __init__(self, version=1, **kwargs):
+ super(VersionStmt, self).__init__(**kwargs)
+ version = int(version)
+ if version not in [1, 2]:
+ raise ValueError('Valid versions are 1 or 2')
+ self.version = version
+
+ def to_excellon(self, settings=None):
+ return 'VER,%d' % self.version
+
+
+class FormatStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ fmt = int(line.split(',')[1])
+ return cls(fmt, **kwargs)
+
+ def __init__(self, format=1, **kwargs):
+ super(FormatStmt, self).__init__(**kwargs)
+ format = int(format)
+ if format not in [1, 2]:
+ raise ValueError('Valid formats are 1 or 2')
+ self.format = format
+
+ def to_excellon(self, settings=None):
+ return 'FMAT,%d' % self.format
+
+ @property
+ def format_tuple(self):
+ return (self.format, 6 - self.format)
+
+
+class LinkToolStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ linked = [int(tool) for tool in line.split('/')]
+ return cls(linked, **kwargs)
+
+ def __init__(self, linked_tools, **kwargs):
+ super(LinkToolStmt, self).__init__(**kwargs)
+ self.linked_tools = [int(x) for x in linked_tools]
+
+ def to_excellon(self, settings=None):
+ return '/'.join([str(x) for x in self.linked_tools])
+
+
+class MeasuringModeStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ if not ('M71' in line or 'M72' in line):
+ raise ValueError('Not a measuring mode statement')
+ return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs)
+
+ def __init__(self, units='inch', **kwargs):
+ super(MeasuringModeStmt, self).__init__(**kwargs)
+ units = units.lower()
+ if units not in ['inch', 'metric']:
+ raise ValueError('units must be "inch" or "metric"')
+ self.units = units
+
+ def to_excellon(self, settings=None):
+ return 'M72' if self.units == 'inch' else 'M71'
+
+ def to_inch(self):
+ self.units = 'inch'
+
+ def to_metric(self):
+ self.units = 'metric'
+
+
+class RouteModeStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(RouteModeStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G00'
+
+
+class LinearModeStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(LinearModeStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G01'
+
+
+class DrillModeStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(DrillModeStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G05'
+
+
+class AbsoluteModeStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(AbsoluteModeStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G90'
+
+
+class UnknownStmt(ExcellonStatement):
+
+ @classmethod
+ def from_excellon(cls, line, **kwargs):
+ return cls(line, **kwargs)
+
+ def __init__(self, stmt, **kwargs):
+ super(UnknownStmt, self).__init__(**kwargs)
+ self.stmt = stmt
+
+ def to_excellon(self, settings=None):
+ return self.stmt
+
+ def __str__(self):
+ return "<Unknown Statement: %s>" % self.stmt
+
+
+class SlotStmt(ExcellonStatement):
+ """
+ G85 statement. Defines a slot created by multiple drills between two specified points.
+
+ Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
+ """
+
+ @classmethod
+ def from_points(cls, start, end):
+
+ return cls(start[0], start[1], end[0], end[1])
+
+ @classmethod
+ def from_excellon(cls, line, settings, **kwargs):
+ # Split the line based on the G85 separator
+ sub_coords = line.split('G85')
+ (x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
+ (x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
+
+ # Some files seem to specify only one of the coordinates
+ if x_end_coord == None:
+ x_end_coord = x_start_coord
+ if y_end_coord == None:
+ y_end_coord = y_start_coord
+
+ c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
+ c.units = settings.units
+ return c
+
+ @staticmethod
+ def parse_sub_coords(line, settings):
+
+ x_coord = None
+ y_coord = None
+
+ if line[0] == 'X':
+ splitline = line.strip('X').split('Y')
+ x_coord = parse_gerber_value(splitline[0], settings.format,
+ settings.zero_suppression)
+ if len(splitline) == 2:
+ y_coord = parse_gerber_value(splitline[1], settings.format,
+ settings.zero_suppression)
+ else:
+ y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
+ settings.zero_suppression)
+
+ return (x_coord, y_coord)
+
+
+ def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs):
+ super(SlotStmt, self).__init__(**kwargs)
+ self.x_start = x_start
+ self.y_start = y_start
+ self.x_end = x_end
+ self.y_end = y_end
+ self.mode = None
+
+ def to_excellon(self, settings):
+ stmt = ''
+
+ if self.x_start is not None:
+ stmt += 'X%s' % write_gerber_value(self.x_start, settings.format,
+ settings.zero_suppression)
+ if self.y_start is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
+ settings.zero_suppression)
+
+ stmt += 'G85'
+
+ if self.x_end is not None:
+ stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
+ settings.zero_suppression)
+ if self.y_end is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
+ settings.zero_suppression)
+
+ return stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.x_start is not None:
+ self.x_start = inch(self.x_start)
+ if self.y_start is not None:
+ self.y_start = inch(self.y_start)
+ if self.x_end is not None:
+ self.x_end = inch(self.x_end)
+ if self.y_end is not None:
+ self.y_end = inch(self.y_end)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.x_start is not None:
+ self.x_start = metric(self.x_start)
+ if self.y_start is not None:
+ self.y_start = metric(self.y_start)
+ if self.x_end is not None:
+ self.x_end = metric(self.x_end)
+ if self.y_end is not None:
+ self.y_end = metric(self.y_end)
+
+ def offset(self, x_offset=0, y_offset=0):
+ if self.x_start is not None:
+ self.x_start += x_offset
+ if self.y_start is not None:
+ self.y_start += y_offset
+ if self.x_end is not None:
+ self.x_end += x_offset
+ if self.y_end is not None:
+ self.y_end += y_offset
+
+ def __str__(self):
+ start_str = ''
+ if self.x_start is not None:
+ start_str += 'X: %g ' % self.x_start
+ if self.y_start is not None:
+ start_str += 'Y: %g ' % self.y_start
+
+ end_str = ''
+ if self.x_end is not None:
+ end_str += 'X: %g ' % self.x_end
+ if self.y_end is not None:
+ end_str += 'Y: %g ' % self.y_end
+
+ return '<Slot Statement: %s to %s>' % (start_str, end_str)
+
+def pairwise(iterator):
+ """ Iterate over list taking two elements at a time.
+
+ e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)]
+ """
+ a, b = itertools.tee(iterator)
+ itr = zip(itertools.islice(a, 0, None, 2), itertools.islice(b, 1, None, 2))
+ for elem in itr:
+ yield elem