From d85790bc6d40deb1d52cc5d8a4c178f664635625 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 18 Jan 2022 01:10:16 +0100 Subject: Unit code refactor WIP --- gerbonara/gerber/apertures.py | 88 +++++++++++++++++------------------ gerbonara/gerber/cam.py | 2 +- gerbonara/gerber/excellon.py | 79 ++++++++++++++++--------------- gerbonara/gerber/gerber_statements.py | 20 ++++---- gerbonara/gerber/graphic_objects.py | 12 ++--- gerbonara/gerber/rs274x.py | 14 +++--- gerbonara/gerber/utils.py | 22 +++++---- 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'' 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'' @@ -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'' @@ -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'' @@ -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[0-9]+) Holesize (?P[0-9]+)\. = (?P[0-9/.]+) Tolerance = \+[0-9/.]+/-[0-9/.]+ (?PPLATED|NON_PLATED|OPTIONAL) (?PMILS|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[0-9]+) = (?P[.0-9]+) (?PINCH|inch|METRIC|mm)') + @exprs.match(';Holesize (?P[0-9]+) = (?P[.0-9]+) (?PINCH|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[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[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('(?PG01|G02|G03)' + xy_coord + aij_coord) + @exprs.match('(?PG01|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): -- cgit