Get unit conversion working for Gerber/Excellon files
Started operations module for file operations/transforms
This commit is contained in:
parent
bc532997ae
commit
288ac27084
16 changed files with 859 additions and 94 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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
120
gerber/operations.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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>')
|
||||
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue