Excellon, unit conversion WIP
This commit is contained in:
parent
336a18fb49
commit
73a44901c0
10 changed files with 149 additions and 219 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import math
|
|||
from dataclasses import dataclass, replace, fields, InitVar, KW_ONLY
|
||||
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
from .utils import convert_units
|
||||
from .utils import MM, Inch
|
||||
|
||||
from . import graphic_primitives as gp
|
||||
|
||||
|
|
@ -12,11 +12,11 @@ def _flash_hole(self, x, y, unit=None):
|
|||
if getattr(self, 'hole_rect_h', None) is not None:
|
||||
return [*self.primitives(x, y, unit),
|
||||
gp.Rectangle((x, y),
|
||||
(self.convert_to(self.hole_dia, unit), self.convert_to(self.hole_rect_h, unit)),
|
||||
(self.unit.to(unit, self.hole_dia), self.unit.to(unit, self.hole_rect_h)),
|
||||
rotation=self.rotation, polarity_dark=False)]
|
||||
elif self.hole_dia is not None:
|
||||
return [*self.primitives(x, y, unit),
|
||||
gp.Circle(x, y, self.convert_to(self.hole_dia/2, unit), polarity_dark=False)]
|
||||
gp.Circle(x, y, self.unit.to(unit, self.hole_dia/2), polarity_dark=False)]
|
||||
else:
|
||||
return self.primitives(x, y, unit)
|
||||
|
||||
|
|
@ -31,8 +31,6 @@ class Length:
|
|||
def __init__(self, obj_type):
|
||||
self.type = obj_type
|
||||
|
||||
CONVERSION_FACTOR = {None: 1, 'mm': 25.4, 'inch': 1/25.4}
|
||||
|
||||
@dataclass
|
||||
class Aperture:
|
||||
_ : KW_ONLY
|
||||
|
|
@ -45,12 +43,6 @@ class Aperture:
|
|||
else:
|
||||
return 'circle'
|
||||
|
||||
def convert(self, value, unit):
|
||||
return convert_units(value, self.unit, unit)
|
||||
|
||||
def convert_from(self, value, unit):
|
||||
return convert_units(value, unit, self.unit)
|
||||
|
||||
def params(self, unit=None):
|
||||
out = []
|
||||
for f in fields(self):
|
||||
|
|
@ -59,7 +51,7 @@ class Aperture:
|
|||
|
||||
val = getattr(self, f.name)
|
||||
if isinstance(f.type, Length):
|
||||
val = self.convert_to(val, unit)
|
||||
val = self.unit.to(unit, val)
|
||||
out.append(val)
|
||||
|
||||
return out
|
||||
|
|
@ -82,7 +74,7 @@ class Aperture:
|
|||
|
||||
def __eq__(self, other):
|
||||
# We need to choose some unit here.
|
||||
return hasattr(other, to_gerber) and self.to_gerber('mm') == other.to_gerber('mm')
|
||||
return hasattr(other, to_gerber) and self.to_gerber(MM) == other.to_gerber(MM)
|
||||
|
||||
def _rotate_hole_90(self):
|
||||
if self.hole_rect_h is None:
|
||||
|
|
@ -98,7 +90,7 @@ class ExcellonTool(Aperture):
|
|||
depth_offset : Length(float) = 0
|
||||
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.Circle(x, y, self.convert_to(self.diameter/2, unit)) ]
|
||||
return [ gp.Circle(x, y, self.unit.to(unit, self.diameter/2)) ]
|
||||
|
||||
def to_xnc(self, settings):
|
||||
z_off += 'Z' + settings.write_gerber_value(self.depth_offset) if self.depth_offset is not None else ''
|
||||
|
|
@ -121,11 +113,11 @@ class ExcellonTool(Aperture):
|
|||
z_off = '' if self.depth_offset is None else f' z_offset={self.depth_offset}'
|
||||
return f'<Excellon Tool d={self.diameter:.3f}{plated}{z_off}>'
|
||||
|
||||
def equivalent_width(self, unit=None):
|
||||
return self.convert_to(self.diameter, unit)
|
||||
def equivalent_width(self, unit=MM):
|
||||
return self.unit.to(unit, self.diameter)
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
offset = self.convert_from(offset, unit)
|
||||
def dilated(self, offset, unit=MM):
|
||||
offset = self.unit.to(unit, offset)
|
||||
return replace(self, diameter=self.diameter+2*offset)
|
||||
|
||||
def _rotated(self):
|
||||
|
|
@ -134,10 +126,10 @@ class ExcellonTool(Aperture):
|
|||
return self.to_macro(self.rotation)
|
||||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.circle, self.params(unit='mm'))
|
||||
return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM))
|
||||
|
||||
def params(self, unit=None):
|
||||
return self.convert_to(self.diameter, unit)
|
||||
return [self.unit.to(unit, self.diameter)]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -150,7 +142,7 @@ class CircleAperture(Aperture):
|
|||
rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber
|
||||
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.Circle(x, y, self.convert_to(self.diameter/2, unit)) ]
|
||||
return [ gp.Circle(x, y, self.unit.to(unit, self.diameter/2)) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<circle aperture d={self.diameter:.3}>'
|
||||
|
|
@ -158,10 +150,10 @@ class CircleAperture(Aperture):
|
|||
flash = _flash_hole
|
||||
|
||||
def equivalent_width(self, unit=None):
|
||||
return self.convert_to(self.diameter, unit)
|
||||
return self.unit.to(unit, self.diameter)
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
offset = self.convert_from(offset, unit)
|
||||
def dilated(self, offset, unit=MM):
|
||||
offset = self.unit.from(unit, offset)
|
||||
return replace(self, diameter=self.diameter+2*offset, hole_dia=None, hole_rect_h=None)
|
||||
|
||||
def _rotated(self):
|
||||
|
|
@ -171,13 +163,13 @@ class CircleAperture(Aperture):
|
|||
return self.to_macro(self.rotation)
|
||||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.circle, self.params(unit='mm'))
|
||||
return ApertureMacroInstance(GenericMacros.circle, self.params(unit=MM))
|
||||
|
||||
def params(self, unit=None):
|
||||
return strip_right(
|
||||
self.convert_to(self.diameter, unit),
|
||||
self.convert_to(self.hole_dia, unit),
|
||||
self.convert_to(self.hole_rect_h, unit))
|
||||
self.unit.to(unit, self.diameter),
|
||||
self.unit.to(unit, self.hole_dia),
|
||||
self.unit.to(unit, self.hole_rect_h))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -191,7 +183,7 @@ class RectangleAperture(Aperture):
|
|||
rotation : float = 0 # radians
|
||||
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.Rectangle(x, y, self.convert_to(self.w, unit), self.convert_to(self.h, unit), rotation=self.rotation) ]
|
||||
return [ gp.Rectangle(x, y, self.unit.to(unit, self.w), self.unit.to(unit, self.h), rotation=self.rotation) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<rect aperture {self.w:.3}x{self.h:.3}>'
|
||||
|
|
@ -199,10 +191,10 @@ class RectangleAperture(Aperture):
|
|||
flash = _flash_hole
|
||||
|
||||
def equivalent_width(self, unit=None):
|
||||
return self.convert_to(math.sqrt(self.w**2 + self.h**2), unit)
|
||||
return self.unit.to(unit, math.sqrt(self.w**2 + self.h**2))
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
offset = self.convert_from(offset, unit)
|
||||
def dilated(self, offset, unit=MM):
|
||||
offset = self.unit.from(unit, offset)
|
||||
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
|
||||
|
||||
def _rotated(self):
|
||||
|
|
@ -215,18 +207,18 @@ class RectangleAperture(Aperture):
|
|||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.rect,
|
||||
[self.convert_to(self.w, 'mm'),
|
||||
self.convert_to(self.h, 'mm'),
|
||||
self.convert_to(self.hole_dia, 'mm') or 0,
|
||||
self.convert_to(self.hole_rect_h, 'mm') or 0,
|
||||
[self.unit.to(MM, self.w),
|
||||
self.unit.to(MM, self.h),
|
||||
self.unit.to(MM, self.hole_dia) or 0,
|
||||
self.unit.to(MM, self.hole_rect_h) or 0,
|
||||
self.rotation])
|
||||
|
||||
def params(self, unit=None):
|
||||
return strip_right(
|
||||
self.convert_to(self.w, unit),
|
||||
self.convert_to(self.h, unit),
|
||||
self.convert_to(self.hole_dia, unit),
|
||||
self.convert_to(self.hole_rect_h, unit))
|
||||
self.unit.to(unit, self.w),
|
||||
self.unit.to(unit, self.h),
|
||||
self.unit.to(unit, self.hole_dia),
|
||||
self.unit.to(unit, self.hole_rect_h))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -240,15 +232,15 @@ class ObroundAperture(Aperture):
|
|||
rotation : float = 0
|
||||
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.Obround(x, y, self.convert_to(self.w, unit), self.convert_to(self.h, unit), rotation=self.rotation) ]
|
||||
return [ gp.Obround(x, y, self.unit.to(unit, self.w), self.unit.to(unit, self.h), rotation=self.rotation) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<obround aperture {self.w:.3}x{self.h:.3}>'
|
||||
|
||||
flash = _flash_hole
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
offset = self.convert_from(offset, unit)
|
||||
def dilated(self, offset, unit=MM):
|
||||
offset = self.unit.from(unit, offset)
|
||||
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None, hole_rect_h=None)
|
||||
|
||||
def _rotated(self):
|
||||
|
|
@ -263,18 +255,18 @@ class ObroundAperture(Aperture):
|
|||
# generic macro only supports w > h so flip x/y if h > w
|
||||
inst = self if self.w > self.h else replace(self, w=self.h, h=self.w, **_rotate_hole_90(self), rotation=self.rotation-90)
|
||||
return ApertureMacroInstance(GenericMacros.obround,
|
||||
[self.convert_to(inst.w, 'mm'),
|
||||
self.convert_to(ints.h, 'mm'),
|
||||
self.convert_to(inst.hole_dia, 'mm'),
|
||||
self.convert_to(inst.hole_rect_h, 'mm'),
|
||||
[self.unit.to(MM, inst.w),
|
||||
self.unit.to(MM, ints.h),
|
||||
self.unit.to(MM, inst.hole_dia),
|
||||
self.unit.to(MM, inst.hole_rect_h),
|
||||
inst.rotation])
|
||||
|
||||
def params(self, unit=None):
|
||||
return strip_right(
|
||||
self.convert_to(self.w, unit),
|
||||
self.convert_to(self.h, unit),
|
||||
self.convert_to(self.hole_dia, unit),
|
||||
self.convert_to(self.hole_rect_h, unit))
|
||||
self.unit.to(unit, self.w),
|
||||
self.unit.to(unit, self.h),
|
||||
self.unit.to(unit, self.hole_dia),
|
||||
self.unit.to(unit, self.hole_rect_h))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -289,13 +281,13 @@ class PolygonAperture(Aperture):
|
|||
self.n_vertices = int(self.n_vertices)
|
||||
|
||||
def primitives(self, x, y, unit=None):
|
||||
return [ gp.RegularPolygon(x, y, self.convert_to(self.diameter, unit)/2, self.n_vertices, rotation=self.rotation) ]
|
||||
return [ gp.RegularPolygon(x, y, self.unit.to(unit, self.diameter)/2, self.n_vertices, rotation=self.rotation) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<{self.n_vertices}-gon aperture d={self.diameter:.3}'
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
offset = self.convert_from(offset, unit)
|
||||
def dilated(self, offset, unit=MM):
|
||||
offset = self.unit.from(unit, offset)
|
||||
return replace(self, diameter=self.diameter+2*offset, hole_dia=None)
|
||||
|
||||
flash = _flash_hole
|
||||
|
|
@ -304,16 +296,16 @@ class PolygonAperture(Aperture):
|
|||
return self
|
||||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.polygon, self.params('mm'))
|
||||
return ApertureMacroInstance(GenericMacros.polygon, self.params(MM))
|
||||
|
||||
def params(self, unit=None):
|
||||
rotation = self.rotation % (2*math.pi / self.n_vertices) if self.rotation is not None else None
|
||||
if self.hole_dia is not None:
|
||||
return self.convert_to(self.diameter, unit), self.n_vertices, rotation, self.convert_to(self.hole_dia, unit)
|
||||
return self.unit.to(unit, self.diameter), self.n_vertices, rotation, self.unit.to(unit, self.hole_dia)
|
||||
elif rotation is not None and not math.isclose(rotation, 0):
|
||||
return self.convert_to(self.diameter, unit), self.n_vertices, rotation
|
||||
return self.unit.to(unit, self.diameter), self.n_vertices, rotation
|
||||
else:
|
||||
return self.convert_to(self.diameter, unit), self.n_vertices
|
||||
return self.unit.to(unit, self.diameter), self.n_vertices
|
||||
|
||||
@dataclass
|
||||
class ApertureMacroInstance(Aperture):
|
||||
|
|
@ -330,7 +322,7 @@ class ApertureMacroInstance(Aperture):
|
|||
offset=(x, y), rotation=self.rotation,
|
||||
parameters=self.parameters, unit=unit)
|
||||
|
||||
def dilated(self, offset, unit='mm'):
|
||||
def dilated(self, offset, unit=MM):
|
||||
return replace(self, macro=self.macro.dilated(offset, unit))
|
||||
|
||||
def _rotated(self):
|
||||
|
|
|
|||
|
|
@ -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 ''
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 + '*'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue