#! /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. """ CAM File ============ **AM file classes** This module provides common base classes for Excellon/Gerber CNC files """ class FileSettings(object): """ CAM File Settings Provides a common representation of gerber/excellon file settings Parameters ---------- notation: string notation format. either 'absolute' or 'incremental' units : string Measurement units. 'inch' or 'metric' zero_suppression: string 'leading' to suppress leading zeros, 'trailing' to suppress trailing zeros. This is the convention used in Gerber files. format : tuple (int, int) Decimal format zeros : string 'leading' to include leading zeros, 'trailing to include trailing zeros. This is the convention used in Excellon files Notes ----- Either `zeros` or `zero_suppression` should be specified, there is no need to specify both. `zero_suppression` will take on the opposite value of `zeros` and vice versa """ def __init__(self, notation='absolute', units='inch', zero_suppression=None, format=(2, 5), zeros=None, angle_units='degrees'): 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 is None and zeros is None: self.zero_suppression = 'trailing' elif zero_suppression == zeros: raise ValueError('Zeros and Zero Suppression must be different. \ Best practice is to specify only one.') elif zero_suppression is not None: if zero_suppression not in ['leading', 'trailing']: raise ValueError('Zero suppression must be either leading or \ trailling') self.zero_suppression = zero_suppression elif zeros is not None: if zeros not in ['leading', 'trailing']: raise ValueError('Zeros must be either leading or trailling') self.zeros = zeros if len(format) != 2: raise ValueError('Format must be a tuple(n=2) of integers') self.format = format if angle_units not in ('degrees', 'radians'): raise ValueError('Angle units may be degrees or radians') self.angle_units = angle_units @property def zero_suppression(self): return self._zero_suppression @zero_suppression.setter def zero_suppression(self, value): self._zero_suppression = value self._zeros = 'leading' if value == 'trailing' else 'trailing' @property def zeros(self): return self._zeros @zeros.setter def zeros(self, value): self._zeros = value self._zero_suppression = 'leading' if value == 'trailing' else 'trailing' 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 == 'zeros': return self.zeros elif key == 'format': return self.format elif key == 'angle_units': return self.angle_units else: raise KeyError() def __setitem__(self, key, value): if key == 'notation': if value not in ['absolute', 'incremental']: raise ValueError('Notation must be either \ absolute or incremental') self.notation = value elif key == 'units': if value not in ['inch', 'metric']: raise ValueError('Units must be either inch or metric') self.units = value elif key == 'zero_suppression': if value not in ['leading', 'trailing']: raise ValueError('Zero suppression must be either leading or \ trailling') self.zero_suppression = value elif key == 'zeros': if value not in ['leading', 'trailing']: raise ValueError('Zeros must be either leading or trailling') self.zeros = value elif key == 'format': if len(value) != 2: raise ValueError('Format must be a tuple(n=2) of integers') self.format = value elif key == 'angle_units': if value not in ('degrees', 'radians'): raise ValueError('Angle units may be degrees or radians') self.angle_units = value else: raise KeyError('%s is not a valid key' % key) def __eq__(self, other): return (self.notation == other.notation and self.units == other.units and self.zero_suppression == other.zero_suppression and self.format == other.format and self.angle_units == other.angle_units) class CamFile(object): """ Base class for Gerber/Excellon files. Provides a common set of settings parameters. Parameters ---------- settings : FileSettings The current file configuration. primitives : iterable List of primitives in the file. filename : string Name of the file that this CamFile represents. layer_name : string Name of the PCB layer that the file 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, statements=None, settings=None, primitives=None, filename=None, layer_name=None): if settings is not None: self.notation = settings['notation'] self.units = settings['units'] self.zero_suppression = settings['zero_suppression'] self.zeros = settings['zeros'] self.format = settings['format'] else: self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' self.zeros = 'leading' self.format = (2, 5) self.statements = statements if statements is not None else [] self.primitives = primitives self.filename = filename self.layer_name = layer_name @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) @property def bounds(self): """ File boundaries """ pass def render(self, ctx, filename=None): """ Generate image of layer. Parameters ---------- ctx : :class:`GerberContext` GerberContext subclass used for rendering the image filename : string If provided, save the rendered image to `filename` """ bounds = [tuple([x * 1.2, y*1.2]) for x, y in self.bounds] ctx.set_bounds(bounds) for p in self.primitives: ctx.render(p) if filename is not None: ctx.dump(filename)