diff --git a/gerber/cnc.py b/gerber/cnc.py new file mode 100644 index 0000000..a7f3b85 --- /dev/null +++ b/gerber/cnc.py @@ -0,0 +1,117 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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. +""" +gerber.cnc +============ +**CNC file classes** + +This module provides common base classes for Excellon/Gerber CNC files +""" + + +class FileSettings(object): + """ CNC File Settings + + Provides a common representation of gerber/excellon file settings + """ + def __init__(self, notation='absolute', units='inch', + zero_suppression='trailing', format=(2,5)): + if notation not in ['absolute', 'incremental']: + raise ValueError('Notation must be either absolute or incremental') + self.notation = notation + + if units not in ['inch', 'metric']: + raise ValueError('Units must be either inch or metric') + self.units = units + + if zero_suppression not in ['leading', 'trailing']: + raise ValueError('Zero suppression must be either leading or \ + trailling') + self.zero_suppression = zero_suppression + + if len(format) != 2: + raise ValueError('Format must be a tuple(n=2) of integers') + self.format = format + + def __getitem__(self, key): + if key == 'notation': + return self.notation + elif key == 'units': + return self.units + elif key =='zero_suppression': + return self.zero_suppression + elif key == 'format': + return self.format + else: + raise KeyError() + +class CncFile(object): + """ Base class for Gerber/Excellon files. + + Provides a common set of settings parameters. + + Parameters + ---------- + settings : FileSettings + The current file configuration. + + filename : string + Name of the file that this CncFile represents. + + Attributes + ---------- + settings : FileSettings + File settings as a FileSettings object + + notation : string + File notation setting. May be either 'absolute' or 'incremental' + + units : string + File units setting. May be 'inch' or 'metric' + + zero_suppression : string + File zero-suppression setting. May be either 'leading' or 'trailling' + + format : tuple (, ) + File decimal representation format as a tuple of (integer digits, + decimal digits) + """ + + def __init__(self, settings=None, filename=None): + if settings is not None: + self.notation = settings['notation'] + self.units = settings['units'] + self.zero_suppression = settings['zero_suppression'] + self.format = settings['format'] + else: + self.notation = 'absolute' + self.units = 'inch' + self.zero_suppression = 'trailing' + self.format = (2,5) + self.filename = filename + + @property + def settings(self): + """ File settings + + Returns + ------- + settings : FileSettings (dict-like) + A FileSettings object with the specified configuration. + """ + return FileSettings(self.notation, self.units, self.zero_suppression, + self.format) diff --git a/gerber/excellon.py b/gerber/excellon.py index d92d57c..5cb33ad 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -2,6 +2,7 @@ import re from itertools import tee, izip from .utils import parse_gerber_value +from .cnc import CncFile, FileSettings def read(filename): @@ -10,7 +11,7 @@ def read(filename): return ExcellonParser().parse(filename) -class ExcellonFile(object): +class ExcellonFile(CncFile): """ A class representing a single excellon file The ExcellonFile class represents a single excellon file. @@ -34,11 +35,10 @@ class ExcellonFile(object): either 'inch' or 'metric'. """ - def __init__(self, tools, hits, settings, filename): + def __init__(self, tools, hits, settings, filename=None): + super(ExcellonFile, self).__init__(settings, filename) self.tools = tools self.hits = hits - self.settings = settings - self.filename = filename def report(self): """ Print drill report @@ -53,11 +53,67 @@ class ExcellonFile(object): ctx.dump(filename) -class Tool(object): +class ExcellonTool(object): """ 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. """ + @classmethod def from_line(cls, line, settings): + """ Create a Tool from an excellon gile 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 = re.split('([BCFHSTZ])', line)[1:] commands = [(command, value) for command, value in pairwise(commands)] args = {} @@ -89,13 +145,19 @@ class Tool(object): self.max_hit_count = kwargs.get('max_hit_count') self.depth_offset = kwargs.get('depth_offset') self.units = settings.get('units', 'inch') + self.hit_count = 0 + + def _hit(self): + self.hit_count += 1 def __repr__(self): unit = 'in.' if self.units == 'inch' else 'mm' - return '' % (self.number, self.diameter, unit) + return '' % (self.number, self.diameter, unit) class ExcellonParser(object): + """ Excellon File Parser + """ def __init__(self, ctx=None): self.ctx = ctx self.notation = 'absolute' @@ -115,13 +177,11 @@ class ExcellonParser(object): with open(filename, 'r') as f: for line in f: self._parse(line) - settings = {'notation': self.notation, 'units': self.units, - 'zero_suppression': self.zero_suppression, - 'format': self.format} - return ExcellonFile(self.tools, self.hits, settings, filename) + return ExcellonFile(self.tools, self.hits, self._settings(), filename) def dump(self, filename): - self.ctx.dump(filename) + if self.ctx is not None: + self.ctx.dump(filename) def _parse(self, line): if 'M48' in line: @@ -159,7 +219,7 @@ class ExcellonParser(object): # tool definition if line[0] == 'T' and self.state == 'HEADER': - tool = Tool.from_line(line, self._settings()) + tool = ExcellonTool.from_line(line, self._settings()) self.tools[tool.number] = tool elif line[0] == 'T' and self.state != 'HEADER': @@ -187,13 +247,16 @@ class ExcellonParser(object): self.pos[1] += y if self.state == 'DRILL': self.hits.append((self.active_tool, self.pos)) + self.active_tool._hit() if self.ctx is not None: self.ctx.drill(self.pos[0], self.pos[1], self.active_tool.diameter) def _settings(self): - return {'units': self.units, 'zero_suppression': self.zero_suppression, - 'format': self.format} + return FileSettings(units=self.units, format=self.format, + zero_suppression=self.zero_suppression, + notation=self.notation) + def pairwise(iterator): diff --git a/gerber/gerber.py b/gerber/gerber.py index 949037b..eb5821c 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -27,6 +27,9 @@ This module provides an RS-274-X class and parser import re import json from .statements import * +from .cnc import CncFile, FileSettings + + def read(filename): @@ -35,7 +38,7 @@ def read(filename): return GerberParser().parse(filename) -class GerberFile(object): +class GerberFile(CncFile): """ A class representing a single gerber file The GerberFile class represents a single gerber file. @@ -68,9 +71,8 @@ class GerberFile(object): """ def __init__(self, statements, settings, filename=None): - self.filename = filename + super(GerberFile, self).__init__(settings, filename) self.statements = statements - self.settings = settings @property def comments(self): @@ -90,7 +92,8 @@ class GerberFile(object): def bounds(self): xbounds = [0.0, 0.0] ybounds = [0.0, 0.0] - for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: + for stmt in [stmt for stmt in self.statements + if isinstance(stmt, CoordStmt)]: if stmt.x is not None and stmt.x < xbounds[0]: xbounds[0] = stmt.x if stmt.x is not None and stmt.x > xbounds[1]: @@ -169,7 +172,7 @@ class GerberParser(object): EOF_STMT = re.compile(r"(?PM02)\*") def __init__(self): - self.settings = {} + self.settings = FileSettings() self.statements = [] def parse(self, filename): @@ -240,13 +243,13 @@ class GerberParser(object): if param: if param["param"] == "FS": stmt = FSParamStmt.from_dict(param) - self.settings = {'zero_suppression': stmt.zero_suppression, - 'format': stmt.format, - 'notation': stmt.notation} + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation yield stmt elif param["param"] == "MO": stmt = MOParamStmt.from_dict(param) - self.settings['units'] = stmt.mode + self.settings.units = stmt.mode yield stmt elif param["param"] == "IP": yield IPParamStmt.from_dict(param)