From 73a44901c0ef0e94e9465c2f35750ca6f85a4473 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 17 Jan 2022 23:14:52 +0100 Subject: Excellon, unit conversion WIP --- gerbonara/gerber/aperture_macros/expression.py | 7 +- gerbonara/gerber/aperture_macros/parse.py | 3 +- gerbonara/gerber/aperture_macros/primitive.py | 8 -- gerbonara/gerber/apertures.py | 110 ++++++++++++------------- gerbonara/gerber/cam.py | 6 ++ gerbonara/gerber/excellon.py | 87 ++++++++++--------- gerbonara/gerber/gerber_statements.py | 10 +-- gerbonara/gerber/graphic_objects.py | 24 +++--- gerbonara/gerber/rs274x.py | 72 ++++++---------- gerbonara/gerber/utils.py | 41 --------- 10 files changed, 149 insertions(+), 219 deletions(-) (limited to 'gerbonara') 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'' - 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'' @@ -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'' @@ -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'' 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 - -- cgit