From 288ac27084b47166ac662402ea340d0aa25d8f56 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 04:31:23 -0500 Subject: Get unit conversion working for Gerber/Excellon files Started operations module for file operations/transforms --- gerber/am_statements.py | 15 +- gerber/cam.py | 4 +- gerber/excellon.py | 29 +++- gerber/excellon_statements.py | 102 +++++++++++--- gerber/gerber_statements.py | 62 ++++++-- gerber/operations.py | 120 ++++++++++++++++ gerber/primitives.py | 186 ++++++++++++++++++++---- gerber/rs274x.py | 18 ++- gerber/tests/test_am_statements.py | 5 + gerber/tests/test_cam.py | 6 +- gerber/tests/test_excellon.py | 26 +++- gerber/tests/test_excellon_statements.py | 72 +++++++++- gerber/tests/test_gerber_statements.py | 45 +++++- gerber/tests/test_primitives.py | 235 +++++++++++++++++++++++++++++-- gerber/tests/test_rs274x.py | 20 ++- gerber/utils.py | 8 ++ 16 files changed, 859 insertions(+), 94 deletions(-) create mode 100644 gerber/operations.py diff --git a/gerber/am_statements.py b/gerber/am_statements.py index dc97dfa..bdb12dd 100644 --- a/gerber/am_statements.py +++ b/gerber/am_statements.py @@ -16,7 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .utils import validate_coordinates +from .utils import validate_coordinates, inch, metric # TODO: Add support for aperture macro variables @@ -26,12 +26,6 @@ __all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive', 'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive', 'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive'] -def metric(value): - return value * 25.4 - -def inch(value): - return value / 25.4 - class AMPrimitive(object): """ Aperture Macro Primitive Base Class @@ -58,7 +52,7 @@ class AMPrimitive(object): TypeError, ValueError """ def __init__(self, code, exposure=None): - VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22) + VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22, 9999) if not isinstance(code, int): raise TypeError('Aperture Macro Primitive code must be an integer') elif code not in VALID_CODES: @@ -74,6 +68,8 @@ class AMPrimitive(object): def to_metric(self): raise NotImplementedError('Subclass must implement `to-metric`') + def __eq__(self, other): + return self.__dict__ == other.__dict__ class AMCommentPrimitive(AMPrimitive): """ Aperture Macro Comment primitive. Code 0 @@ -818,11 +814,12 @@ class AMUnsupportPrimitive(AMPrimitive): return cls(primitive) def __init__(self, primitive): + super(AMUnsupportPrimitive, self).__init__(9999) self.primitive = primitive def to_inch(self): pass - + def to_metric(self): pass diff --git a/gerber/cam.py b/gerber/cam.py index caca517..243070d 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -225,9 +225,9 @@ class CamFile(object): @property def bounds(self): - """ File baundaries + """ File boundaries """ - raise NotImplementedError('bounds must be implemented in a subclass') + pass def render(self, ctx, filename=None): """ Generate image of layer. diff --git a/gerber/excellon.py b/gerber/excellon.py index a7f3a27..a339827 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -23,12 +23,12 @@ Excellon File module This module provides Excellon file classes and parsing utilities """ +import math from .excellon_statements import * from .cam import CamFile, FileSettings from .primitives import Drill -import math -import re + def read(filename): """ Read data from filename and return an ExcellonFile @@ -122,6 +122,31 @@ class ExcellonFile(CamFile): for statement in self.statements: f.write(statement.to_excellon(self.settings) + '\n') + def to_inch(self): + """ + Convert units to inches + """ + if self.units != 'inch': + self.units = 'inch' + for statement in self.statements: + statement.to_inch() + for tool in self.tools.itervalues(): + tool.to_inch() + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + """ Convert units to metric + """ + if self.units != 'metric': + self.units = 'metric' + for statement in self.statements: + statement.to_metric() + for tool in self.tools.itervalues(): + tool.to_metric() + for primitive in self.primitives: + primitive.to_metric() + class ExcellonParser(object): """ Excellon File Parser diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 7e2772c..99f7d46 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -21,16 +21,19 @@ Excellon Statements """ -from .utils import parse_gerber_value, write_gerber_value, decimal_string import re +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', 'DrillModeStmt', 'AbsoluteModeStmt', - 'RepeatHoleStmt', 'UnknownStmt', 'ExcellonStatement' - ] + 'MeasuringModeStmt', 'RouteModeStmt', 'DrillModeStmt', + 'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt', + 'ExcellonStatement',] class ExcellonStatement(object): @@ -38,11 +41,21 @@ class ExcellonStatement(object): """ @classmethod def from_excellon(cls, line): - raise NotImplementedError('`from_excellon` must be implemented in a subclass') + raise NotImplementedError('from_excellon must be implemented in a ' + 'subclass') def to_excellon(self, settings=None): - raise NotImplementedError('`to_excellon` must be implemented in a subclass') + raise NotImplementedError('to_excellon must be implemented in a ' + 'subclass') + + def to_inch(self): + pass + + def to_metric(self): + pass + def __eq__(self, other): + return self.__dict__ == other.__dict__ class ExcellonTool(ExcellonStatement): """ Excellon Tool class @@ -179,12 +192,17 @@ class ExcellonTool(ExcellonStatement): return stmt def to_inch(self): - if self.diameter is not None: - self.diameter = self.diameter / 25.4 + 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.diameter is not None: - self.diameter = self.diameter * 25.4 + 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 @@ -240,11 +258,14 @@ class CoordinateStmt(ExcellonStatement): 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) + 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) + 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) + y_coord = parse_gerber_value(line.strip(' Y'), settings.format, + settings.zero_suppression) return cls(x_coord, y_coord) def __init__(self, x=None, y=None): @@ -254,22 +275,24 @@ class CoordinateStmt(ExcellonStatement): 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) + 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) + stmt += 'Y%s' % write_gerber_value(self.y, settings.format, + settings.zero_suppression) return stmt def to_inch(self): if self.x is not None: - self.x = self.x / 25.4 + self.x = inch(self.x) if self.y is not None: - self.y = self.y / 25.4 + self.y = inch(self.y) def to_metric(self): if self.x is not None: - self.x = self.x * 25.4 + self.x = metric(self.x) if self.y is not None: - self.y = self.y * 25.4 + self.y = metric(self.y) def __str__(self): coord_str = '' @@ -285,7 +308,8 @@ class RepeatHoleStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, settings): - match = re.compile(r'R(?P[0-9]*)X?(?P\d*\.?\d*)?Y?(?P\d*\.?\d*)?').match(line) + match = re.compile(r'R(?P[0-9]*)X?(?P\d*\.?\d*)?Y?' + '(?P\d*\.?\d*)?').match(line) stmt = match.groupdict() count = int(stmt['rcount']) xdelta = (parse_gerber_value(stmt['xdelta'], settings.format, @@ -304,11 +328,21 @@ class RepeatHoleStmt(ExcellonStatement): def to_excellon(self, settings): stmt = 'R%d' % self.count if self.xdelta != 0.0: - stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format, settings.zero_suppression) + stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format, + settings.zero_suppression) if self.ydelta != 0.0: - stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format, settings.zero_suppression) + stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format, + settings.zero_suppression) return stmt + def to_inch(self): + self.xdelta = inch(self.xdelta) + self.ydelta = inch(self.ydelta) + + def to_metric(self): + self.xdelta = metric(self.xdelta) + self.ydelta = metric(self.ydelta) + def __str__(self): return '' % self.count @@ -357,7 +391,8 @@ class EndOfProgramStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, settings): - match = re.compile(r'M30X?(?P\d*\.?\d*)?Y?(?P\d*\.?\d*)?').match(line) + match = re.compile(r'M30X?(?P\d*\.?\d*)?Y?' + '(?P\d*\.?\d*)?').match(line) stmt = match.groupdict() x = (parse_gerber_value(stmt['x'], settings.format, settings.zero_suppression) @@ -379,6 +414,17 @@ class EndOfProgramStmt(ExcellonStatement): stmt += 'Y%s' % write_gerber_value(self.y) return stmt + def to_inch(self): + 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.x is not None: + self.x = metric(self.x) + if self.y is not None: + self.y = metric(self.y) class UnitStmt(ExcellonStatement): @@ -398,6 +444,11 @@ class UnitStmt(ExcellonStatement): else 'TZ') return stmt + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' class IncrementalModeStmt(ExcellonStatement): @@ -479,6 +530,11 @@ class MeasuringModeStmt(ExcellonStatement): 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): diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index a6feef6..b231cdb 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -20,7 +20,8 @@ Gerber (RS-274X) Statements **Gerber RS-274X file statement classes** """ -from .utils import parse_gerber_value, write_gerber_value, decimal_string +from .utils import (parse_gerber_value, write_gerber_value, decimal_string, + inch, metric) from .am_statements import * @@ -51,6 +52,15 @@ class Statement(object): s = s.rstrip() + ">" return s + def to_inch(self): + pass + + def to_metric(self): + pass + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + class ParamStmt(Statement): """ Gerber parameter statement Base class @@ -180,6 +190,12 @@ class MOParamStmt(ParamStmt): mode = 'MM' if self.mode == 'metric' else 'IN' return '%MO{0}*%'.format(mode) + def to_inch(self): + self.mode = 'inch' + + def to_metric(self): + self.mode = 'metric' + def __str__(self): mode_str = 'millimeters' if self.mode == 'metric' else 'inches' return ('' % mode_str) @@ -267,10 +283,10 @@ class ADParamStmt(ParamStmt): self.modifiers = [] def to_inch(self): - self.modifiers = [tuple([x / 25.4 for x in modifier]) for modifier in self.modifiers] + self.modifiers = [tuple([inch(x) for x in modifier]) for modifier in self.modifiers] def to_metric(self): - self.modifiers = [tuple([x * 25.4 for x in modifier]) for modifier in self.modifiers] + self.modifiers = [tuple([metric(x) for x in modifier]) for modifier in self.modifiers] def to_gerber(self, settings=None): if len(self.modifiers): @@ -599,6 +615,18 @@ class OFParamStmt(ParamStmt): ret += 'B' + decimal_string(self.b, precision=5) return ret + '*%' + def to_inch(self): + if self.a is not None: + self.a = inch(self.a) + if self.b is not None: + self.b = inch(self.b) + + def to_metric(self): + if self.a is not None: + self.a = metric(self.a) + if self.b is not None: + self.b = metric(self.b) + def __str__(self): offset_str = '' if self.a is not None: @@ -651,6 +679,18 @@ class SFParamStmt(ParamStmt): ret += 'B' + decimal_string(self.b, precision=5) return ret + '*%' + def to_inch(self): + if self.a is not None: + self.a = inch(self.a) + if self.b is not None: + self.b = inch(self.b) + + def to_metric(self): + if self.a is not None: + self.a = metric(self.a) + if self.b is not None: + self.b = metric(self.b) + def __str__(self): scale_factor = '' if self.a is not None: @@ -775,25 +815,25 @@ class CoordStmt(Statement): def to_inch(self): if self.x is not None: - self.x = self.x / 25.4 + self.x = inch(self.x) if self.y is not None: - self.y = self.y / 25.4 + self.y = inch(self.y) if self.i is not None: - self.i = self.i / 25.4 + self.i = inch(self.i) if self.j is not None: - self.j = self.j / 25.4 + self.j = inch(self.j) if self.function == "G71": self.function = "G70" def to_metric(self): if self.x is not None: - self.x = self.x * 25.4 + self.x = metric(self.x) if self.y is not None: - self.y = self.y * 25.4 + self.y = metric(self.y) if self.i is not None: - self.i = self.i * 25.4 + self.i = metric(self.i) if self.j is not None: - self.j = self.j * 25.4 + self.j = metric(self.j) if self.function == "G70": self.function = "G71" diff --git a/gerber/operations.py b/gerber/operations.py new file mode 100644 index 0000000..9624a16 --- /dev/null +++ b/gerber/operations.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2015 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 Operations +=================== +**Transformations and other operations performed on Gerber and Excellon files** + +""" +import copy + +def to_inch(cam_file): + """ Convert Gerber or Excellon file units to imperial + + Parameters + ---------- + cam_file : `gerber.cam.CamFile` subclass + Gerber or Excellon file to convert + + Returns + ------- + gerber_file : `gerber.cam.CamFile` subclass + A deep copy of the source file with units converted to imperial. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.to_inch() + return cam_file + +def to_metric(cam_file): + """ Convert Gerber or Excellon file units to metric + + Parameters + ---------- + cam_file : `gerber.cam.CamFile` subclass + Gerber or Excellon file to convert + + Returns + ------- + gerber_file : `gerber.cam.CamFile` subclass + A deep copy of the source file with units converted to metric. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.to_metric() + return cam_file + +def offset(cam_file, x_offset, y_offset): + """ Offset a Cam file by a specified amount in the X and Y directions. + + Parameters + ---------- + cam_file : `gerber.cam.CamFile` subclass + Gerber or Excellon file to offset + + x_offset : float + Amount to offset the file in the X direction + + y_offset : float + Amount to offset the file in the Y direction + + Returns + ------- + gerber_file : `gerber.cam.CamFile` subclass + An offset deep copy of the source file. + """ + # TODO + pass + +def scale(cam_file, x_scale, y_scale): + """ Scale a Cam file by a specified amount in the X and Y directions. + + Parameters + ---------- + cam_file : `gerber.cam.CamFile` subclass + Gerber or Excellon file to scale + + x_scale : float + X-axis scale factor + + y_scale : float + Y-axis scale factor + + Returns + ------- + gerber_file : `gerber.cam.CamFile` subclass + An scaled deep copy of the source file. + """ + # TODO + pass + +def rotate(cam_file, angle): + """ Rotate a Cam file a specified amount about the origin. + + Parameters + ---------- + cam_file : `gerber.cam.CamFile` subclass + Gerber or Excellon file to rotate + + angle : float + Angle to rotate the file in degrees. + + Returns + ------- + gerber_file : `gerber.cam.CamFile` subclass + An rotated deep copy of the source file. + """ + # TODO + pass diff --git a/gerber/primitives.py b/gerber/primitives.py index ffdbea7..cb6e4ea 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -16,7 +16,8 @@ # limitations under the License. import math from operator import sub -from .utils import validate_coordinates + +from .utils import validate_coordinates, inch, metric class Primitive(object): @@ -46,7 +47,11 @@ class Primitive(object): Return ((min x, max x), (min y, max y)) """ - raise NotImplementedError('Bounding box calculation must be implemented in subclass') + raise NotImplementedError('Bounding box calculation must be ' + 'implemented in subclass') + + def __eq__(self, other): + return self.__dict__ == other.__dict__ class Line(Primitive): @@ -91,18 +96,18 @@ class Line(Primitive): # Find all the corners of the start and end position start_ll = (start[0] - (width / 2.), start[1] - (height / 2.)) - start_lr = (start[0] - (width / 2.), - start[1] + (height / 2.)) - start_ul = (start[0] + (width / 2.), + start_lr = (start[0] + (width / 2.), start[1] - (height / 2.)) + start_ul = (start[0] - (width / 2.), + start[1] + (height / 2.)) start_ur = (start[0] + (width / 2.), start[1] + (height / 2.)) end_ll = (end[0] - (width / 2.), end[1] - (height / 2.)) - end_lr = (end[0] - (width / 2.), - end[1] + (height / 2.)) - end_ul = (end[0] + (width / 2.), + end_lr = (end[0] + (width / 2.), end[1] - (height / 2.)) + end_ul = (end[0] - (width / 2.), + end[1] + (height / 2.)) end_ur = (end[0] + (width / 2.), end[1] + (height / 2.)) @@ -124,10 +129,17 @@ class Line(Primitive): return (end_ll, start_lr, start_ur, end_ul) elif end[0] < start[0] and end[1] > start[1]: return (start_ll, start_lr, start_ur, end_ur, end_ul, end_ll) - else: - return None + def to_inch(self): + self.aperture.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + + def to_metric(self): + self.aperture.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) class Arc(Primitive): @@ -206,6 +218,18 @@ class Arc(Primitive): max_y = max(y) + self.aperture.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.aperture.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + self.center = tuple(map(inch, self.center)) + + def to_metric(self): + self.aperture.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + self.center = tuple(map(metric, self.center)) + class Circle(Primitive): """ @@ -228,9 +252,15 @@ class Circle(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) - @property - def stroke_width(self): - return self.diameter + def to_inch(self): + if self.position is not None: + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) + + def to_metric(self): + if self.position is not None: + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) class Ellipse(Primitive): @@ -276,12 +306,12 @@ class Rectangle(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), + return (self.position[0] - (self._abs_width / 2.), self.position[1] - (self._abs_height / 2.)) @property def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -292,6 +322,15 @@ class Rectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) class Diamond(Primitive): @@ -311,12 +350,12 @@ class Diamond(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), + return (self.position[0] - (self._abs_width / 2.), self.position[1] - (self._abs_height / 2.)) @property def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -327,6 +366,16 @@ class Diamond(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + class ChamferRectangle(Primitive): """ @@ -347,12 +396,12 @@ class ChamferRectangle(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), + return (self.position[0] - (self._abs_width / 2.), self.position[1] - (self._abs_height / 2.)) @property def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -363,6 +412,18 @@ class ChamferRectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.chamfer = inch(self.chamfer) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.chamfer = metric(self.chamfer) + class RoundRectangle(Primitive): """ @@ -383,12 +444,12 @@ class RoundRectangle(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), + return (self.position[0] - (self._abs_width / 2.), self.position[1] - (self._abs_height / 2.)) @property def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -399,6 +460,18 @@ class RoundRectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.radius = inch(self.radius) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.radius = metric(self.radius) + class Obround(Primitive): """ @@ -417,12 +490,12 @@ class Obround(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), + return (self.position[0] - (self._abs_width / 2.), self.position[1] - (self._abs_height / 2.)) @property def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -455,6 +528,16 @@ class Obround(Primitive): self.height) return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + class Polygon(Primitive): """ @@ -474,6 +557,14 @@ class Polygon(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.radius = inch(self.radius) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.radius = metric(self.radius) + class Region(Primitive): """ @@ -491,6 +582,12 @@ class Region(Primitive): max_y = max(y_list) return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.points = [tuple(map(inch, point)) for point in self.points] + + def to_metric(self): + self.points = [tuple(map(metric, point)) for point in self.points] + class RoundButterfly(Primitive): """ A circle with two diagonally-opposite quadrants removed @@ -513,6 +610,15 @@ class RoundButterfly(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) + + class SquareButterfly(Primitive): """ A square with two diagonally-opposite quadrants removed """ @@ -531,6 +637,14 @@ class SquareButterfly(Primitive): max_y = self.position[1] + (self.side / 2.) return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.side = inch(self.side) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.side = metric(self.side) + class Donut(Primitive): """ A Shape with an identical concentric shape removed from its center @@ -558,12 +672,12 @@ class Donut(Primitive): @property def lower_left(self): - return (self.position[0] - (self.width / 2.), + return (self.position[0] - (self.width / 2.), self.position[1] - (self.height / 2.)) @property def upper_right(self): - return (self.position[0] + (self.width / 2.), + return (self.position[0] + (self.width / 2.), self.position[1] + (self.height / 2.)) @property @@ -574,6 +688,20 @@ class Donut(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.inner_diameter = inch(self.inner_diameter) + self.outer_diaemter = inch(self.outer_diameter) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.inner_diameter = metric(self.inner_diameter) + self.outer_diaemter = metric(self.outer_diameter) + class Drill(Primitive): """ A drill hole @@ -597,3 +725,11 @@ class Drill(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) + diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 71ca111..21947e1 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -18,14 +18,15 @@ """ This module provides an RS-274-X class and parser. """ - import copy import json import re + from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings + def read(filename): """ Read data from filename and return a GerberFile @@ -112,6 +113,21 @@ class GerberFile(CamFile): f.write(statement.to_gerber(settings or self.settings)) f.write("\n") + def to_inch(self): + if self.units != 'inch': + self.units = 'inch' + for statement in self.statements: + statement.to_inch() + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + if self.units != 'metric': + self.units = 'metric' + for statement in self.statements: + statement.to_metric() + for primitive in self.primitives: + primitive.to_metric() class GerberParser(object): diff --git a/gerber/tests/test_am_statements.py b/gerber/tests/test_am_statements.py index 696d951..0cee13d 100644 --- a/gerber/tests/test_am_statements.py +++ b/gerber/tests/test_am_statements.py @@ -324,6 +324,11 @@ def test_AMUnsupportPrimitive(): u = AMUnsupportPrimitive('Test') assert_equal(u.to_gerber(), 'Test') +def test_AMUnsupportPrimitive_smoketest(): + u = AMUnsupportPrimitive.from_gerber('Test') + u.to_inch() + u.to_metric() + def test_inch(): diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py index 185e716..6296cc9 100644 --- a/gerber/tests/test_cam.py +++ b/gerber/tests/test_cam.py @@ -65,9 +65,9 @@ def test_camfile_settings(): cf = CamFile() assert_equal(cf.settings, FileSettings()) -#def test_bounds_override(): -# cf = CamFile() -# assert_raises(NotImplementedError, cf.bounds) +def test_bounds_override_smoketest(): + cf = CamFile() + cf.bounds def test_zeros(): diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py index ea067b5..24cf793 100644 --- a/gerber/tests/test_excellon.py +++ b/gerber/tests/test_excellon.py @@ -2,12 +2,13 @@ # -*- coding: utf-8 -*- # Author: Hamilton Kibbe +import os + from ..cam import FileSettings from ..excellon import read, detect_excellon_format, ExcellonFile, ExcellonParser from ..excellon_statements import ExcellonTool from tests import * -import os NCDRILL_FILE = os.path.join(os.path.dirname(__file__), 'resources/ncdrill.DRD') @@ -37,6 +38,29 @@ def test_bounds(): def test_report(): ncdrill = read(NCDRILL_FILE) + +def test_conversion(): + import copy + ncdrill = read(NCDRILL_FILE) + assert_equal(ncdrill.settings.units, 'inch') + ncdrill_inch = copy.deepcopy(ncdrill) + ncdrill.to_metric() + assert_equal(ncdrill.settings.units, 'metric') + + for tool in ncdrill_inch.tools.itervalues(): + tool.to_metric() + for primitive in ncdrill_inch.primitives: + primitive.to_metric() + for statement in ncdrill_inch.statements: + statement.to_metric() + + for m_tool, i_tool in zip(ncdrill.tools.itervalues(), ncdrill_inch.tools.itervalues()): + assert_equal(i_tool, m_tool) + + for m, i in zip(ncdrill.primitives,ncdrill_inch.primitives): + assert_equal(m, i) + + def test_parser_hole_count(): settings = FileSettings(**detect_excellon_format(NCDRILL_FILE)) p = ExcellonParser(settings) diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 35bd045..4daeb4b 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -3,7 +3,7 @@ # Author: Hamilton Kibbe -from .tests import assert_equal, assert_raises +from .tests import assert_equal, assert_not_equal, assert_raises from ..excellon_statements import * from ..cam import FileSettings @@ -65,19 +65,34 @@ def test_excellontool_order(): assert_equal(tool1.rpm, tool2.rpm) def test_excellontool_conversion(): - tool = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 25.4}) + tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 25.4}) tool.to_inch() assert_equal(tool.diameter, 1.) - tool = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 1}) + tool = ExcellonTool.from_dict(FileSettings(units='inch'), {'number': 8, 'diameter': 1.}) tool.to_metric() assert_equal(tool.diameter, 25.4) + # Shouldn't change units if we're already using target units + tool = ExcellonTool.from_dict(FileSettings(units='inch'), {'number': 8, 'diameter': 25.4}) + tool.to_inch() + assert_equal(tool.diameter, 25.4) + tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 1.}) + tool.to_metric() + assert_equal(tool.diameter, 1.) + + def test_excellontool_repr(): tool = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125}) assert_equal(str(tool), '') tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 0.125}) assert_equal(str(tool), '') +def test_excellontool_equality(): + t = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125}) + t1 = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125}) + assert_equal(t, t1) + t1 = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 0.125}) + assert_not_equal(t, t1) def test_toolselection_factory(): """ Test ToolSelectionStmt factory method """ @@ -166,6 +181,19 @@ def test_repeatholestmt_dump(): stmt = RepeatHoleStmt.from_excellon(line, FileSettings()) assert_equal(stmt.to_excellon(FileSettings()), line) +def test_repeatholestmt_conversion(): + line = 'R4X0254Y254' + stmt = RepeatHoleStmt.from_excellon(line, FileSettings()) + stmt.to_inch() + assert_equal(stmt.xdelta, 0.1) + assert_equal(stmt.ydelta, 1.) + + line = 'R4X01Y1' + stmt = RepeatHoleStmt.from_excellon(line, FileSettings()) + stmt.to_metric() + assert_equal(stmt.xdelta, 25.4) + assert_equal(stmt.ydelta, 254.) + def test_repeathole_str(): stmt = RepeatHoleStmt.from_excellon('R4X015Y32', FileSettings()) assert_equal(str(stmt), '') @@ -223,6 +251,16 @@ def test_endofprogramStmt_dump(): stmt = EndOfProgramStmt.from_excellon(line, FileSettings()) assert_equal(stmt.to_excellon(FileSettings()), line) +def test_endofprogramstmt_conversion(): + stmt = EndOfProgramStmt.from_excellon('M30X0254Y254', FileSettings()) + stmt.to_inch() + assert_equal(stmt.x, 0.1) + assert_equal(stmt.y, 1.0) + + stmt = EndOfProgramStmt.from_excellon('M30X01Y1', FileSettings()) + stmt.to_metric() + assert_equal(stmt.x, 25.4) + assert_equal(stmt.y, 254.) def test_unitstmt_factory(): """ Test UnitStmt factory method @@ -256,6 +294,14 @@ def test_unitstmt_dump(): stmt = UnitStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) +def test_unitstmt_conversion(): + stmt = UnitStmt.from_excellon('METRIC,TZ') + stmt.to_inch() + assert_equal(stmt.units, 'inch') + + stmt = UnitStmt.from_excellon('INCH,TZ') + stmt.to_metric() + assert_equal(stmt.units, 'metric') def test_incrementalmode_factory(): """ Test IncrementalModeStmt factory method @@ -385,6 +431,18 @@ def test_measmodestmt_validation(): assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70') assert_raises(ValueError, MeasuringModeStmt, 'millimeters') +def test_measmodestmt_conversion(): + line = 'M72' + stmt = MeasuringModeStmt.from_excellon(line) + assert_equal(stmt.units, 'inch') + stmt.to_metric() + assert_equal(stmt.units, 'metric') + + line = 'M71' + stmt = MeasuringModeStmt.from_excellon(line) + assert_equal(stmt.units, 'metric') + stmt.to_inch() + assert_equal(stmt.units, 'inch') def test_routemode_stmt(): stmt = RouteModeStmt() @@ -406,3 +464,11 @@ def test_unknownstmt(): def test_unknownstmt_dump(): stmt = UnknownStmt('TEST') assert_equal(stmt.to_excellon(FileSettings()), 'TEST') + + +def test_excellontstmt(): + """ Smoke test ExcellonStatement + """ + stmt = ExcellonStatement() + stmt.to_inch() + stmt.to_metric() \ No newline at end of file diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index c6040c0..bf7035f 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -7,6 +7,12 @@ from .tests import * from ..gerber_statements import * from ..cam import FileSettings +def test_Statement_smoketest(): + stmt = Statement('Test') + assert_equal(stmt.type, 'Test') + stmt.to_inch() + stmt.to_metric() + assert_equal(str(stmt), '') def test_FSParamStmt_factory(): """ Test FSParamStruct factory @@ -114,6 +120,17 @@ def test_MOParamStmt_dump(): assert_equal(mo.to_gerber(), '%MOMM*%') +def test_MOParamStmt_conversion(): + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + mo.to_inch() + assert_equal(mo.mode, 'inch') + + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + mo.to_metric() + assert_equal(mo.mode, 'metric') + def test_MOParamStmt_string(): """ Test MOParamStmt.__str__() """ @@ -213,6 +230,20 @@ def test_OFParamStmt_dump(): assert_equal(of.to_gerber(), '%OFA0.12345B0.12345*%') +def test_OFParamStmt_conversion(): + stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'} + of = OFParamStmt.from_dict(stmt) + of.to_inch() + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + stmt = {'param': 'OF', 'a': '0.1', 'b': '1.0'} + of = OFParamStmt.from_dict(stmt) + of.to_metric() + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + + def test_OFParamStmt_string(): """ Test OFParamStmt __str__ """ @@ -232,6 +263,19 @@ def test_SFParamStmt_dump(): sf = SFParamStmt.from_dict(stmt) assert_equal(sf.to_gerber(), '%SFA1.4B0.9*%') +def test_SFParamStmt_conversion(): + stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'} + of = SFParamStmt.from_dict(stmt) + of.to_inch() + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + stmt = {'param': 'OF', 'a': '0.1', 'b': '1.0'} + of = SFParamStmt.from_dict(stmt) + of.to_metric() + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + def test_SFParamStmt_string(): stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'} sf = SFParamStmt.from_dict(stmt) @@ -651,4 +695,3 @@ def test_aperturestmt_dump(): assert_equal(str(ast), '') - \ No newline at end of file diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index f8b8620..cada6d4 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -51,6 +51,57 @@ def test_line_bounds(): l = Line(start, end, r) assert_equal(l.bounding_box, expected) +def test_line_vertices(): + c = Circle((0, 0), 2) + l = Line((0, 0), (1, 1), c) + assert_equal(l.vertices, None) + + # All 4 compass points, all 4 quadrants and the case where start == end + test_cases = [((0, 0), (1, 0), ((-1, -1), (-1, 1), (2, 1), (2, -1))), + ((0, 0), (1, 1), ((-1, -1), (-1, 1), (0, 2), (2, 2), (2, 0), (1,-1))), + ((0, 0), (0, 1), ((-1, -1), (-1, 2), (1, 2), (1, -1))), + ((0, 0), (-1, 1), ((-1, -1), (-2, 0), (-2, 2), (0, 2), (1, 1), (1, -1))), + ((0, 0), (-1, 0), ((-2, -1), (-2, 1), (1, 1), (1, -1))), + ((0, 0), (-1, -1), ((-2, -2), (1, -1), (1, 1), (-1, 1), (-2, 0), (0,-2))), + ((0, 0), (0, -1), ((-1, -2), (-1, 1), (1, 1), (1, -2))), + ((0, 0), (1, -1), ((-1, -1), (0, -2), (2, -2), (2, 0), (1, 1), (-1, 1))), + ((0, 0), (0, 0), ((-1, -1), (-1, 1), (1, 1), (1, -1))),] + r = Rectangle((0, 0), 2, 2) + + for start, end, vertices in test_cases: + l = Line(start, end, r) + assert_equal(set(vertices), set(l.vertices)) + +def test_line_conversion(): + c = Circle((0, 0), 25.4) + l = Line((2.54, 25.4), (254.0, 2540.0), c) + l.to_inch() + assert_equal(l.start, (0.1, 1.0)) + assert_equal(l.end, (10.0, 100.0)) + assert_equal(l.aperture.diameter, 1.0) + + c = Circle((0, 0), 1.0) + l = Line((0.1, 1.0), (10.0, 100.0), c) + l.to_metric() + assert_equal(l.start, (2.54, 25.4)) + assert_equal(l.end, (254.0, 2540.0)) + assert_equal(l.aperture.diameter, 25.4) + + r = Rectangle((0, 0), 25.4, 254.0) + l = Line((2.54, 25.4), (254.0, 2540.0), r) + l.to_inch() + assert_equal(l.start, (0.1, 1.0)) + assert_equal(l.end, (10.0, 100.0)) + assert_equal(l.aperture.width, 1.0) + assert_equal(l.aperture.height, 10.0) + + r = Rectangle((0, 0), 1.0, 10.0) + l = Line((0.1, 1.0), (10.0, 100.0), r) + l.to_metric() + assert_equal(l.start, (2.54, 25.4)) + assert_equal(l.end, (254.0, 2540.0)) + assert_equal(l.aperture.width, 25.4) + assert_equal(l.aperture.height, 254.0) def test_arc_radius(): @@ -89,6 +140,24 @@ def test_arc_bounds(): a = Arc(start, end, center, direction, c) assert_equal(a.bounding_box, bounds) +def test_arc_conversion(): + c = Circle((0, 0), 25.4) + a = Arc((2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0),'clockwise', c) + a.to_inch() + assert_equal(a.start, (0.1, 1.0)) + assert_equal(a.end, (10.0, 100.0)) + assert_equal(a.center, (1000.0, 10000.0)) + assert_equal(a.aperture.diameter, 1.0) + + c = Circle((0, 0), 1.0) + a = Arc((0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0),'clockwise', c) + a.to_metric() + assert_equal(a.start, (2.54, 25.4)) + assert_equal(a.end, (254.0, 2540.0)) + assert_equal(a.center, (25400.0, 254000.0)) + assert_equal(a.aperture.diameter, 25.4) + + def test_circle_radius(): """ Test Circle primitive radius calculation @@ -146,7 +215,7 @@ def test_rectangle_bounds(): xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) - + def test_diamond_ctor(): """ Test diamond creation """ @@ -169,6 +238,19 @@ def test_diamond_bounds(): assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) +def test_diamond_conversion(): + d = Diamond((2.54, 25.4), 254.0, 2540.0) + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.width, 10.0) + assert_equal(d.height, 100.0) + + d = Diamond((0.1, 1.0), 10.0, 100.0) + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.width, 254.0) + assert_equal(d.height, 2540.0) + def test_chamfer_rectangle_ctor(): """ Test chamfer rectangle creation @@ -198,6 +280,21 @@ def test_chamfer_rectangle_bounds(): assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) +def test_chamfer_rectangle_conversion(): + r = ChamferRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False)) + r.to_inch() + assert_equal(r.position, (0.1, 1.0)) + assert_equal(r.width, 10.0) + assert_equal(r.height, 100.0) + assert_equal(r.chamfer, 0.01) + + r = ChamferRectangle((0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False)) + r.to_metric() + assert_equal(r.position, (2.54,25.4)) + assert_equal(r.width, 254.0) + assert_equal(r.height, 2540.0) + assert_equal(r.chamfer, 0.254) + def test_round_rectangle_ctor(): """ Test round rectangle creation """ @@ -226,6 +323,21 @@ def test_round_rectangle_bounds(): assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) +def test_round_rectangle_conversion(): + r = RoundRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False)) + r.to_inch() + assert_equal(r.position, (0.1, 1.0)) + assert_equal(r.width, 10.0) + assert_equal(r.height, 100.0) + assert_equal(r.radius, 0.01) + + r = RoundRectangle((0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False)) + r.to_metric() + assert_equal(r.position, (2.54,25.4)) + assert_equal(r.width, 254.0) + assert_equal(r.height, 2540.0) + assert_equal(r.radius, 0.254) + def test_obround_ctor(): """ Test obround creation """ @@ -270,7 +382,22 @@ def test_obround_subshapes(): assert_array_almost_equal(ss['rectangle'].position, (0, 0)) assert_array_almost_equal(ss['circle1'].position, (1.5, 0)) assert_array_almost_equal(ss['circle2'].position, (-1.5, 0)) - + + +def test_obround_conversion(): + o = Obround((2.54,25.4), 254.0, 2540.0) + o.to_inch() + assert_equal(o.position, (0.1, 1.0)) + assert_equal(o.width, 10.0) + assert_equal(o.height, 100.0) + + o= Obround((0.1, 1.0), 10.0, 100.0) + o.to_metric() + assert_equal(o.position, (2.54, 25.4)) + assert_equal(o.width, 254.0) + assert_equal(o.height, 2540.0) + + def test_polygon_ctor(): """ Test polygon creation """ @@ -282,7 +409,7 @@ def test_polygon_ctor(): assert_equal(p.position, pos) assert_equal(p.sides, sides) assert_equal(p.radius, radius) - + def test_polygon_bounds(): """ Test polygon bounding box calculation """ @@ -296,6 +423,18 @@ def test_polygon_bounds(): assert_array_almost_equal(ybounds, (-2, 6)) +def test_polygon_conversion(): + p = Polygon((2.54, 25.4), 3, 254.0) + p.to_inch() + assert_equal(p.position, (0.1, 1.0)) + assert_equal(p.radius, 10.0) + + p = Polygon((0.1, 1.0), 3, 10.0) + p.to_metric() + assert_equal(p.position, (2.54, 25.4)) + assert_equal(p.radius, 254.0) + + def test_region_ctor(): """ Test Region creation """ @@ -313,8 +452,20 @@ def test_region_bounds(): xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (0, 1)) assert_array_almost_equal(ybounds, (0, 1)) - - + +def test_region_conversion(): + points = ((2.54, 25.4), (254.0,2540.0), (25400.0,254000.0), (2.54,25.4)) + r = Region(points) + r.to_inch() + assert_equal(set(r.points), {(0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0)}) + + points = ((0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0), (0.1, 1.0)) + r = Region(points) + r.to_metric() + assert_equal(set(r.points), {(2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0)}) + + + def test_round_butterfly_ctor(): """ Test round butterfly creation """ @@ -331,6 +482,18 @@ def test_round_butterfly_ctor_validation(): assert_raises(TypeError, RoundButterfly, 3, 5) assert_raises(TypeError, RoundButterfly, (3,4,5), 5) + +def test_round_butterfly_conversion(): + b = RoundButterfly((2.54, 25.4), 254.0) + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.diameter, 10.0) + + b = RoundButterfly((0.1, 1.0), 10.0) + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.diameter, (254.0)) + def test_round_butterfly_bounds(): """ Test RoundButterfly bounding box calculation """ @@ -338,7 +501,7 @@ def test_round_butterfly_bounds(): xbounds, ybounds = b.bounding_box assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) - + def test_square_butterfly_ctor(): """ Test SquareButterfly creation """ @@ -363,6 +526,17 @@ def test_square_butterfly_bounds(): assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) +def test_squarebutterfly_conversion(): + b = SquareButterfly((2.54, 25.4), 254.0) + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.side, 10.0) + + b = SquareButterfly((0.1, 1.0), 10.0) + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.side, (254.0)) + def test_donut_ctor(): """ Test Donut primitive creation """ @@ -380,9 +554,28 @@ def test_donut_ctor_validation(): assert_raises(TypeError, Donut, (3, 4, 5), 'round', 5, 7) assert_raises(ValueError, Donut, (0, 0), 'triangle', 3, 5) assert_raises(ValueError, Donut, (0, 0), 'round', 5, 3) - + def test_donut_bounds(): - pass + d = Donut((0, 0), 'round', 0.0, 2.0) + assert_equal(d.lower_left, (-1.0, -1.0)) + assert_equal(d.upper_right, (1.0, 1.0)) + xbounds, ybounds = d.bounding_box + assert_equal(xbounds, (-1., 1.)) + assert_equal(ybounds, (-1., 1.)) + +def test_donut_conversion(): + d = Donut((2.54, 25.4), 'round', 254.0, 2540.0) + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.inner_diameter, 10.0) + assert_equal(d.outer_diaemter, 100.0) + + d = Donut((0.1, 1.0), 'round', 10.0, 100.0) + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.inner_diameter, 254.0) + assert_equal(d.outer_diaemter, 2540.0) + def test_drill_ctor(): """ Test drill primitive creation @@ -393,13 +586,15 @@ def test_drill_ctor(): assert_equal(d.position, position) assert_equal(d.diameter, diameter) assert_equal(d.radius, diameter/2.) - + + def test_drill_ctor_validation(): """ Test drill argument validation """ assert_raises(TypeError, Drill, 3, 5) assert_raises(TypeError, Drill, (3,4,5), 5) - + + def test_drill_bounds(): d = Drill((0, 0), 2) xbounds, ybounds = d.bounding_box @@ -409,5 +604,21 @@ def test_drill_bounds(): xbounds, ybounds = d.bounding_box assert_array_almost_equal(xbounds, (0, 2)) assert_array_almost_equal(ybounds, (1, 3)) - - \ No newline at end of file + +def test_drill_conversion(): + d = Drill((2.54, 25.4), 254.) + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.diameter, 10.0) + + d = Drill((0.1, 1.0), 10.) + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.diameter, 254.0) + +def test_drill_equality(): + d = Drill((2.54, 25.4), 254.) + d1 = Drill((2.54, 25.4), 254.) + assert_equal(d, d1) + d1 = Drill((2.54, 25.4), 254.2) + assert_not_equal(d, d1) diff --git a/gerber/tests/test_rs274x.py b/gerber/tests/test_rs274x.py index 5d528dc..27f6f49 100644 --- a/gerber/tests/test_rs274x.py +++ b/gerber/tests/test_rs274x.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- # Author: Hamilton Kibbe +import os + from ..rs274x import read, GerberFile from tests import * -import os TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), 'resources/top_copper.GTL') @@ -25,3 +26,20 @@ def test_size_parameter(): assert_equal(size[0], 2.2869) assert_equal(size[1], 1.8064) +def test_conversion(): + import copy + top_copper = read(TOP_COPPER_FILE) + assert_equal(top_copper.units, 'inch') + top_copper_inch = copy.deepcopy(top_copper) + top_copper.to_metric() + for statement in top_copper_inch.statements: + statement.to_metric() + for primitive in top_copper_inch.primitives: + primitive.to_metric() + assert_equal(top_copper.units, 'metric') + for i, m in zip(top_copper.statements, top_copper_inch.statements): + assert_equal(i, m) + + for i, m in zip(top_copper.primitives, top_copper_inch.primitives): + assert_equal(i, m) + diff --git a/gerber/utils.py b/gerber/utils.py index 23575b3..542611d 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -26,6 +26,7 @@ files. # Author: Hamilton Kibbe # License: +MILLIMETERS_PER_INCH = 25.4 def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number @@ -235,3 +236,10 @@ def validate_coordinates(position): for coord in position: if not (isinstance(coord, int) or isinstance(coord, float)): raise TypeError('Coordinates must be integers or floats') + + +def metric(value): + return value * MILLIMETERS_PER_INCH + +def inch(value): + return value / MILLIMETERS_PER_INCH -- cgit