Get unit conversion working for Gerber/Excellon files

Started operations module for file operations/transforms
This commit is contained in:
Hamilton Kibbe 2015-02-18 04:31:23 -05:00
parent bc532997ae
commit 288ac27084
16 changed files with 859 additions and 94 deletions

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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<rcount>[0-9]*)X?(?P<xdelta>\d*\.?\d*)?Y?(?P<ydelta>\d*\.?\d*)?').match(line)
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,
@ -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 '<Repeat Hole: %d times>' % self.count
@ -357,7 +391,8 @@ class EndOfProgramStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings):
match = re.compile(r'M30X?(?P<x>\d*\.?\d*)?Y?(?P<y>\d*\.?\d*)?').match(line)
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)
@ -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):

View file

@ -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: %s>' % 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"

120
gerber/operations.py Normal file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# copyright 2015 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.
"""
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

View file

@ -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)

View file

@ -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):

View file

@ -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():

View file

@ -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():

View file

@ -2,12 +2,13 @@
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
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)

View file

@ -3,7 +3,7 @@
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
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), '<ExcellonTool 08: 0.125in. dia.>')
tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 0.125})
assert_equal(str(tool), '<ExcellonTool 08: 0.125mm dia.>')
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), '<Repeat Hole: 4 times>')
@ -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()

View file

@ -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), '<Statement type=Test>')
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), '<Aperture: 3>')

View file

@ -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))
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)

View file

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
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)

View file

@ -26,6 +26,7 @@ files.
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
# 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