From 778e81974580d910eac5e3f977acf79744d3e085 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 29 Apr 2023 01:00:45 +0200 Subject: Freeze apertures and aperture macros, make gerbonara faster --- gerbonara/aperture_macros/expression.py | 68 ++++++----- gerbonara/aperture_macros/parse.py | 183 ++++++++++++++---------------- gerbonara/aperture_macros/primitive.py | 194 +++++++++++++++++--------------- 3 files changed, 225 insertions(+), 220 deletions(-) (limited to 'gerbonara/aperture_macros') diff --git a/gerbonara/aperture_macros/expression.py b/gerbonara/aperture_macros/expression.py index 0b2168f..63f1e42 100644 --- a/gerbonara/aperture_macros/expression.py +++ b/gerbonara/aperture_macros/expression.py @@ -3,17 +3,20 @@ # Copyright 2021 Jan Sebastian Götte +from dataclasses import dataclass import operator import re import ast -from ..utils import MM, Inch, MILLIMETERS_PER_INCH +from ..utils import LengthUnit, MM, Inch, MILLIMETERS_PER_INCH def expr(obj): return obj if isinstance(obj, Expression) else ConstantExpression(obj) +_make_expr = expr +@dataclass(frozen=True, slots=True) class Expression: def optimized(self, variable_binding={}): return self @@ -63,13 +66,18 @@ class Expression: def __pos__(self): return self + +@dataclass(frozen=True, slots=True) class UnitExpression(Expression): + expr: Expression + unit: LengthUnit + def __init__(self, expr, unit): - if isinstance(expr, Expression): - self._expr = expr - else: - self._expr = ConstantExpression(expr) - self.unit = unit + expr = _make_expr(expr) + if isinstance(expr, UnitExpression): + expr = expr.converted(unit) + object.__setattr__(self, 'expr', expr) + object.__setattr__(self, 'unit', unit) def to_gerber(self, unit=None): return self.converted(unit).optimized().to_gerber() @@ -77,23 +85,23 @@ class UnitExpression(Expression): def __eq__(self, other): return type(other) == type(self) and \ self.unit == other.unit and\ - self._expr == other._expr + self.expr == other.expr def __str__(self): - return f'<{self._expr.to_gerber()} {self.unit}>' + return f'<{self.expr.to_gerber()} {self.unit}>' def __repr__(self): - return f'' + return f'' def converted(self, unit): if self.unit is None or unit is None or self.unit == unit: - return self._expr + return self.expr elif MM == unit: - return self._expr * MILLIMETERS_PER_INCH + return self.expr * MILLIMETERS_PER_INCH elif Inch == unit: - return self._expr / MILLIMETERS_PER_INCH + return self.expr / MILLIMETERS_PER_INCH else: raise ValueError(f'invalid unit {unit}, must be "inch" or "mm".') @@ -103,12 +111,12 @@ class UnitExpression(Expression): raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.') if self.unit == other.unit or self.unit is None or other.unit is None: - return UnitExpression(self._expr + other._expr, self.unit) + return UnitExpression(self.expr + other.expr, self.unit) if other.unit == 'mm': # -> and self.unit == 'inch' - return UnitExpression(self._expr + (other._expr / MILLIMETERS_PER_INCH), self.unit) + return UnitExpression(self.expr + (other.expr / MILLIMETERS_PER_INCH), self.unit) else: # other.unit == 'inch' and self.unit == 'mm' - return UnitExpression(self._expr + (other._expr * MILLIMETERS_PER_INCH), self.unit) + return UnitExpression(self.expr + (other.expr * MILLIMETERS_PER_INCH), self.unit) def __radd__(self, other): # left hand side cannot have been an UnitExpression or __radd__ would not have been called @@ -122,27 +130,26 @@ class UnitExpression(Expression): raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.') def __mul__(self, other): - return UnitExpression(self._expr * other, self.unit) + return UnitExpression(self.expr * other, self.unit) def __rmul__(self, other): - return UnitExpression(other * self._expr, self.unit) + return UnitExpression(other * self.expr, self.unit) def __truediv__(self, other): - return UnitExpression(self._expr / other, self.unit) + return UnitExpression(self.expr / other, self.unit) def __rtruediv__(self, other): - return UnitExpression(other / self._expr, self.unit) + return UnitExpression(other / self.expr, self.unit) def __neg__(self): - return UnitExpression(-self._expr, self.unit) + return UnitExpression(-self.expr, self.unit) def __pos__(self): return self - +@dataclass(frozen=True, slots=True) class ConstantExpression(Expression): - def __init__(self, value): - self.value = value + value: float def __float__(self): return float(self.value) @@ -154,9 +161,9 @@ class ConstantExpression(Expression): return f'{self.value:.6f}'.rstrip('0').rstrip('.') +@dataclass(frozen=True, slots=True) class VariableExpression(Expression): - def __init__(self, number): - self.number = number + number: int def optimized(self, variable_binding={}): if self.number in variable_binding: @@ -171,11 +178,16 @@ class VariableExpression(Expression): return f'${self.number}' +@dataclass(frozen=True, slots=True) class OperatorExpression(Expression): + op: str + l: Expression + r: Expression + def __init__(self, op, l, r): - self.op = op - self.l = ConstantExpression(l) if isinstance(l, (int, float)) else l - self.r = ConstantExpression(r) if isinstance(r, (int, float)) else r + object.__setattr__(self, 'op', op) + object.__setattr__(self, 'l', expr(l)) + object.__setattr__(self, 'r', expr(r)) def __eq__(self, other): return type(self) == type(other) and \ diff --git a/gerbonara/aperture_macros/parse.py b/gerbonara/aperture_macros/parse.py index 84c35e0..72703ae 100644 --- a/gerbonara/aperture_macros/parse.py +++ b/gerbonara/aperture_macros/parse.py @@ -3,6 +3,7 @@ # Copyright 2021 Jan Sebastian Götte +from dataclasses import dataclass, field, replace import operator import re import ast @@ -46,16 +47,23 @@ def _parse_expression(expr): raise SyntaxError('Invalid aperture macro expression') from e return _map_expression(parsed) +@dataclass(frozen=True, slots=True) class ApertureMacro: - def __init__(self, name=None, primitives=None, variables=None): - self._name = name - self.comments = [] - self.variables = variables or {} - self.primitives = primitives or [] + name: str = None + primitives: tuple = () + variables: tuple = () + comments: tuple = () + + def __post_init__(self): + if self.name is None: + # We can't use field(default_factory=...) here because that factory doesn't get a reference to the instance. + object.__setattr__(self, 'name', f'gn_{hash(self):x}') @classmethod def parse_macro(cls, name, body, unit): - macro = cls(name) + comments = [] + variables = {} + primitives = [] blocks = body.split('*') for block in blocks: @@ -63,7 +71,7 @@ class ApertureMacro: continue if block.startswith('0 '): # comment - macro.comments.append(block[2:]) + comments.append(block[2:]) continue block = re.sub(r'\s', '', block) @@ -71,28 +79,18 @@ class ApertureMacro: if block[0] == '$': # variable definition name, expr = block.partition('=') number = int(name[1:]) - if number in macro.variables: + if number in variables: raise SyntaxError(f'Re-definition of aperture macro variable {number} inside macro') - macro.variables[number] = _parse_expression(expr) + variables[number] = _parse_expression(expr) else: # primitive primitive, *args = block.split(',') args = [ _parse_expression(arg) for arg in args ] primitive = ap.PRIMITIVE_CLASSES[int(primitive)](unit=unit, args=args) - macro.primitives.append(primitive) - - return macro - - @property - def name(self): - if self._name is not None: - return self._name - else: - return f'gn_{hash(self)}' + primitives.append(primitive) - @name.setter - def name(self, name): - self._name = name + variables = [variables.get(i+1) for i in range(max(variables.keys()))] + return kls(name, tuple(primitives), tuple(variables), tuple(primitives)) def __str__(self): return f'' @@ -100,54 +98,41 @@ class ApertureMacro: def __repr__(self): return str(self) - def __eq__(self, other): - return hasattr(other, 'to_gerber') and self.to_gerber() == other.to_gerber() - - def __hash__(self): - return hash(self.to_gerber()) - def dilated(self, offset, unit=MM): - dup = copy.deepcopy(self) new_primitives = [] - for primitive in dup.primitives: + for primitive in self.primitives: try: if primitive.exposure.calculate(): - primitive.dilate(offset, unit) - new_primitives.append(primitive) + new_primitives += primitive.dilated(offset, unit) except IndexError: warnings.warn('Cannot dilate aperture macro primitive with exposure value computed from macro variable.') pass - dup.primitives = new_primitives - return dup + return replace(self, primitives=tuple(new_primitives)) def to_gerber(self, unit=None): comments = [ str(c) for c in self.comments ] - variable_defs = [ f'${var.to_gerber(unit)}={expr}' for var, expr in self.variables.items() ] + variable_defs = [ f'${var.to_gerber(unit)}={expr}' for var, expr in enumerate(self.variables, start=1) ] primitive_defs = [ prim.to_gerber(unit) for prim in self.primitives ] return '*\n'.join(comments + variable_defs + primitive_defs) def to_graphic_primitives(self, offset, rotation, parameters : [float], unit=None, polarity_dark=True): - variables = dict(self.variables) + variables = {i: v for i, v in enumerate(self.variables, start=1)} for number, value in enumerate(parameters, start=1): if number in variables: - raise SyntaxError(f'Re-definition of aperture macro variable {i} through parameter {value}') + raise SyntaxError(f'Re-definition of aperture macro variable {number} through parameter {value}') variables[number] = value for primitive in self.primitives: yield from primitive.to_graphic_primitives(offset, rotation, variables, unit, polarity_dark) def rotated(self, angle): - dup = copy.deepcopy(self) - for primitive in dup.primitives: - # aperture macro primitives use degree counter-clockwise, our API uses radians clockwise - primitive.rotation -= rad_to_deg(angle) - return dup + # aperture macro primitives use degree counter-clockwise, our API uses radians clockwise + return replace(self, primitives=tuple( + replace(primitive, rotation=primitive.rotation - rad_to_deg(angle)) for primitive in self.primitives)) def scaled(self, scale): - dup = copy.deepcopy(self) - for primitive in dup.primitives: - primitive.scale(scale) - return dup + return replace(self, primitives=tuple( + primitive.scaled(scale) for primitive in self.primitives)) var = VariableExpression @@ -155,83 +140,81 @@ deg_per_rad = 180 / math.pi class GenericMacros: - _generic_hole = lambda n: [ - ap.Circle('mm', [0, var(n), 0, 0]), - ap.CenterLine('mm', [0, var(n), var(n+1), 0, 0, var(n+2) * -deg_per_rad])] + _generic_hole = lambda n: (ap.Circle('mm', 0, var(n), 0, 0),) # NOTE: All generic macros have rotation values specified in **clockwise radians** like the rest of the user-facing # API. - circle = ApertureMacro('GNC', [ - ap.Circle('mm', [1, var(1), 0, 0, var(4) * -deg_per_rad]), - *_generic_hole(2)]) + circle = ApertureMacro('GNC', ( + ap.Circle('mm', 1, var(1), 0, 0, var(4) * -deg_per_rad), + *_generic_hole(2))) - rect = ApertureMacro('GNR', [ - ap.CenterLine('mm', [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]), - *_generic_hole(3)]) + rect = ApertureMacro('GNR', ( + ap.CenterLine('mm', 1, var(1), var(2), 0, 0, var(5) * -deg_per_rad), + *_generic_hole(3))) # params: width, height, corner radius, *hole, rotation - rounded_rect = ApertureMacro('GRR', [ - ap.CenterLine('mm', [1, var(1)-2*var(3), var(2), 0, 0, var(6) * -deg_per_rad]), - ap.CenterLine('mm', [1, var(1), var(2)-2*var(3), 0, 0, var(6) * -deg_per_rad]), - ap.Circle('mm', [1, var(3)*2, +(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad]), - ap.Circle('mm', [1, var(3)*2, +(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad]), - ap.Circle('mm', [1, var(3)*2, -(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad]), - ap.Circle('mm', [1, var(3)*2, -(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad]), - *_generic_hole(4)]) + rounded_rect = ApertureMacro('GRR', ( + ap.CenterLine('mm', 1, var(1)-2*var(3), var(2), 0, 0, var(6) * -deg_per_rad), + ap.CenterLine('mm', 1, var(1), var(2)-2*var(3), 0, 0, var(6) * -deg_per_rad), + ap.Circle('mm', 1, var(3)*2, +(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad), + ap.Circle('mm', 1, var(3)*2, +(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad), + ap.Circle('mm', 1, var(3)*2, -(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad), + ap.Circle('mm', 1, var(3)*2, -(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad), + *_generic_hole(4))) # params: width, height, length difference between narrow side (top) and wide side (bottom), *hole, rotation - isosceles_trapezoid = ApertureMacro('GTR', [ - ap.Outline('mm', [1, 4, - var(1)/-2, var(2)/-2, + isosceles_trapezoid = ApertureMacro('GTR', ( + ap.Outline('mm', 1, 4, + (var(1)/-2, var(2)/-2, var(1)/-2+var(3)/2, var(2)/2, var(1)/2-var(3)/2, var(2)/2, var(1)/2, var(2)/-2, - var(1)/-2, var(2)/-2, - var(6) * -deg_per_rad]), - *_generic_hole(4)]) + var(1)/-2, var(2)/-2,), + var(6) * -deg_per_rad), + *_generic_hole(4))) # params: width, height, length difference between narrow side (top) and wide side (bottom), margin, *hole, rotation - rounded_isosceles_trapezoid = ApertureMacro('GRTR', [ - ap.Outline('mm', [1, 4, - var(1)/-2, var(2)/-2, + rounded_isosceles_trapezoid = ApertureMacro('GRTR', ( + ap.Outline('mm', 1, 4, + (var(1)/-2, var(2)/-2, var(1)/-2+var(3)/2, var(2)/2, var(1)/2-var(3)/2, var(2)/2, var(1)/2, var(2)/-2, + var(1)/-2, var(2)/-2,), + var(6) * -deg_per_rad), + ap.VectorLine('mm', 1, var(4)*2, var(1)/-2, var(2)/-2, - var(6) * -deg_per_rad]), - ap.VectorLine('mm', [1, var(4)*2, - var(1)/-2, var(2)/-2, - var(1)/-2+var(3)/2, var(2)/2,]), - ap.VectorLine('mm', [1, var(4)*2, + var(1)/-2+var(3)/2, var(2)/2,), + ap.VectorLine('mm', 1, var(4)*2, var(1)/-2+var(3)/2, var(2)/2, - var(1)/2-var(3)/2, var(2)/2,]), - ap.VectorLine('mm', [1, var(4)*2, + var(1)/2-var(3)/2, var(2)/2,), + ap.VectorLine('mm', 1, var(4)*2, var(1)/2-var(3)/2, var(2)/2, - var(1)/2, var(2)/-2,]), - ap.VectorLine('mm', [1, var(4)*2, + var(1)/2, var(2)/-2,), + ap.VectorLine('mm', 1, var(4)*2, var(1)/2, var(2)/-2, - var(1)/-2, var(2)/-2,]), - ap.Circle('mm', [1, var(4)*2, - var(1)/-2, var(2)/-2,]), - ap.Circle('mm', [1, var(4)*2, - var(1)/-2+var(3)/2, var(2)/2,]), - ap.Circle('mm', [1, var(4)*2, - var(1)/2-var(3)/2, var(2)/2,]), - ap.Circle('mm', [1, var(4)*2, - var(1)/2, var(2)/-2,]), - *_generic_hole(5)]) + var(1)/-2, var(2)/-2,), + ap.Circle('mm', 1, var(4)*2, + var(1)/-2, var(2)/-2,), + ap.Circle('mm', 1, var(4)*2, + var(1)/-2+var(3)/2, var(2)/2,), + ap.Circle('mm', 1, var(4)*2, + var(1)/2-var(3)/2, var(2)/2,), + ap.Circle('mm', 1, var(4)*2, + var(1)/2, var(2)/-2,), + *_generic_hole(5))) # w must be larger than h # params: width, height, *hole, rotation - obround = ApertureMacro('GNO', [ - ap.CenterLine('mm', [1, var(1)-var(2), var(2), 0, 0, var(5) * -deg_per_rad]), - ap.Circle('mm', [1, var(2), +(var(1)-var(2))/2, 0, var(5) * -deg_per_rad]), - ap.Circle('mm', [1, var(2), -(var(1)-var(2))/2, 0, var(5) * -deg_per_rad]), - *_generic_hole(3) ]) - - polygon = ApertureMacro('GNP', [ - ap.Polygon('mm', [1, var(2), 0, 0, var(1), var(3) * -deg_per_rad]), - ap.Circle('mm', [0, var(4), 0, 0])]) + obround = ApertureMacro('GNO', ( + ap.CenterLine('mm', 1, var(1)-var(2), var(2), 0, 0, var(5) * -deg_per_rad), + ap.Circle('mm', 1, var(2), +(var(1)-var(2))/2, 0, var(5) * -deg_per_rad), + ap.Circle('mm', 1, var(2), -(var(1)-var(2))/2, 0, var(5) * -deg_per_rad), + *_generic_hole(3) )) + + polygon = ApertureMacro('GNP', ( + ap.Polygon('mm', 1, var(2), 0, 0, var(1), var(3) * -deg_per_rad), + ap.Circle('mm', 0, var(4), 0, 0))) if __name__ == '__main__': diff --git a/gerbonara/aperture_macros/primitive.py b/gerbonara/aperture_macros/primitive.py index e424623..5700743 100644 --- a/gerbonara/aperture_macros/primitive.py +++ b/gerbonara/aperture_macros/primitive.py @@ -7,12 +7,13 @@ import warnings import contextlib import math +from dataclasses import dataclass, fields from .expression import Expression, UnitExpression, ConstantExpression, expr from .. import graphic_primitives as gp from .. import graphic_objects as go -from ..utils import rotate_point +from ..utils import rotate_point, LengthUnit def point_distance(a, b): @@ -30,24 +31,20 @@ def rad_to_deg(a): return a * (180 / math.pi) +@dataclass(frozen=True, slots=True) class Primitive: - def __init__(self, unit, args): - self.unit = unit - - if len(args) > len(type(self).__annotations__): - raise ValueError(f'Too many arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})') - - for arg, (name, fieldtype) in zip(args, type(self).__annotations__.items()): - arg = expr(arg) # convert int/float to Expression object - - if fieldtype == UnitExpression: - setattr(self, name, UnitExpression(arg, unit)) - else: - setattr(self, name, arg) + unit: LengthUnit + exposure : Expression - for name in type(self).__annotations__: - if not hasattr(self, name): - raise ValueError(f'Too few arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})') + def __post_init__(self): + for field in fields(self): + if field.type == UnitExpression: + value = getattr(self, field.name) + if not isinstance(value, UnitExpression): + value = UnitExpression(expr(value), self.unit) + object.__setattr__(self, field.name, value) + elif field.type == Expression: + object.__setattr__(self, field.name, expr(getattr(self, field.name))) def to_gerber(self, unit=None): return f'{self.code},' + ','.join( @@ -60,6 +57,10 @@ class Primitive: def __repr__(self): return str(self) + @classmethod + def from_arglist(kls, arglist): + return kls(*arglist) + class Calculator: def __init__(self, instance, variable_binding={}, unit=None): self.instance = instance @@ -79,19 +80,14 @@ class Primitive: return expr.calculate(self.variable_binding, self.unit) +@dataclass(frozen=True, slots=True) class Circle(Primitive): code = 1 - exposure : Expression diameter : UnitExpression # center x/y x : UnitExpression y : UnitExpression - rotation : Expression = None - - def __init__(self, unit, args): - super().__init__(unit, args) - if self.rotation is None: - self.rotation = ConstantExpression(0) + rotation : Expression = 0 def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): with self.Calculator(self, variable_binding, unit) as calc: @@ -99,24 +95,23 @@ class Circle(Primitive): x, y = x+offset[0], y+offset[1] return [ gp.Circle(x, y, calc.diameter/2, polarity_dark=(bool(calc.exposure) == polarity_dark)) ] - def dilate(self, offset, unit): - self.diameter += UnitExpression(offset, unit) + def dilated(self, offset, unit): + return replace(self, diameter=self.diameter + UnitExpression(offset, unit)) - def scale(self, scale): - self.x *= UnitExpression(scale) - self.y *= UnitExpression(scale) - self.diameter *= UnitExpression(scale) + def scaled(self, scale): + return replace(self, x=self.x * UnitExpression(scale), y=self.y * UnitExpression(scale), + diameter=self.diameter * UnitExpression(scale)) +@dataclass(frozen=True, slots=True) class VectorLine(Primitive): code = 20 - exposure : Expression width : UnitExpression start_x : UnitExpression start_y : UnitExpression end_x : UnitExpression end_y : UnitExpression - rotation : Expression = None + rotation : Expression = 0 def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): with self.Calculator(self, variable_binding, unit) as calc: @@ -133,25 +128,26 @@ class VectorLine(Primitive): return [ gp.Rectangle(center_x, center_y, length, calc.width, rotation=rotation, polarity_dark=(bool(calc.exposure) == polarity_dark)) ] - def dilate(self, offset, unit): - self.width += UnitExpression(2*offset, unit) + def dilated(self, offset, unit): + return replace(self, width=self.width + UnitExpression(2*offset, unit)) - def scale(self, scale): - self.start_x *= UnitExpression(scale) - self.start_y *= UnitExpression(scale) - self.end_x *= UnitExpression(scale) - self.end_y *= UnitExpression(scale) + def scaled(self, scale): + return replace(self, + start_x=self.start_x * UnitExpression(scale), + start_y=self.start_y * UnitExpression(scale), + end_x=self.end_x * UnitExpression(scale), + end_y=self.end_y * UnitExpression(scale)) +@dataclass(frozen=True, slots=True) class CenterLine(Primitive): code = 21 - exposure : Expression width : UnitExpression height : UnitExpression # center x/y - x : UnitExpression - y : UnitExpression - rotation : Expression + x : UnitExpression = 0 + y : UnitExpression = 0 + rotation : Expression = 0 def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): with self.Calculator(self, variable_binding, unit) as calc: @@ -162,25 +158,26 @@ class CenterLine(Primitive): return [ gp.Rectangle(x, y, w, h, rotation, polarity_dark=(bool(calc.exposure) == polarity_dark)) ] - def dilate(self, offset, unit): - self.width += UnitExpression(2*offset, unit) + def dilated(self, offset, unit): + return replace(self, width=self.width + UnitExpression(2*offset, unit)) - def scale(self, scale): - self.width *= UnitExpression(scale) - self.height *= UnitExpression(scale) - self.x *= UnitExpression(scale) - self.y *= UnitExpression(scale) + def scaled(self, scale): + return replace(self, + width=self.width * UnitExpression(scale), + height=self.height * UnitExpression(scale), + x=self.x * UnitExpression(scale), + y=self.y * UnitExpression(scale)) +@dataclass(frozen=True, slots=True) class Polygon(Primitive): code = 5 - exposure : Expression n_vertices : Expression # center x/y x : UnitExpression y : UnitExpression diameter : UnitExpression - rotation : Expression + rotation : Expression = 0 def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): with self.Calculator(self, variable_binding, unit) as calc: @@ -190,25 +187,26 @@ class Polygon(Primitive): return [ gp.ArcPoly.from_regular_polygon(calc.x, calc.y, calc.diameter/2, calc.n_vertices, rotation, polarity_dark=(bool(calc.exposure) == polarity_dark)) ] - def dilate(self, offset, unit): - self.diameter += UnitExpression(2*offset, unit) + def dilated(self, offset, unit): + return replace(self, diameter=self.diameter + UnitExpression(2*offset, unit)) def scale(self, scale): - self.diameter *= UnitExpression(scale) - self.x *= UnitExpression(scale) - self.y *= UnitExpression(scale) + return replace(self, + diameter=self.diameter * UnitExpression(scale), + x=self.x * UnitExpression(scale), + y=self.y * UnitExpression(scale)) +@dataclass(frozen=True, slots=True) class Thermal(Primitive): code = 7 - exposure : Expression # center x/y x : UnitExpression y : UnitExpression d_outer : UnitExpression d_inner : UnitExpression gap_w : UnitExpression - rotation : Expression + rotation : Expression = 0 def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): with self.Calculator(self, variable_binding, unit) as calc: @@ -231,74 +229,86 @@ class Thermal(Primitive): warnings.warn('Attempted dilation of macro aperture thermal primitive. This is not supported.') def scale(self, scale): - self.d_outer *= UnitExpression(scale) - self.d_inner *= UnitExpression(scale) - self.gap_w *= UnitExpression(scale) - self.x *= UnitExpression(scale) - self.y *= UnitExpression(scale) + return replace(self, + d_outer=self.d_outer * UnitExpression(scale), + d_inner=self.d_inner * UnitExpression(scale), + gap_w=self.gap_w * UnitExpression(scale), + x=self.x * UnitExpression(scale), + y=self.y * UnitExpression(scale)) +@dataclass(frozen=True, slots=True) class Outline(Primitive): code = 4 + length: Expression + coords: tuple + rotation: Expression = 0 - def __init__(self, unit, args): - if len(args) < 10: - raise ValueError(f'Invalid aperture macro outline primitive, not enough parameters ({len(args)}).') - if len(args) > 5004: - raise ValueError(f'Invalid aperture macro outline primitive, too many points ({len(args)//2-2}).') - - self.exposure = expr(args.pop(0)) + def __post_init__(self): + if self.length is None: + object.__setattr__(self, 'length', expr(len(self.coords)//2-1)) + else: + object.__setattr__(self, 'length', expr(self.length)) + object.__setattr__(self, 'rotation', expr(self.rotation)) + object.__setattr__(self, 'exposure', expr(self.exposure)) - # length arg must not contain variables (that would not make sense) - length_arg = (args.pop(0) * ConstantExpression(1)).calculate() + if self.length.calculate() != len(self.coords)//2-1: + raise ValueError('length must exactly equal number of segments, which is the number of points minus one') - if length_arg != len(args)//2-1: - raise ValueError(f'Invalid aperture macro outline primitive, given size {length_arg} does not match length of coordinate list({len(args)//2-1}).') + if self.coords[-2:] != self.coords[:2]: + raise ValueError('Last point must equal first point') - if len(args) % 2 == 1: - self.rotation = expr(args.pop()) - else: - self.rotation = ConstantExpression(0.0) + object.__setattr__(self, 'coords', tuple( + UnitExpression(coord, self.unit) for coord in self.coords)) - if args[0] != args[-2] or args[1] != args[-1]: - raise ValueError(f'Invalid aperture macro outline primitive, polygon is not closed {args[2:4], args[-3:-1]}') + @property + def points(self): + for x, y in zip(self.coords[0::2], self.coords[1::2]): + yield x, y - self.coords = [(UnitExpression(x, unit), UnitExpression(y, unit)) for x, y in zip(args[0::2], args[1::2])] + @classmethod + def from_arglist(kls, arglist): + if len(arglist[3:]) % 2 == 0: + return kls(unit=arglist[0], exposure=arglist[1], length=arglist[2], coords=arglist[3:], rotation=0) + else: + return kls(unit=arglist[0], exposure=arglist[1], length=arglist[2], coords=arglist[3:-1], rotation=arglist[-1]) def __str__(self): return f'' def to_gerber(self, unit=None): - coords = ','.join(coord.to_gerber(unit) for xy in self.coords for coord in xy) + coords = ','.join(coord.to_gerber(unit) for coord in self.coords) return f'{self.code},{self.exposure.to_gerber()},{len(self.coords)-1},{coords},{self.rotation.to_gerber()}' def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): with self.Calculator(self, variable_binding, unit) as calc: rotation += deg_to_rad(calc.rotation) - bound_coords = [ rotate_point(calc(x), calc(y), -rotation, 0, 0) for x, y in self.coords ] + bound_coords = [ rotate_point(calc(x), calc(y), -rotation, 0, 0) for x, y in self.points ] bound_coords = [ (x+offset[0], y+offset[1]) for x, y in bound_coords ] bound_radii = [None] * len(bound_coords) return [gp.ArcPoly(bound_coords, bound_radii, polarity_dark=(bool(calc.exposure) == polarity_dark))] - def dilate(self, offset, unit): + def dilated(self, offset, unit): # we would need a whole polygon offset/clipping library here warnings.warn('Attempted dilation of macro aperture outline primitive. This is not supported.') - def scale(self, scale): - self.coords = [(x*UnitExpression(scale), y*UnitExpression(scale)) for x, y in self.coords] + def scaled(self, scale): + return replace(self, coords=tuple(x*scale for x in self.coords)) +@dataclass(frozen=True, slots=True) class Comment: code = 0 - - def __init__(self, comment): - self.comment = comment + comment: str def to_gerber(self, unit=None): return f'0 {self.comment}' - def scale(self, scale): - pass + def dilated(self, offset, unit): + return self + + def scaled(self, scale): + return self PRIMITIVE_CLASSES = { -- cgit