summaryrefslogtreecommitdiff
path: root/gerbonara/aperture_macros
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-04-29 01:00:45 +0200
committerjaseg <git@jaseg.de>2023-04-29 01:00:45 +0200
commit778e81974580d910eac5e3f977acf79744d3e085 (patch)
tree92d4c5e3ff87aadb972251f16a8763f7b33ddeda /gerbonara/aperture_macros
parent958b47ab471053798ff55194c4aff4cf52f7602a (diff)
downloadgerbonara-778e81974580d910eac5e3f977acf79744d3e085.tar.gz
gerbonara-778e81974580d910eac5e3f977acf79744d3e085.tar.bz2
gerbonara-778e81974580d910eac5e3f977acf79744d3e085.zip
Freeze apertures and aperture macros, make gerbonara faster
Diffstat (limited to 'gerbonara/aperture_macros')
-rw-r--r--gerbonara/aperture_macros/expression.py68
-rw-r--r--gerbonara/aperture_macros/parse.py183
-rw-r--r--gerbonara/aperture_macros/primitive.py194
3 files changed, 225 insertions, 220 deletions
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 <gerbonara@jaseg.de>
+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'<UE {self._expr.to_gerber()} {self.unit}>'
+ return f'<UE {self.expr.to_gerber()} {self.unit}>'
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 <gerbonara@jaseg.de>
+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'<Aperture macro {self.name}, variables {str(self.variables)}, primitives {self.primitives}>'
@@ -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'<Outline {len(self.coords)} points>'
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 = {