summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerbonara/gerber/apertures.py88
-rw-r--r--gerbonara/gerber/cam.py2
-rwxr-xr-xgerbonara/gerber/excellon.py79
-rw-r--r--gerbonara/gerber/gerber_statements.py20
-rw-r--r--gerbonara/gerber/graphic_objects.py12
-rw-r--r--gerbonara/gerber/rs274x.py14
-rw-r--r--gerbonara/gerber/utils.py22
7 files changed, 119 insertions, 118 deletions
diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py
index b05457d..3d3c074 100644
--- a/gerbonara/gerber/apertures.py
+++ b/gerbonara/gerber/apertures.py
@@ -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.unit.to(unit, self.hole_dia), self.unit.to(unit, self.hole_rect_h)),
+ (self.unit.convert_to(unit, self.hole_dia), self.unit.convert_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.unit.to(unit, self.hole_dia/2), polarity_dark=False)]
+ gp.Circle(x, y, self.unit.convert_to(unit, self.hole_dia/2), polarity_dark=False)]
else:
return self.primitives(x, y, unit)
@@ -51,7 +51,7 @@ class Aperture:
val = getattr(self, f.name)
if isinstance(f.type, Length):
- val = self.unit.to(unit, val)
+ val = self.unit.convert_to(unit, val)
out.append(val)
return out
@@ -82,7 +82,7 @@ class Aperture:
else:
return {'hole_dia': self.hole_rect_h, 'hole_rect_h': self.hole_dia}
-@dataclass(frozen=True)
+@dataclass(unsafe_hash=True)
class ExcellonTool(Aperture):
human_readable_shape = 'drill'
diameter : Length(float)
@@ -90,7 +90,7 @@ class ExcellonTool(Aperture):
depth_offset : Length(float) = 0
def primitives(self, x, y, unit=None):
- return [ gp.Circle(x, y, self.unit.to(unit, self.diameter/2)) ]
+ return [ gp.Circle(x, y, self.unit.convert_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 ''
@@ -103,10 +103,10 @@ class ExcellonTool(Aperture):
if not self.plated == other.plated:
return False
- if not math.isclose(self.depth_offset, self.unit.from(other.unit, other.depth_offset)):
+ if not math.isclose(self.depth_offset, self.unit(other.depth_offset, other.unit)):
return False
- return math.isclose(self.diameter, self.unit.from(other.unit, other.diameter))
+ return math.isclose(self.diameter, self.unit(other.diameter, other.unit))
def __str__(self):
plated = '' if self.plated is None else (' plated' if self.plated else ' non-plated')
@@ -114,22 +114,20 @@ class ExcellonTool(Aperture):
return f'<Excellon Tool d={self.diameter:.3f}{plated}{z_off}>'
def equivalent_width(self, unit=MM):
- return self.unit.to(unit, self.diameter)
+ return unit(self.diameter, self.unit)
def dilated(self, offset, unit=MM):
- offset = self.unit.to(unit, offset)
+ offset = unit(offset, self.unit)
return replace(self, diameter=self.diameter+2*offset)
def _rotated(self):
return self
- else:
- return self.to_macro(self.rotation)
def to_macro(self):
return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM))
def params(self, unit=None):
- return [self.unit.to(unit, self.diameter)]
+ return [self.unit.convert_to(unit, self.diameter)]
@dataclass
@@ -142,7 +140,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.unit.to(unit, self.diameter/2)) ]
+ return [ gp.Circle(x, y, self.unit.convert_to(unit, self.diameter/2)) ]
def __str__(self):
return f'<circle aperture d={self.diameter:.3}>'
@@ -150,10 +148,10 @@ class CircleAperture(Aperture):
flash = _flash_hole
def equivalent_width(self, unit=None):
- return self.unit.to(unit, self.diameter)
+ return self.unit.convert_to(unit, self.diameter)
def dilated(self, offset, unit=MM):
- offset = self.unit.from(unit, offset)
+ offset = self.unit(offset, unit)
return replace(self, diameter=self.diameter+2*offset, hole_dia=None, hole_rect_h=None)
def _rotated(self):
@@ -167,9 +165,9 @@ class CircleAperture(Aperture):
def params(self, unit=None):
return strip_right(
- self.unit.to(unit, self.diameter),
- self.unit.to(unit, self.hole_dia),
- self.unit.to(unit, self.hole_rect_h))
+ self.unit.convert_to(unit, self.diameter),
+ self.unit.convert_to(unit, self.hole_dia),
+ self.unit.convert_to(unit, self.hole_rect_h))
@dataclass
@@ -183,7 +181,7 @@ class RectangleAperture(Aperture):
rotation : float = 0 # radians
def primitives(self, x, y, unit=None):
- return [ gp.Rectangle(x, y, self.unit.to(unit, self.w), self.unit.to(unit, self.h), rotation=self.rotation) ]
+ return [ gp.Rectangle(x, y, self.unit.convert_to(unit, self.w), self.unit.convert_to(unit, self.h), rotation=self.rotation) ]
def __str__(self):
return f'<rect aperture {self.w:.3}x{self.h:.3}>'
@@ -191,10 +189,10 @@ class RectangleAperture(Aperture):
flash = _flash_hole
def equivalent_width(self, unit=None):
- return self.unit.to(unit, math.sqrt(self.w**2 + self.h**2))
+ return self.unit.convert_to(unit, math.sqrt(self.w**2 + self.h**2))
def dilated(self, offset, unit=MM):
- offset = self.unit.from(unit, offset)
+ offset = self.unit(offset, unit)
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
def _rotated(self):
@@ -207,18 +205,18 @@ class RectangleAperture(Aperture):
def to_macro(self):
return ApertureMacroInstance(GenericMacros.rect,
- [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,
+ [MM(self.w, self.unit),
+ MM(self.h, self.unit),
+ MM(self.hole_dia, self.unit) or 0,
+ MM(self.hole_rect_h, self.unit) or 0,
self.rotation])
def params(self, unit=None):
return strip_right(
- 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))
+ self.unit.convert_to(unit, self.w),
+ self.unit.convert_to(unit, self.h),
+ self.unit.convert_to(unit, self.hole_dia),
+ self.unit.convert_to(unit, self.hole_rect_h))
@dataclass
@@ -232,7 +230,7 @@ class ObroundAperture(Aperture):
rotation : float = 0
def primitives(self, x, y, unit=None):
- return [ gp.Obround(x, y, self.unit.to(unit, self.w), self.unit.to(unit, self.h), rotation=self.rotation) ]
+ return [ gp.Obround(x, y, self.unit.convert_to(unit, self.w), self.unit.convert_to(unit, self.h), rotation=self.rotation) ]
def __str__(self):
return f'<obround aperture {self.w:.3}x{self.h:.3}>'
@@ -240,7 +238,7 @@ class ObroundAperture(Aperture):
flash = _flash_hole
def dilated(self, offset, unit=MM):
- offset = self.unit.from(unit, offset)
+ offset = self.unit(offset, unit)
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
def _rotated(self):
@@ -255,18 +253,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.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])
+ [MM(inst.w, self.unit),
+ MM(ints.h, self.unit),
+ MM(inst.hole_dia, self.unit),
+ MM(inst.hole_rect_h, self.unit),
+ inst.rotation])
def params(self, unit=None):
return strip_right(
- 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))
+ self.unit.convert_to(unit, self.w),
+ self.unit.convert_to(unit, self.h),
+ self.unit.convert_to(unit, self.hole_dia),
+ self.unit.convert_to(unit, self.hole_rect_h))
@dataclass
@@ -281,13 +279,13 @@ class PolygonAperture(Aperture):
self.n_vertices = int(self.n_vertices)
def primitives(self, x, y, unit=None):
- return [ gp.RegularPolygon(x, y, self.unit.to(unit, self.diameter)/2, self.n_vertices, rotation=self.rotation) ]
+ return [ gp.RegularPolygon(x, y, self.unit.convert_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.unit.from(unit, offset)
+ offset = self.unit(offset, unit)
return replace(self, diameter=self.diameter+2*offset, hole_dia=None)
flash = _flash_hole
@@ -301,11 +299,11 @@ class PolygonAperture(Aperture):
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.unit.to(unit, self.diameter), self.n_vertices, rotation, self.unit.to(unit, self.hole_dia)
+ return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation, self.unit.convert_to(unit, self.hole_dia)
elif rotation is not None and not math.isclose(rotation, 0):
- return self.unit.to(unit, self.diameter), self.n_vertices, rotation
+ return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation
else:
- return self.unit.to(unit, self.diameter), self.n_vertices
+ return self.unit.convert_to(unit, self.diameter), self.n_vertices
@dataclass
class ApertureMacroInstance(Aperture):
diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py
index 42a5848..6fe5f9b 100644
--- a/gerbonara/gerber/cam.py
+++ b/gerbonara/gerber/cam.py
@@ -98,7 +98,7 @@ class FileSettings:
""" Convert a floating point number to a Gerber/Excellon-formatted string. """
if unit is not None:
- value = self.unit.from(unit, value)
+ value = self.unit(value, unit)
integer_digits, decimal_digits = self.number_format
if integer_digits is None:
diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py
index 33a3670..0129867 100755
--- a/gerbonara/gerber/excellon.py
+++ b/gerbonara/gerber/excellon.py
@@ -18,16 +18,18 @@
import math
import operator
import warnings
+import functools
from enum import Enum
from dataclasses import dataclass
from collections import Counter
from .cam import CamFile, FileSettings
-from .excellon_statements import *
-from .graphic_objects import Drill, Slot
+from .graphic_objects import Flash, Line, Arc
from .apertures import ExcellonTool
from .utils import Inch, MM
+def parse(data, settings=None):
+ return ExcellonFile.parse(data, settings=settings)
class ExcellonContext:
def __init__(self, settings, tools):
@@ -48,7 +50,7 @@ class ExcellonContext:
yield 'G05'
def route_mode(self, unit, x, y):
- x, y = self.unit.from(unit, x), self.unit.from(unit, y)
+ x, y = self.unit(x, unit), self.unit(y, unit)
if self.mode == ProgramState.ROUTING and (self.x, self.y) == (x, y):
return # nothing to do
@@ -56,15 +58,15 @@ class ExcellonContext:
yield 'G00' + 'X' + self.settings.write_gerber_value(x) + 'Y' + self.settings.write_gerber_value(y)
def set_current_point(self, unit, x, y):
- self.current_point = self.unit.from(unit, x), self.unit.from(unit, y)
+ self.current_point = self.unit(x, unit), self.unit(y, unit)
class ExcellonFile(CamFile):
- def __init__(self, filename=None)
+ def __init__(self, objects=None, comments=None, import_settings=None, filename=None):
super().__init__(filename=filename)
- self.objects = []
- self.comments = []
- self.import_settings = None
+ self.objects = objects or []
+ self.comments = comments or []
+ self.import_settings = import_settings
def _generate_statements(self, settings):
@@ -131,15 +133,17 @@ class ExcellonFile(CamFile):
return len(self.objects)
def split_by_plating(self):
- plated, nonplated = ExcellonFile(self.filename), ExcellonFile(self.filename)
-
- plated.comments = self.comments.copy()
- plated.import_settings = self.import_settings.copy()
- plated.objects = [ obj for obj in self.objects if obj.plated ]
-
- nonplated.comments = self.comments.copy()
- nonplated.import_settings = self.import_settings.copy()
- nonplated.objects = [ obj for obj in self.objects if not obj.plated ]
+ plated = ExcellonFile(
+ comments = self.comments.copy(),
+ import_settings = self.import_settings.copy(),
+ objects = [ obj for obj in self.objects if obj.plated ],
+ filename = self.filename)
+
+ nonplated = ExcellonFile(
+ comments = self.comments.copy(),
+ import_settings = self.import_settings.copy(),
+ objects = [ obj for obj in self.objects if not obj.plated ],
+ filename = self.filename)
return nonplated, plated
@@ -228,18 +232,16 @@ class ExcellonParser(object):
self.drill_down = False
self.is_plated = None
- @property
- def coordinates(self):
- return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
+ @classmethod
+ def parse(kls, data, settings=None):
+ parser = kls(settings)
+ parser._do_parse(data)
- def parse(self, filename):
- with open(filename, 'r') as f:
- data = f.read()
- return self.parse_raw(data, filename)
+ return ExcellonFile(objects=parser.objects, comments=parser.comments, import_settings=settings)
- def parse(self, filelike):
+ def _do_parse(self, data):
leftover = None
- for line in filelike:
+ for line in data.splitlines():
line = line.strip()
if not line:
@@ -266,7 +268,7 @@ class ExcellonParser(object):
# NOTE: These must be kept before the generic comment handler at the end of this class so they match first.
@exprs.match(';T(?P<index1>[0-9]+) Holesize (?P<index2>[0-9]+)\. = (?P<diameter>[0-9/.]+) Tolerance = \+[0-9/.]+/-[0-9/.]+ (?P<plated>PLATED|NON_PLATED|OPTIONAL) (?P<unit>MILS|MM) Quantity = [0-9]+')
- def parse_allegro_tooldef(self, match)
+ def parse_allegro_tooldef(self, match):
# NOTE: We ignore the given tolerances here since they are non-standard.
self.program_state = ProgramState.HEADER # TODO is this needed? we need a test file.
@@ -291,7 +293,7 @@ class ExcellonParser(object):
self.tools[index] = ExcellonTool(diameter=diameter, plated=is_plated, unit=unit)
# Searching Github I found that EasyEDA has two different variants of the unit specification here.
- easyeda_comment = re.compile(';Holesize (?P<index>[0-9]+) = (?P<diameter>[.0-9]+) (?P<unit>INCH|inch|METRIC|mm)')
+ @exprs.match(';Holesize (?P<index>[0-9]+) = (?P<diameter>[.0-9]+) (?P<unit>INCH|inch|METRIC|mm)')
def parse_easyeda_tooldef(self, match):
unit = Inch if match['unit'].lower() == 'inch' else MM
tool = ExcellonTool(diameter=float(match['diameter']), unit=unit, plated=self.is_plated)
@@ -323,7 +325,10 @@ class ExcellonParser(object):
self.active_tool = self.tools[index]
- @exprs.match(r'R(?P<count>[0-9]+)' + xy_coord).match(line)
+ coord = lambda name, key=None: f'(?P<{key or name}>{name}[+-]?[0-9]*\.?[0-9]*)?'
+ xy_coord = coord('X') + coord('Y')
+
+ @exprs.match(r'R(?P<count>[0-9]+)' + xy_coord)
def handle_repeat_hole(self, match):
if self.program_state == ProgramState.HEADER:
return
@@ -358,7 +363,7 @@ class ExcellonParser(object):
@exprs.match('M95')
@header_command
- def handle_end_header(self, match)
+ def handle_end_header(self, match):
self.program_state = ProgramState.DRILLING
@exprs.match('M00')
@@ -387,9 +392,6 @@ class ExcellonParser(object):
# ignore.
# TODO: maybe add warning if this is followed by other commands.
- coord = lambda name, key=None: f'(?P<{key or name}>{name}[+-]?[0-9]*\.?[0-9]*)?'
- xy_coord = coord('X') + coord('Y')
-
def do_move(self, match=None, x='X', y='Y'):
x = settings.parse_gerber_value(match['X'])
y = settings.parse_gerber_value(match['Y'])
@@ -433,13 +435,10 @@ class ExcellonParser(object):
if self.active_tool:
return self.active_tool
- if (self.active_tool := self.tools.get(1)): # FIXME is this necessary? It seems pretty dumb.
- return self.active_tool
-
warnings.warn('Routing command found before first tool definition.', SyntaxWarning)
return None
- @exprs.match('(?P<mode>G01|G02|G03)' + xy_coord + aij_coord)
+ @exprs.match('(?P<mode>G01|G02|G03)' + xy_coord + coord('A') + coord('I') + coord('J'))
def handle_linear_mode(self, match):
if match['mode'] == 'G01':
self.interpolation_mode = InterpMode.LINEAR
@@ -450,7 +449,7 @@ class ExcellonParser(object):
self.do_interpolation(match)
def do_interpolation(self, match):
- x, y, a, i, j = match['x'], match['y'], match['a'], match['i'], match['j']
+ x, y, a, i, j = match['X'], match['Y'], match['A'], match['I'], match['J']
start, end = self.do_move(match)
@@ -579,6 +578,6 @@ class ExcellonParser(object):
self.program_state = ProgramState.HEADER
@exprs.match(';(.*)')
- def parse_comment(self, match)
- target.comments.append(match[1].strip())
+ def parse_comment(self, match):
+ self.comments.append(match[1].strip())
diff --git a/gerbonara/gerber/gerber_statements.py b/gerbonara/gerber/gerber_statements.py
index 7c9a301..55a4bf3 100644
--- a/gerbonara/gerber/gerber_statements.py
+++ b/gerbonara/gerber/gerber_statements.py
@@ -59,7 +59,7 @@ class LoadPolarityStmt(ParamStmt):
def __init__(self, dark):
self.dark = dark
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
lp = 'D' if self.dark else 'C'
return f'%LP{lp}*%'
@@ -75,7 +75,7 @@ class ApertureDefStmt(ParamStmt):
self.number = number
self.aperture = aperture
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
return f'%ADD{self.number}{self.aperture.to_gerber(settings)}*%'
def __str__(self):
@@ -91,7 +91,7 @@ class ApertureMacroStmt(ParamStmt):
def __init__(self, macro):
self.macro = macro
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
unit = settings.unit if settings else None
return f'%AM{self.macro.name}*\n{self.macro.to_gerber(unit=unit)}*\n%'
@@ -117,10 +117,10 @@ class CoordStmt(Statement):
self.x, self.y, self.i, self.j = x, y, i, j
self.unit = unit
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
ret = ''
for var in 'xyij':
- val = self.unit.to(settings.unit, getattr(self, var))
+ val = self.unit.convert_to(settings.unit, getattr(self, var))
if val is not None:
ret += var.upper() + settings.write_gerber_value(val)
return ret + self.code + '*'
@@ -145,7 +145,7 @@ class FlashStmt(CoordStmt):
class InterpolationModeStmt(Statement):
""" G01 / G02 / G03 interpolation mode statement """
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
return self.code + '*'
def __str__(self):
@@ -179,7 +179,7 @@ class ApertureStmt(Statement):
def __init__(self, d):
self.d = int(d)
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
return 'D{0}*'.format(self.d)
def __str__(self):
@@ -192,7 +192,7 @@ class CommentStmt(Statement):
def __init__(self, comment):
self.comment = comment if comment is not None else ""
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
return f'G04{self.comment}*'
def __str__(self):
@@ -202,7 +202,7 @@ class CommentStmt(Statement):
class EofStmt(Statement):
""" M02 EOF Statement """
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
return 'M02*'
def __str__(self):
@@ -212,7 +212,7 @@ class UnknownStmt(Statement):
def __init__(self, line):
self.line = line
- def to_gerber(self, settings=None):
+ def to_gerber(self, settings):
return self.line
def __str__(self):
diff --git a/gerbonara/gerber/graphic_objects.py b/gerbonara/gerber/graphic_objects.py
index e251540..f97aff4 100644
--- a/gerbonara/gerber/graphic_objects.py
+++ b/gerbonara/gerber/graphic_objects.py
@@ -27,15 +27,15 @@ class GerberObject:
def converted(self, unit):
return replace(self,
- **{ f.name: self.unit.to(unit, getattr(self, f.name))
+ **{ f.name: self.unit.convert_to(unit, getattr(self, f.name))
for f in fields(self) if type(f.type) is Length })
def with_offset(self, dx, dy, unit=MM):
- dx, dy = self.unit.from(unit, dx), self.unit.from(unit, dy)
+ dx, dy = self.unit(dx, unit), self.unit(dy, unit)
return self._with_offset(dx, dy)
def rotate(self, rotation, cx=0, cy=0, unit=MM):
- cx, cy = self.unit.from(unit, cx), self.unit.from(unit, cy)
+ cx, cy = self.unit(cx, unit), self.unit(cy, unit)
self._rotate(rotation, cx, cy)
def bounding_box(self, unit=None):
@@ -133,7 +133,7 @@ class Region(GerberObject):
if unit == self.unit:
yield self.poly
else:
- to = lambda value: self.unit.to(unit, value)
+ to = lambda value: self.unit.convert_to(unit, value)
conv_outline = [ (to(x), to(y))
for x, y in self.poly.outline ]
convert_entry = lambda entry: (entry[0], (to(entry[1][0]), to(entry[1][1])))
@@ -219,7 +219,7 @@ class Line(GerberObject):
ctx.set_current_point(self.unit, *self.p2)
def curve_length(self, unit=MM):
- return self.unit.to(unit, math.dist(self.p1, self.p2))
+ return self.unit.convert_to(unit, math.dist(self.p1, self.p2))
@dataclass
@@ -307,6 +307,6 @@ class Arc(GerberObject):
if f > math.pi:
f = 2*math.pi - f
- return self.unit.to(unit, 2*math.pi*r * (f/math.pi))
+ return self.unit.convert_to(unit, 2*math.pi*r * (f/math.pi))
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py
index 4994c59..42d7f81 100644
--- a/gerbonara/gerber/rs274x.py
+++ b/gerbonara/gerber/rs274x.py
@@ -86,13 +86,13 @@ class GerberFile(CamFile):
(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 = 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)
+ min_x = svg_unit(min_x, arg_unit)
+ min_y = svg_unit(min_y, arg_unit)
+ max_x = svg_unit(max_x, arg_unit)
+ max_y = svg_unit(max_y, arg_unit)
if margin:
- margin = arg_unit.to(svg_unit, margin)
+ margin = svg_unit(margin, arg_unit)
min_x -= margin
min_y -= margin
max_x += margin
@@ -444,7 +444,7 @@ class GraphicsState:
def update_point(self, x, y, unit=None):
old_point = self.point
- x, y = MM.from(unit, x), MM.from(unit, y)
+ x, y = MM(x, unit), MM(y, unit)
if x is None:
x = self.point[0]
@@ -466,7 +466,7 @@ 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])
+ point_mm = MM(point[0], unit), MM(point[1], unit)
# TODO calculate appropriate precision for math.isclose given file_settings.notation
if not points_close(self.point, point_mm):
diff --git a/gerbonara/gerber/utils.py b/gerbonara/gerber/utils.py
index 4074c4e..533f0a2 100644
--- a/gerbonara/gerber/utils.py
+++ b/gerbonara/gerber/utils.py
@@ -27,13 +27,13 @@ import os
from math import radians, sin, cos, sqrt, atan2, pi
-class Unit:
+class LengthUnit:
def __init__(self, name, shorthand, this_in_mm):
self.name = name
self.shorthand = shorthand
self.factor = this_in_mm
- def from(self, unit, value):
+ def convert_from(self, unit, value):
if isinstance(unit, str):
unit = units[unit]
@@ -42,26 +42,30 @@ class Unit:
return value * unit.factor / self.factor
- def to(self, unit, value):
+ def convert_to(self, unit, value):
if isinstance(unit, str):
- unit = units[unit]
+ unit = to_unit(unit)
if unit is None:
return value
- return unit.from(self, value)
+ return unit.convert_from(self, value)
+
+ def __call__(self, value, unit):
+ return self.convert_from(unit, value)
def __eq__(self, other):
if isinstance(other, str):
return other.lower() in (self.name, self.shorthand)
else:
- return self == other
+ return id(self) == id(other)
MILLIMETERS_PER_INCH = 25.4
-Inch = Unit('inch', 'in', MILLIMETERS_PER_INCH)
-MM = Unit('millimeter', 'mm', 1)
-units = {'inch': Inch, 'mm': MM}
+Inch = LengthUnit('inch', 'in', MILLIMETERS_PER_INCH)
+MM = LengthUnit('millimeter', 'mm', 1)
+units = {'inch': Inch, 'mm': MM, None: None}
+to_unit = lambda name: units[name]
def decimal_string(value, precision=6, padding=False):