summaryrefslogtreecommitdiff
path: root/gerbonara
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2022-01-17 23:14:52 +0100
committerjaseg <git@jaseg.de>2022-01-17 23:14:52 +0100
commit73a44901c0ef0e94e9465c2f35750ca6f85a4473 (patch)
treea5c66526a38fe5a4e3d004dc7127680b6d0e25c6 /gerbonara
parent336a18fb493c79824323a59865083a0037a4a2f4 (diff)
downloadgerbonara-73a44901c0ef0e94e9465c2f35750ca6f85a4473.tar.gz
gerbonara-73a44901c0ef0e94e9465c2f35750ca6f85a4473.tar.bz2
gerbonara-73a44901c0ef0e94e9465c2f35750ca6f85a4473.zip
Excellon, unit conversion WIP
Diffstat (limited to 'gerbonara')
-rw-r--r--gerbonara/gerber/aperture_macros/expression.py7
-rw-r--r--gerbonara/gerber/aperture_macros/parse.py3
-rw-r--r--gerbonara/gerber/aperture_macros/primitive.py8
-rw-r--r--gerbonara/gerber/apertures.py110
-rw-r--r--gerbonara/gerber/cam.py6
-rwxr-xr-xgerbonara/gerber/excellon.py87
-rw-r--r--gerbonara/gerber/gerber_statements.py10
-rw-r--r--gerbonara/gerber/graphic_objects.py24
-rw-r--r--gerbonara/gerber/rs274x.py72
-rw-r--r--gerbonara/gerber/utils.py41
10 files changed, 149 insertions, 219 deletions
diff --git a/gerbonara/gerber/aperture_macros/expression.py b/gerbonara/gerber/aperture_macros/expression.py
index fb399d3..2375c56 100644
--- a/gerbonara/gerber/aperture_macros/expression.py
+++ b/gerbonara/gerber/aperture_macros/expression.py
@@ -7,8 +7,7 @@ import operator
import re
import ast
-
-MILLIMETERS_PER_INCH = 25.4
+from ..utils import MM, Inch, MILLIMETERS_PER_INCH
def expr(obj):
@@ -81,10 +80,10 @@ class UnitExpression(Expression):
if self.unit is None or unit is None or self.unit == unit:
return self._expr
- elif unit == 'mm':
+ elif unit == MM:
return self._expr * MILLIMETERS_PER_INCH
- elif unit == 'inch':
+ elif unit == Inch:
return self._expr / MILLIMETERS_PER_INCH
else:
diff --git a/gerbonara/gerber/aperture_macros/parse.py b/gerbonara/gerber/aperture_macros/parse.py
index 375bb5b..43af309 100644
--- a/gerbonara/gerber/aperture_macros/parse.py
+++ b/gerbonara/gerber/aperture_macros/parse.py
@@ -11,6 +11,7 @@ import math
from . import primitive as ap
from .expression import *
+from ..utils import MM
def rad_to_deg(x):
return (x / math.pi) * 180
@@ -98,7 +99,7 @@ class ApertureMacro:
def __hash__(self):
return hash(self.to_gerber())
- def dilated(self, offset, unit='mm'):
+ def dilated(self, offset, unit=MM):
dup = copy.deepcopy(self)
new_primitives = []
for primitive in dup.primitives:
diff --git a/gerbonara/gerber/aperture_macros/primitive.py b/gerbonara/gerber/aperture_macros/primitive.py
index b569637..4de19c4 100644
--- a/gerbonara/gerber/aperture_macros/primitive.py
+++ b/gerbonara/gerber/aperture_macros/primitive.py
@@ -21,14 +21,6 @@ def point_distance(a, b):
def deg_to_rad(a):
return (a / 180) * math.pi
-def convert(value, src, dst):
- if src == dst or src is None or dst is None or value is None:
- return value
- elif dst == 'mm':
- return value * 25.4
- else:
- return value / 25.4
-
class Primitive:
def __init__(self, unit, args):
self.unit = unit
diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py
index e362e0d..b05457d 100644
--- a/gerbonara/gerber/apertures.py
+++ b/gerbonara/gerber/apertures.py
@@ -3,7 +3,7 @@ import math
from dataclasses import dataclass, replace, fields, InitVar, KW_ONLY
from .aperture_macros.parse import GenericMacros
-from .utils import convert_units
+from .utils import MM, Inch
from . import graphic_primitives as gp
@@ -12,11 +12,11 @@ def _flash_hole(self, x, y, unit=None):
if getattr(self, 'hole_rect_h', None) is not None:
return [*self.primitives(x, y, unit),
gp.Rectangle((x, y),
- (self.convert_to(self.hole_dia, unit), self.convert_to(self.hole_rect_h, unit)),
+ (self.unit.to(unit, self.hole_dia), self.unit.to(unit, self.hole_rect_h)),
rotation=self.rotation, polarity_dark=False)]
elif self.hole_dia is not None:
return [*self.primitives(x, y, unit),
- gp.Circle(x, y, self.convert_to(self.hole_dia/2, unit), polarity_dark=False)]
+ gp.Circle(x, y, self.unit.to(unit, self.hole_dia/2), polarity_dark=False)]
else:
return self.primitives(x, y, unit)
@@ -31,8 +31,6 @@ class Length:
def __init__(self, obj_type):
self.type = obj_type
-CONVERSION_FACTOR = {None: 1, 'mm': 25.4, 'inch': 1/25.4}
-
@dataclass
class Aperture:
_ : KW_ONLY
@@ -45,12 +43,6 @@ class Aperture:
else:
return 'circle'
- def convert(self, value, unit):
- return convert_units(value, self.unit, unit)
-
- def convert_from(self, value, unit):
- return convert_units(value, unit, self.unit)
-
def params(self, unit=None):
out = []
for f in fields(self):
@@ -59,7 +51,7 @@ class Aperture:
val = getattr(self, f.name)
if isinstance(f.type, Length):
- val = self.convert_to(val, unit)
+ val = self.unit.to(unit, val)
out.append(val)
return out
@@ -82,7 +74,7 @@ class Aperture:
def __eq__(self, other):
# We need to choose some unit here.
- return hasattr(other, to_gerber) and self.to_gerber('mm') == other.to_gerber('mm')
+ return hasattr(other, to_gerber) and self.to_gerber(MM) == other.to_gerber(MM)
def _rotate_hole_90(self):
if self.hole_rect_h is None:
@@ -98,7 +90,7 @@ class ExcellonTool(Aperture):
depth_offset : Length(float) = 0
def primitives(self, x, y, unit=None):
- return [ gp.Circle(x, y, self.convert_to(self.diameter/2, unit)) ]
+ return [ gp.Circle(x, y, self.unit.to(unit, self.diameter/2)) ]
def to_xnc(self, settings):
z_off += 'Z' + settings.write_gerber_value(self.depth_offset) if self.depth_offset is not None else ''
@@ -121,11 +113,11 @@ class ExcellonTool(Aperture):
z_off = '' if self.depth_offset is None else f' z_offset={self.depth_offset}'
return f'<Excellon Tool d={self.diameter:.3f}{plated}{z_off}>'
- def equivalent_width(self, unit=None):
- return self.convert_to(self.diameter, unit)
+ def equivalent_width(self, unit=MM):
+ return self.unit.to(unit, self.diameter)
- def dilated(self, offset, unit='mm'):
- offset = self.convert_from(offset, unit)
+ def dilated(self, offset, unit=MM):
+ offset = self.unit.to(unit, offset)
return replace(self, diameter=self.diameter+2*offset)
def _rotated(self):
@@ -134,10 +126,10 @@ class ExcellonTool(Aperture):
return self.to_macro(self.rotation)
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.circle, self.params(unit='mm'))
+ return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM))
def params(self, unit=None):
- return self.convert_to(self.diameter, unit)
+ return [self.unit.to(unit, self.diameter)]
@dataclass
@@ -150,7 +142,7 @@ class CircleAperture(Aperture):
rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber
def primitives(self, x, y, unit=None):
- return [ gp.Circle(x, y, self.convert_to(self.diameter/2, unit)) ]
+ return [ gp.Circle(x, y, self.unit.to(unit, self.diameter/2)) ]
def __str__(self):
return f'<circle aperture d={self.diameter:.3}>'
@@ -158,10 +150,10 @@ class CircleAperture(Aperture):
flash = _flash_hole
def equivalent_width(self, unit=None):
- return self.convert_to(self.diameter, unit)
+ return self.unit.to(unit, self.diameter)
- def dilated(self, offset, unit='mm'):
- offset = self.convert_from(offset, unit)
+ def dilated(self, offset, unit=MM):
+ offset = self.unit.from(unit, offset)
return replace(self, diameter=self.diameter+2*offset, hole_dia=None, hole_rect_h=None)
def _rotated(self):
@@ -171,13 +163,13 @@ class CircleAperture(Aperture):
return self.to_macro(self.rotation)
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.circle, self.params(unit='mm'))
+ return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM))
def params(self, unit=None):
return strip_right(
- self.convert_to(self.diameter, unit),
- self.convert_to(self.hole_dia, unit),
- self.convert_to(self.hole_rect_h, unit))
+ self.unit.to(unit, self.diameter),
+ self.unit.to(unit, self.hole_dia),
+ self.unit.to(unit, self.hole_rect_h))
@dataclass
@@ -191,7 +183,7 @@ class RectangleAperture(Aperture):
rotation : float = 0 # radians
def primitives(self, x, y, unit=None):
- return [ gp.Rectangle(x, y, self.convert_to(self.w, unit), self.convert_to(self.h, unit), rotation=self.rotation) ]
+ return [ gp.Rectangle(x, y, self.unit.to(unit, self.w), self.unit.to(unit, self.h), rotation=self.rotation) ]
def __str__(self):
return f'<rect aperture {self.w:.3}x{self.h:.3}>'
@@ -199,10 +191,10 @@ class RectangleAperture(Aperture):
flash = _flash_hole
def equivalent_width(self, unit=None):
- return self.convert_to(math.sqrt(self.w**2 + self.h**2), unit)
+ return self.unit.to(unit, math.sqrt(self.w**2 + self.h**2))
- def dilated(self, offset, unit='mm'):
- offset = self.convert_from(offset, unit)
+ def dilated(self, offset, unit=MM):
+ offset = self.unit.from(unit, offset)
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
def _rotated(self):
@@ -215,18 +207,18 @@ class RectangleAperture(Aperture):
def to_macro(self):
return ApertureMacroInstance(GenericMacros.rect,
- [self.convert_to(self.w, 'mm'),
- self.convert_to(self.h, 'mm'),
- self.convert_to(self.hole_dia, 'mm') or 0,
- self.convert_to(self.hole_rect_h, 'mm') or 0,
+ [self.unit.to(MM, self.w),
+ self.unit.to(MM, self.h),
+ self.unit.to(MM, self.hole_dia) or 0,
+ self.unit.to(MM, self.hole_rect_h) or 0,
self.rotation])
def params(self, unit=None):
return strip_right(
- self.convert_to(self.w, unit),
- self.convert_to(self.h, unit),
- self.convert_to(self.hole_dia, unit),
- self.convert_to(self.hole_rect_h, unit))
+ self.unit.to(unit, self.w),
+ self.unit.to(unit, self.h),
+ self.unit.to(unit, self.hole_dia),
+ self.unit.to(unit, self.hole_rect_h))
@dataclass
@@ -240,15 +232,15 @@ class ObroundAperture(Aperture):
rotation : float = 0
def primitives(self, x, y, unit=None):
- return [ gp.Obround(x, y, self.convert_to(self.w, unit), self.convert_to(self.h, unit), rotation=self.rotation) ]
+ return [ gp.Obround(x, y, self.unit.to(unit, self.w), self.unit.to(unit, self.h), rotation=self.rotation) ]
def __str__(self):
return f'<obround aperture {self.w:.3}x{self.h:.3}>'
flash = _flash_hole
- def dilated(self, offset, unit='mm'):
- offset = self.convert_from(offset, unit)
+ def dilated(self, offset, unit=MM):
+ offset = self.unit.from(unit, offset)
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
def _rotated(self):
@@ -263,18 +255,18 @@ class ObroundAperture(Aperture):
# generic macro only supports w > h so flip x/y if h > w
inst = self if self.w > self.h else replace(self, w=self.h, h=self.w, **_rotate_hole_90(self), rotation=self.rotation-90)
return ApertureMacroInstance(GenericMacros.obround,
- [self.convert_to(inst.w, 'mm'),
- self.convert_to(ints.h, 'mm'),
- self.convert_to(inst.hole_dia, 'mm'),
- self.convert_to(inst.hole_rect_h, 'mm'),
+ [self.unit.to(MM, inst.w),
+ self.unit.to(MM, ints.h),
+ self.unit.to(MM, inst.hole_dia),
+ self.unit.to(MM, inst.hole_rect_h),
inst.rotation])
def params(self, unit=None):
return strip_right(
- self.convert_to(self.w, unit),
- self.convert_to(self.h, unit),
- self.convert_to(self.hole_dia, unit),
- self.convert_to(self.hole_rect_h, unit))
+ self.unit.to(unit, self.w),
+ self.unit.to(unit, self.h),
+ self.unit.to(unit, self.hole_dia),
+ self.unit.to(unit, self.hole_rect_h))
@dataclass
@@ -289,13 +281,13 @@ class PolygonAperture(Aperture):
self.n_vertices = int(self.n_vertices)
def primitives(self, x, y, unit=None):
- return [ gp.RegularPolygon(x, y, self.convert_to(self.diameter, unit)/2, self.n_vertices, rotation=self.rotation) ]
+ return [ gp.RegularPolygon(x, y, self.unit.to(unit, self.diameter)/2, self.n_vertices, rotation=self.rotation) ]
def __str__(self):
return f'<{self.n_vertices}-gon aperture d={self.diameter:.3}'
- def dilated(self, offset, unit='mm'):
- offset = self.convert_from(offset, unit)
+ def dilated(self, offset, unit=MM):
+ offset = self.unit.from(unit, offset)
return replace(self, diameter=self.diameter+2*offset, hole_dia=None)
flash = _flash_hole
@@ -304,16 +296,16 @@ class PolygonAperture(Aperture):
return self
def to_macro(self):
- return ApertureMacroInstance(GenericMacros.polygon, self.params('mm'))
+ return ApertureMacroInstance(GenericMacros.polygon, self.params(MM))
def params(self, unit=None):
rotation = self.rotation % (2*math.pi / self.n_vertices) if self.rotation is not None else None
if self.hole_dia is not None:
- return self.convert_to(self.diameter, unit), self.n_vertices, rotation, self.convert_to(self.hole_dia, unit)
+ return self.unit.to(unit, self.diameter), self.n_vertices, rotation, self.unit.to(unit, self.hole_dia)
elif rotation is not None and not math.isclose(rotation, 0):
- return self.convert_to(self.diameter, unit), self.n_vertices, rotation
+ return self.unit.to(unit, self.diameter), self.n_vertices, rotation
else:
- return self.convert_to(self.diameter, unit), self.n_vertices
+ return self.unit.to(unit, self.diameter), self.n_vertices
@dataclass
class ApertureMacroInstance(Aperture):
@@ -330,7 +322,7 @@ class ApertureMacroInstance(Aperture):
offset=(x, y), rotation=self.rotation,
parameters=self.parameters, unit=unit)
- def dilated(self, offset, unit='mm'):
+ def dilated(self, offset, unit=MM):
return replace(self, macro=self.macro.dilated(offset, unit))
def _rotated(self):
diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py
index f42c24d..42a5848 100644
--- a/gerbonara/gerber/cam.py
+++ b/gerbonara/gerber/cam.py
@@ -80,6 +80,8 @@ class FileSettings:
# Format precision
integer_digits, decimal_digits = self.number_format
+ if integer_digits is None or decimal_digits is None:
+ raise SyntaxError('No number format set and value does not contain a decimal point')
# Remove extraneous information
sign = '-' if value[0] == '-' else ''
@@ -99,6 +101,10 @@ class FileSettings:
value = self.unit.from(unit, value)
integer_digits, decimal_digits = self.number_format
+ if integer_digits is None:
+ integer_digits = 3
+ if decimal_digits is None:
+ decimal_digits = 3
# negative sign affects padding, so deal with it at the end...
sign = '-' if value < 0 else ''
diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py
index c9d76d6..33a3670 100755
--- a/gerbonara/gerber/excellon.py
+++ b/gerbonara/gerber/excellon.py
@@ -75,7 +75,7 @@ class ExcellonFile(CamFile):
yield ';' + comment
yield 'M48'
- yield 'METRIC' if settings.unit == 'mm' else 'INCH'
+ yield 'METRIC' if settings.unit == MM else 'INCH'
# Build tool index
tools = set(obj.tool for obj in self.objects)
@@ -166,6 +166,22 @@ class ExcellonFile(CamFile):
def hit_count(self):
return Counter(obj.tool for obj in self.objects)
+ def drill_sizes(self):
+ return sorted({ obj.tool.diameter for obj in self.objects })
+
+ @property
+ def bounds(self):
+ if not self.objects:
+ return None
+
+ (x_min, y_min), (x_max, y_max) = self.objects[0].bounding_box()
+ for obj in self.objects:
+ (obj_x_min, obj_y_min), (obj_x_max, obj_y_max) = self.objects[0].bounding_box()
+ x_min, y_min = min(x_min, obj_x_min), min(y_min, obj_y_min)
+ x_max, y_max = max(x_max, obj_x_max), max(y_max, obj_y_max)
+
+ return ((x_min, y_min), (x_max, y_max))
+
class RegexMatcher:
def __init__(self):
self.mapping = {}
@@ -195,14 +211,18 @@ class InterpMode(Enum):
class ExcellonParser(object):
- def __init__(self):
- self.settings = FileSettings(number_format=(2,4))
+ def __init__(self, settings=None):
+ # NOTE XNC files do not contain an explicit number format specification, but all values have decimal points.
+ # Thus, we set the default number format to (None, None). If the file does not contain an explicit specification
+ # and FileSettings.parse_gerber_value encounters a number without an explicit decimal point, it will throw a
+ # SyntaxError. In case of e.g. Allegro files where the number format and other options are specified separately
+ # from the excellon file, the caller must pass in an already filled-out FileSettings object.
+ if settings is None:
+ self.settings = FileSettings(number_format=(None, None))
self.program_state = None
self.interpolation_mode = InterpMode.LINEAR
- self.statements = []
self.tools = {}
- self.comment_tools = {}
- self.hits = []
+ self.objects = []
self.active_tool = None
self.pos = 0, 0
self.drill_down = False
@@ -212,39 +232,11 @@ class ExcellonParser(object):
def coordinates(self):
return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
- @property
- def bounds(self):
- xmin = ymin = 100000000000
- xmax = ymax = -100000000000
- for x, y in self.coordinates:
- if x is not None:
- xmin = x if x < xmin else xmin
- xmax = x if x > xmax else xmax
- if y is not None:
- ymin = y if y < ymin else ymin
- ymax = y if y > ymax else ymax
- return ((xmin, xmax), (ymin, ymax))
-
- @property
- def hole_sizes(self):
- return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)]
-
- @property
- def hole_count(self):
- return len(self.hits)
-
def parse(self, filename):
with open(filename, 'r') as f:
data = f.read()
return self.parse_raw(data, filename)
- def parse_raw(self, data, filename=None):
- for line in data.splitlines():
- self._parse_line(line.strip())
- for stmt in self.statements:
- stmt.units = self.units
- return ExcellonFile(self.statements, self.tools, self.hits, self.settings, filename)
-
def parse(self, filelike):
leftover = None
for line in filelike:
@@ -481,12 +473,14 @@ class ExcellonParser(object):
clockwise = (self.interpolation_mode == InterpMode.CIRCULAR_CW)
- if a:
+ if a: # radius given
if i or j:
warnings.warn('Arc without both radius and center specified.', SyntaxWarning)
- r = settings.parse_gerber_value(a)
+ # Convert endpoint-radius-endpoint notation to endpoint-center-endpoint notation. We always use the
+ # smaller arc here.
# from https://math.stackexchange.com/a/1781546
+ r = settings.parse_gerber_value(a)
x1, y1 = start
x2, y2 = end
dx, dy = (x2-x1)/2, (y2-y1)/2
@@ -499,7 +493,8 @@ class ExcellonParser(object):
cx = x0 - f*dy
cy = y0 + f*dx
i, j = cx-start[0], cy-start[1]
- else:
+
+ else: # explicit center given
i = settings.parse_gerber_value(i)
j = settings.parse_gerber_value(j)
@@ -514,6 +509,15 @@ class ExcellonParser(object):
@header_command
def handle_inch_mode(self, match):
self.settings.unit = Inch
+
+ @exprs.match('(METRIC|INCH),(LZ|TZ)(0*\.0*)?')
+ def parse_easyeda_format(self, match):
+ self.settings.unit = MM if match[1] == 'METRIC' else Inch
+ self.settings.zeros = 'leading' if match[2] == 'LZ' else 'trailing'
+ # Newer EasyEDA exports have this in an altium-like FILE_FORMAT comment instead. Some files even have both.
+ if match[3]:
+ integer, _, fractional = match[3].partition('.')
+ self.settings.number_format = len(integer), len(fractional)
@exprs.match('G90')
@header_command
@@ -553,10 +557,17 @@ class ExcellonParser(object):
self.do_interpolation(match)
@exprs.match(';FILE_FORMAT=([0-9]:[0-9])')
- def parse_altium_number_format_comment(self, match):
+ def parse_altium_easyeda_number_format_comment(self, match):
+ # Altium or newer EasyEDA exports
x, _, y = fmt.partition(':')
self.settings.number_format = int(x), int(y)
+ @exprs.match(';Layer: (.*)')
+ def parse_easyeda_layer_name(self, match):
+ # EasyEDA embeds the layer name in a comment. EasyEDA uses separate files for plated/non-plated. The (default?)
+ # layer names are: "Drill PTH", "Drill NPTH"
+ self.is_plated = 'NPTH' not in match[1]
+
@exprs.match(';TYPE=(PLATED|NON_PLATED)')
def parse_altium_composite_plating_comment(self, match):
# These can happen both before a tool definition and before a tool selection statement.
diff --git a/gerbonara/gerber/gerber_statements.py b/gerbonara/gerber/gerber_statements.py
index c2f1934..7c9a301 100644
--- a/gerbonara/gerber/gerber_statements.py
+++ b/gerbonara/gerber/gerber_statements.py
@@ -23,14 +23,6 @@ Gerber (RS-274X) Statements
# FIXME make this entire file obsolete and just return strings from graphical objects directly instead
-def convert(value, src, dst):
- if src == dst or src is None or dst is None or value is None:
- return value
- elif dst == 'mm':
- return value * 25.4
- else:
- return value / 25.4
-
class Statement:
pass
@@ -128,7 +120,7 @@ class CoordStmt(Statement):
def to_gerber(self, settings=None):
ret = ''
for var in 'xyij':
- val = convert(getattr(self, var), self.unit, settings.unit)
+ val = self.unit.to(settings.unit, getattr(self, var))
if val is not None:
ret += var.upper() + settings.write_gerber_value(val)
return ret + self.code + '*'
diff --git a/gerbonara/gerber/graphic_objects.py b/gerbonara/gerber/graphic_objects.py
index 8f2e4b4..e251540 100644
--- a/gerbonara/gerber/graphic_objects.py
+++ b/gerbonara/gerber/graphic_objects.py
@@ -10,7 +10,7 @@ from .gerber_statements import *
def convert(value, src, dst):
if src == dst or src is None or dst is None or value is None:
return value
- elif dst == 'mm':
+ elif dst == MM:
return value * 25.4
else:
return value / 25.4
@@ -27,20 +27,15 @@ class GerberObject:
def converted(self, unit):
return replace(self,
- **{
- f.name: convert(getattr(self, f.name), self.unit, unit)
- for f in fields(self) if type(f.type) is Length
- })
+ **{ f.name: self.unit.to(unit, getattr(self, f.name))
+ for f in fields(self) if type(f.type) is Length })
- def _conv(self, value, unit):
- return convert(value, src=unit, dst=self.unit)
-
- def with_offset(self, dx, dy, unit='mm'):
- dx, dy = self._conv(dx, unit), self._conv(dy, unit)
+ def with_offset(self, dx, dy, unit=MM):
+ dx, dy = self.unit.from(unit, dx), self.unit.from(unit, dy)
return self._with_offset(dx, dy)
- def rotate(self, rotation, cx=0, cy=0, unit='mm'):
- cx, cy = self._conv(cx, unit), self._conv(cy, unit)
+ def rotate(self, rotation, cx=0, cy=0, unit=MM):
+ cx, cy = self.unit.from(unit, cx), self.unit.from(unit, cy)
self._rotate(rotation, cx, cy)
def bounding_box(self, unit=None):
@@ -138,9 +133,10 @@ class Region(GerberObject):
if unit == self.unit:
yield self.poly
else:
- conv_outline = [ (convert(x, self.unit, unit), convert(y, self.unit, unit))
+ to = lambda value: self.unit.to(unit, value)
+ conv_outline = [ (to(x), to(y))
for x, y in self.poly.outline ]
- convert_entry = lambda entry: (entry[0], (convert(entry[1][0], self.unit, unit), convert(entry[1][1], self.unit, unit)))
+ convert_entry = lambda entry: (entry[0], (to(entry[1][0]), to(entry[1][1])))
conv_arc = [ None if entry is None else convert_entry(entry) for entry in self.poly.arc_centers ]
yield gp.ArcPoly(conv_outline, conv_arc)
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py
index 4fad902..4994c59 100644
--- a/gerbonara/gerber/rs274x.py
+++ b/gerbonara/gerber/rs274x.py
@@ -35,21 +35,13 @@ import textwrap
from .gerber_statements import *
from .cam import CamFile, FileSettings
-from .utils import sq_distance, rotate_point
+from .utils import sq_distance, rotate_point, MM, Inch, units
from .aperture_macros.parse import ApertureMacro, GenericMacros
from . import graphic_primitives as gp
from . import graphic_objects as go
from . import apertures
-def convert(value, src, dst):
- if src == dst or src is None or dst is None or value is None:
- return value
- elif dst == 'mm':
- return value * 25.4
- else:
- return value / 25.4
-
def points_close(a, b):
if a == b:
return True
@@ -88,19 +80,19 @@ class GerberFile(CamFile):
self.objects = []
self.import_settings = None
- def to_svg(self, tag=Tag, margin=0, arg_unit='mm', svg_unit='mm', force_bounds=None, color='black'):
+ def to_svg(self, tag=Tag, margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, color='black'):
if force_bounds is None:
(min_x, min_y), (max_x, max_y) = self.bounding_box(svg_unit, default=((0, 0), (0, 0)))
else:
(min_x, min_y), (max_x, max_y) = force_bounds
- min_x = convert(min_x, arg_unit, svg_unit)
- min_y = convert(min_y, arg_unit, svg_unit)
- max_x = convert(max_x, arg_unit, svg_unit)
- max_y = convert(max_y, arg_unit, svg_unit)
+ min_x = arg_unit.to(svg_unit, min_x)
+ min_y = arg_unit.to(svg_unit, min_y)
+ max_x = arg_unit.to(svg_unit, max_x)
+ max_y = arg_unit.to(svg_unit, max_y)
if margin:
- margin = convert(margin, arg_unit, svg_unit)
+ margin = arg_unit.to(svg_unit, margin)
min_x -= margin
min_y -= margin
max_x += margin
@@ -164,7 +156,7 @@ class GerberFile(CamFile):
macro.name = new_name
seen_macro_names.add(new_name)
- def dilate(self, offset, unit='mm', polarity_dark=True):
+ def dilate(self, offset, unit=MM, polarity_dark=True):
self.apertures = [ aperture.dilated(offset, unit) for aperture in self.apertures ]
@@ -204,11 +196,11 @@ class GerberFile(CamFile):
GerberParser(obj, include_dir=enable_include_dir).parse(data)
return obj
- def size(self, unit='mm'):
+ def size(self, unit=MM):
(x0, y0), (x1, y1) = self.bounding_box(unit, default=((0, 0), (0, 0)))
return (x1 - x0, y1 - y0)
- def bounding_box(self, unit='mm', default=None):
+ def bounding_box(self, unit=MM, default=None):
""" Calculate bounding box of file. Returns value given by 'default' argument when there are no graphical
objects (default: None)
"""
@@ -279,12 +271,12 @@ class GerberFile(CamFile):
settings.number_format = (5,6)
return '\n'.join(stmt.to_gerber(settings) for stmt in self.generate_statements())
- def offset(self, dx=0, dy=0, unit='mm'):
+ def offset(self, dx=0, dy=0, unit=MM):
# TODO round offset to file resolution
self.objects = [ obj.with_offset(dx, dy, unit) for obj in self.objects ]
- def rotate(self, angle:'radian', center=(0,0), unit='mm'):
+ def rotate(self, angle:'radian', center=(0,0), unit=MM):
""" Rotate file contents around given point.
Arguments:
@@ -452,12 +444,13 @@ class GraphicsState:
def update_point(self, x, y, unit=None):
old_point = self.point
+ x, y = MM.from(unit, x), MM.from(unit, y)
+
if x is None:
x = self.point[0]
if y is None:
y = self.point[1]
- if unit == 'inch':
- x, y = x*25.4, y*25.4
+
self.point = (x, y)
return old_point
@@ -473,11 +466,8 @@ class GraphicsState:
yield ApertureStmt(self.aperture_map[id(aperture)])
def set_current_point(self, point, unit=None):
+ point_mm = MM.from(unit, point[0]), MM.from(unit, point[1])
# TODO calculate appropriate precision for math.isclose given file_settings.notation
- if unit == 'inch':
- point_mm = point[0]*25.4, point[1]*25.4
- else:
- point_mm = point
if not points_close(self.point, point_mm):
self.point = point_mm
@@ -716,9 +706,9 @@ class GerberParser:
def _parse_unit_mode(self, match):
if match['unit'] == 'MM':
- self.file_settings.unit = 'mm'
+ self.file_settings.unit = MM
else:
- self.file_settings.unit = 'inch'
+ self.file_settings.unit = Inch
def _parse_load_polarity(self, match):
self.graphics_state.polarity_dark = match['polarity'] == 'D'
@@ -754,17 +744,14 @@ class GerberParser:
self.include_stack.pop()
def _parse_image_name(self, match):
- warnings.warn('Deprecated IN (image name) statement found. This deprecated since rev. I4 (Oct 2013).',
- DeprecationWarning)
+ warnings.warn('Deprecated IN (image name) statement found. This deprecated since rev. I4 (Oct 2013).', DeprecationWarning)
self.target.comments.append(f'Image name: {match["name"]}')
def _parse_load_name(self, match):
- warnings.warn('Deprecated LN (load name) statement found. This deprecated since rev. I4 (Oct 2013).',
- DeprecationWarning)
+ warnings.warn('Deprecated LN (load name) statement found. This deprecated since rev. I4 (Oct 2013).', DeprecationWarning)
def _parse_axis_selection(self, match):
- warnings.warn('Deprecated AS (axis selection) statement found. This deprecated since rev. I1 (Dec 2012).',
- DeprecationWarning)
+ warnings.warn('Deprecated AS (axis selection) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning)
self.graphics_state.output_axes = match['axes']
def _parse_image_polarity(self, match):
@@ -774,18 +761,15 @@ class GerberParser:
self.graphics_state.image_polarity = dict(POS='positive', NEG='negative')[match['polarity']]
def _parse_image_rotation(self, match):
- warnings.warn('Deprecated IR (image rotation) statement found. This deprecated since rev. I1 (Dec 2012).',
- DeprecationWarning)
+ warnings.warn('Deprecated IR (image rotation) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning)
self.graphics_state.image_rotation = int(match['rotation'])
def _parse_mirror_image(self, match):
- warnings.warn('Deprecated MI (mirror image) statement found. This deprecated since rev. I1 (Dec 2012).',
- DeprecationWarning)
+ warnings.warn('Deprecated MI (mirror image) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning)
self.graphics_state.mirror = bool(int(match['a'] or '0')), bool(int(match['b'] or '1'))
def _parse_scale_factor(self, match):
- warnings.warn('Deprecated SF (scale factor) statement found. This deprecated since rev. I1 (Dec 2012).',
- DeprecationWarning)
+ warnings.warn('Deprecated SF (scale factor) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning)
a = float(match['a']) if match['a'] else 1.0
b = float(match['b']) if match['b'] else 1.0
self.graphics_state.scale_factor = a, b
@@ -807,16 +791,14 @@ class GerberParser:
self.current_region = None
def _parse_old_unit(self, match):
- self.file_settings.unit = 'inch' if match['mode'] == 'G70' else 'mm'
- warnings.warn(f'Deprecated {match["mode"]} unit mode statement found. This deprecated since 2012.',
- DeprecationWarning)
+ self.file_settings.unit = Inch if match['mode'] == 'G70' else MM
+ warnings.warn(f'Deprecated {match["mode"]} unit mode statement found. This deprecated since 2012.', DeprecationWarning)
self.target.comments.append('Replaced deprecated {match["mode"]} unit mode statement with MO statement')
def _parse_old_notation(self, match):
# FIXME make sure we always have FS at end of processing.
self.file_settings.notation = 'absolute' if match['mode'] == 'G90' else 'incremental'
- warnings.warn(f'Deprecated {match["mode"]} notation mode statement found. This deprecated since 2012.',
- DeprecationWarning)
+ warnings.warn(f'Deprecated {match["mode"]} notation mode statement found. This deprecated since 2012.', DeprecationWarning)
self.target.comments.append('Replaced deprecated {match["mode"]} notation mode statement with FS statement')
def _parse_eof(self, _match):
diff --git a/gerbonara/gerber/utils.py b/gerbonara/gerber/utils.py
index 8e84c87..4074c4e 100644
--- a/gerbonara/gerber/utils.py
+++ b/gerbonara/gerber/utils.py
@@ -110,39 +110,6 @@ def validate_coordinates(position):
if not (isinstance(coord, int) or isinstance(coord, float)):
raise TypeError('Coordinates must be integers or floats')
-
-def metric(value):
- """ Convert inch value to millimeters
-
- Parameters
- ----------
- value : float
- A value in inches.
-
- Returns
- -------
- value : float
- The equivalent value expressed in millimeters.
- """
- return value * MILLIMETERS_PER_INCH
-
-
-def inch(value):
- """ Convert millimeter value to inches
-
- Parameters
- ----------
- value : float
- A value in millimeters.
-
- Returns
- -------
- value : float
- The equivalent value expressed in inches.
- """
- return value / MILLIMETERS_PER_INCH
-
-
def rotate_point(point, angle, center=(0.0, 0.0)):
""" Rotate a point about another point.
@@ -183,11 +150,3 @@ def sq_distance(point1, point2):
diff2 = point1[1] - point2[1]
return diff1 * diff1 + diff2 * diff2
-def convert_units(value, src, dst):
- if src == dst or src is None or dst is None or value is None:
- return value
- elif dst == 'mm':
- return value * 25.4
- else:
- return value / 25.4
-