Unit code refactor WIP
This commit is contained in:
parent
73a44901c0
commit
d85790bc6d
7 changed files with 118 additions and 117 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = ExcellonFile(
|
||||
comments = self.comments.copy(),
|
||||
import_settings = self.import_settings.copy(),
|
||||
objects = [ obj for obj in self.objects if obj.plated ],
|
||||
filename = 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 ]
|
||||
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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue