summaryrefslogtreecommitdiff
path: root/gerber
diff options
context:
space:
mode:
Diffstat (limited to 'gerber')
-rw-r--r--gerber/am_statements.py15
-rw-r--r--gerber/cam.py4
-rwxr-xr-xgerber/excellon.py29
-rw-r--r--gerber/excellon_statements.py102
-rw-r--r--gerber/gerber_statements.py62
-rw-r--r--gerber/operations.py120
-rw-r--r--gerber/primitives.py186
-rw-r--r--gerber/rs274x.py18
-rw-r--r--gerber/tests/test_am_statements.py5
-rw-r--r--gerber/tests/test_cam.py6
-rw-r--r--gerber/tests/test_excellon.py26
-rw-r--r--gerber/tests/test_excellon_statements.py72
-rw-r--r--gerber/tests/test_gerber_statements.py45
-rw-r--r--gerber/tests/test_primitives.py235
-rw-r--r--gerber/tests/test_rs274x.py20
-rw-r--r--gerber/utils.py8
16 files changed, 859 insertions, 94 deletions
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<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):
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: %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"
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 <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
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 <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)
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 <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() \ 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), '<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>')
- \ 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 <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)
+
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 <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