Freeze apertures and aperture macros, make gerbonara faster
This commit is contained in:
parent
958b47ab47
commit
778e819745
8 changed files with 382 additions and 414 deletions
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
primitives.append(primitive)
|
||||
|
||||
return macro
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return f'gn_{hash(self)}'
|
||||
|
||||
@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) ])
|
||||
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])])
|
||||
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__':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
unit: LengthUnit
|
||||
exposure : Expression
|
||||
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
# length arg must not contain variables (that would not make sense)
|
||||
length_arg = (args.pop(0) * ConstantExpression(1)).calculate()
|
||||
|
||||
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 len(args) % 2 == 1:
|
||||
self.rotation = expr(args.pop())
|
||||
def __post_init__(self):
|
||||
if self.length is None:
|
||||
object.__setattr__(self, 'length', expr(len(self.coords)//2-1))
|
||||
else:
|
||||
self.rotation = ConstantExpression(0.0)
|
||||
object.__setattr__(self, 'length', expr(self.length))
|
||||
object.__setattr__(self, 'rotation', expr(self.rotation))
|
||||
object.__setattr__(self, 'exposure', expr(self.exposure))
|
||||
|
||||
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]}')
|
||||
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')
|
||||
|
||||
self.coords = [(UnitExpression(x, unit), UnitExpression(y, unit)) for x, y in zip(args[0::2], args[1::2])]
|
||||
if self.coords[-2:] != self.coords[:2]:
|
||||
raise ValueError('Last point must equal first point')
|
||||
|
||||
object.__setattr__(self, 'coords', tuple(
|
||||
UnitExpression(coord, self.unit) for coord in self.coords))
|
||||
|
||||
@property
|
||||
def points(self):
|
||||
for x, y in zip(self.coords[0::2], self.coords[1::2]):
|
||||
yield x, y
|
||||
|
||||
@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 = {
|
||||
|
|
|
|||
|
|
@ -17,20 +17,17 @@
|
|||
#
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, replace, field, fields, InitVar
|
||||
from dataclasses import dataclass, replace, field, fields, InitVar, KW_ONLY
|
||||
from functools import lru_cache
|
||||
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
from .utils import MM, Inch, sum_bounds
|
||||
from .utils import LengthUnit, MM, Inch, sum_bounds
|
||||
|
||||
from . import graphic_primitives as gp
|
||||
|
||||
|
||||
def _flash_hole(self, x, y, unit=None, polarity_dark=True):
|
||||
if getattr(self, 'hole_rect_h', None) is not None:
|
||||
w, h = self.unit.convert_to(unit, self.hole_dia), self.unit.convert_to(unit, self.hole_rect_h)
|
||||
return [*self._primitives(x, y, unit, polarity_dark),
|
||||
gp.Rectangle(x, y, w, h, rotation=self.rotation, polarity_dark=(not polarity_dark))]
|
||||
elif self.hole_dia is not None:
|
||||
if self.hole_dia is not None:
|
||||
return [*self._primitives(x, y, unit, polarity_dark),
|
||||
gp.Circle(x, y, self.unit.convert_to(unit, self.hole_dia/2), polarity_dark=(not polarity_dark))]
|
||||
else:
|
||||
|
|
@ -40,7 +37,7 @@ def _strip_right(*args):
|
|||
args = list(args)
|
||||
while args and args[-1] is None:
|
||||
args.pop()
|
||||
return args
|
||||
return tuple(args)
|
||||
|
||||
def _none_close(a, b):
|
||||
if a is None and b is None:
|
||||
|
|
@ -57,39 +54,14 @@ class Length:
|
|||
def __init__(self, obj_type):
|
||||
self.type = obj_type
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Aperture:
|
||||
""" Base class for all apertures. """
|
||||
|
||||
# hackety hack: Work around python < 3.10 not having dataclasses.KW_ONLY.
|
||||
#
|
||||
# For details, refer to graphic_objects.py
|
||||
def __init_subclass__(cls):
|
||||
#: :py:class:`gerbonara.utils.LengthUnit` used for all length fields of this aperture.
|
||||
cls.unit = None
|
||||
#: GerberX2 attributes of this aperture. Note that this will only contain aperture attributes, not file attributes.
|
||||
#: File attributes are stored in the :py:attr:`~.GerberFile.attrs` of the :py:class:`.GerberFile`.
|
||||
cls.attrs = field(default_factory=dict)
|
||||
#: Aperture index this aperture had when it was read from the Gerber file. This field is purely informational since
|
||||
#: apertures are de-duplicated and re-numbered when writing a Gerber file. For `D10`, this field would be `10`. When
|
||||
#: you programmatically create a new aperture, you do not have to set this.
|
||||
cls.original_number = None
|
||||
|
||||
d = {'unit': str, 'attrs': dict, 'original_number': int}
|
||||
if hasattr(cls, '__annotations__'):
|
||||
cls.__annotations__.update(d)
|
||||
else:
|
||||
cls.__annotations__ = d
|
||||
|
||||
@property
|
||||
def hole_shape(self):
|
||||
""" Get shape of hole based on :py:attr:`hole_dia` and :py:attr:`hole_rect_h`: "rect" or "circle" or None. """
|
||||
if getattr(self, 'hole_rect_h') is not None:
|
||||
return 'rect'
|
||||
elif getattr(self, 'hole_dia') is not None:
|
||||
return 'circle'
|
||||
else:
|
||||
return None
|
||||
_ : KW_ONLY
|
||||
unit: LengthUnit = None
|
||||
attrs: tuple = None
|
||||
original_number: int = None
|
||||
_bounding_box: tuple = None
|
||||
|
||||
def _params(self, unit=None):
|
||||
out = []
|
||||
|
|
@ -119,7 +91,10 @@ class Aperture:
|
|||
return self._primitives(x, y, unit, polarity_dark)
|
||||
|
||||
def bounding_box(self, unit=None):
|
||||
return sum_bounds((prim.bounding_box() for prim in self.flash(0, 0, unit, True)))
|
||||
if self._bounding_box is None:
|
||||
object.__setattr__(self, '_bounding_box',
|
||||
sum_bounds((prim.bounding_box() for prim in self.flash(0, 0, MM, True))))
|
||||
return MM.convert_bounds_to(unit, self._bounding_box)
|
||||
|
||||
def equivalent_width(self, unit=None):
|
||||
""" Get the width of a line interpolated using this aperture in the given :py:class:`~.LengthUnit`.
|
||||
|
|
@ -133,16 +108,12 @@ class Aperture:
|
|||
|
||||
:rtype: str
|
||||
"""
|
||||
# Hack: The standard aperture shapes C, R, O do not have a rotation parameter. To make this API easier to use,
|
||||
# we emulate this parameter. Our circle, rectangle and oblong classes below have a rotation parameter. Only at
|
||||
# export time during to_gerber, this parameter is evaluated.
|
||||
unit = settings.unit if settings else None
|
||||
actual_inst = self.rotated()
|
||||
params = 'X'.join(f'{float(par):.4}' for par in actual_inst._params(unit) if par is not None)
|
||||
params = 'X'.join(f'{float(par):.4}' for par in self._params(unit) if par is not None)
|
||||
if params:
|
||||
return f'{actual_inst._gerber_shape_code},{params}'
|
||||
return f'{self._gerber_shape_code},{params}'
|
||||
else:
|
||||
return actual_inst._gerber_shape_code
|
||||
return self._gerber_shape_code
|
||||
|
||||
def to_macro(self):
|
||||
""" Convert this :py:class:`.Aperture` into an :py:class:`.ApertureMacro` inside an
|
||||
|
|
@ -150,24 +121,10 @@ class Aperture:
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __eq__(self, other):
|
||||
""" Compare two apertures. Apertures are compared based on their Gerber representation. Two apertures are
|
||||
considered equal if their Gerber aperture definitions are identical.
|
||||
"""
|
||||
# We need to choose some unit here.
|
||||
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:
|
||||
return {'hole_dia': self.hole_dia, 'hole_rect_h': None}
|
||||
else:
|
||||
return {'hole_dia': self.hole_rect_h, 'hole_rect_h': self.hole_dia}
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ExcellonTool(Aperture):
|
||||
""" Special Aperture_ subclass for use in :py:class:`.ExcellonFile`. Similar to :py:class:`.CircleAperture`, but
|
||||
does not have :py:attr:`.CircleAperture.hole_dia` or :py:attr:`.CircleAperture.hole_rect_h`, and has the additional
|
||||
:py:attr:`plated` attribute.
|
||||
does not have :py:attr:`.CircleAperture.hole_dia`, and has the additional :py:attr:`plated` attribute.
|
||||
"""
|
||||
_gerber_shape_code = 'C'
|
||||
_human_readable_shape = 'drill'
|
||||
|
|
@ -183,18 +140,6 @@ class ExcellonTool(Aperture):
|
|||
def to_xnc(self, settings):
|
||||
return 'C' + settings.write_excellon_value(self.diameter, self.unit)
|
||||
|
||||
def __eq__(self, other):
|
||||
""" Compare two :py:class:`.ExcellonTool` instances. They are considered equal if their diameter and plating
|
||||
match.
|
||||
"""
|
||||
if not isinstance(other, ExcellonTool):
|
||||
return False
|
||||
|
||||
if not self.plated == other.plated:
|
||||
return False
|
||||
|
||||
return _none_close(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')
|
||||
return f'<Excellon Tool d={self.diameter:.3f}{plated} [{self.unit}]>'
|
||||
|
|
@ -207,17 +152,18 @@ class ExcellonTool(Aperture):
|
|||
offset = unit(offset, self.unit)
|
||||
return replace(self, diameter=self.diameter+2*offset)
|
||||
|
||||
@lru_cache()
|
||||
def rotated(self, angle=0):
|
||||
return self
|
||||
|
||||
def to_macro(self):
|
||||
def to_macro(self, rotation=0):
|
||||
return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM))
|
||||
|
||||
def _params(self, unit=None):
|
||||
return [self.unit.convert_to(unit, self.diameter)]
|
||||
return (self.unit.convert_to(unit, self.diameter),)
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CircleAperture(Aperture):
|
||||
""" Besides flashing circles or rings, CircleApertures are used to set the width of a
|
||||
:py:class:`~.graphic_objects.Line` or :py:class:`~.graphic_objects.Arc`.
|
||||
|
|
@ -228,10 +174,6 @@ class CircleAperture(Aperture):
|
|||
diameter : Length(float)
|
||||
#: float with the hole diameter of this aperture in :py:attr:`unit` units. ``0`` for no hole.
|
||||
hole_dia : Length(float) = None
|
||||
#: float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
|
||||
hole_rect_h : Length(float) = None
|
||||
# float with radians. This is only used for rectangular holes (as circles are rotationally symmetric).
|
||||
rotation : float = 0
|
||||
|
||||
def _primitives(self, x, y, unit=None, polarity_dark=True):
|
||||
return [ gp.Circle(x, y, self.unit.convert_to(unit, self.diameter/2), polarity_dark=polarity_dark) ]
|
||||
|
|
@ -246,31 +188,27 @@ class CircleAperture(Aperture):
|
|||
|
||||
def dilated(self, offset, unit=MM):
|
||||
offset = self.unit(offset, unit)
|
||||
return replace(self, diameter=self.diameter+2*offset, hole_dia=None, hole_rect_h=None)
|
||||
return replace(self, diameter=self.diameter+2*offset, hole_dia=None)
|
||||
|
||||
@lru_cache()
|
||||
def rotated(self, angle=0):
|
||||
if math.isclose((self.rotation+angle) % (2*math.pi), 0, abs_tol=1e-6) or self.hole_rect_h is None:
|
||||
return self
|
||||
else:
|
||||
return self.to_macro(self.rotation+angle)
|
||||
return self
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self,
|
||||
diameter=self.diameter*scale,
|
||||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale,
|
||||
hole_rect_h=None if self.hole_rect_h is None else self.hole_rect_h*scale)
|
||||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale)
|
||||
|
||||
def to_macro(self):
|
||||
def to_macro(self, rotation=0):
|
||||
return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM))
|
||||
|
||||
def _params(self, unit=None):
|
||||
return _strip_right(
|
||||
self.unit.convert_to(unit, self.diameter),
|
||||
self.unit.convert_to(unit, self.hole_dia),
|
||||
self.unit.convert_to(unit, self.hole_rect_h))
|
||||
self.unit.convert_to(unit, self.hole_dia))
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RectangleAperture(Aperture):
|
||||
""" Gerber rectangle aperture. Can only be used for flashes, since the line width of an interpolation of a rectangle
|
||||
aperture is not well-defined and there is no tool that implements it in a geometrically correct way. """
|
||||
|
|
@ -282,14 +220,10 @@ class RectangleAperture(Aperture):
|
|||
h : Length(float)
|
||||
#: float with the hole diameter of this aperture in :py:attr:`unit` units. ``0`` for no hole.
|
||||
hole_dia : Length(float) = None
|
||||
#: float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
|
||||
hole_rect_h : Length(float) = None
|
||||
# Rotation in radians. This rotates both the aperture and the rectangular hole if it has one.
|
||||
rotation : float = 0 # radians
|
||||
|
||||
def _primitives(self, x, y, unit=None, polarity_dark=True):
|
||||
return [ gp.Rectangle(x, y, self.unit.convert_to(unit, self.w), self.unit.convert_to(unit, self.h),
|
||||
rotation=self.rotation, polarity_dark=polarity_dark) ]
|
||||
rotation=0, polarity_dark=polarity_dark) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<rect aperture {self.w:.3}x{self.h:.3} [{self.unit}]>'
|
||||
|
|
@ -301,42 +235,39 @@ class RectangleAperture(Aperture):
|
|||
|
||||
def dilated(self, offset, unit=MM):
|
||||
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)
|
||||
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None)
|
||||
|
||||
@lru_cache()
|
||||
def rotated(self, angle=0):
|
||||
self.rotation += angle
|
||||
if math.isclose(self.rotation % math.pi, 0):
|
||||
self.rotation = 0
|
||||
if math.isclose(angle % math.pi, 0):
|
||||
return self
|
||||
elif math.isclose(self.rotation % math.pi, math.pi/2):
|
||||
return replace(self, w=self.h, h=self.w, **self._rotate_hole_90(), rotation=0)
|
||||
elif math.isclose(angle % math.pi, math.pi/2):
|
||||
return replace(self, w=self.h, h=self.w, hole_dia=self.hole_dia)
|
||||
else: # odd angle
|
||||
return self.to_macro()
|
||||
return self.to_macro(angle)
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self,
|
||||
w=self.w*scale,
|
||||
h=self.h*scale,
|
||||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale,
|
||||
hole_rect_h=None if self.hole_rect_h is None else self.hole_rect_h*scale)
|
||||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale)
|
||||
|
||||
def to_macro(self, rotation=0):
|
||||
return ApertureMacroInstance(GenericMacros.rect,
|
||||
[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 + rotation])
|
||||
0,
|
||||
rotation])
|
||||
|
||||
def _params(self, unit=None):
|
||||
return _strip_right(
|
||||
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))
|
||||
self.unit.convert_to(unit, self.hole_dia))
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ObroundAperture(Aperture):
|
||||
""" Aperture whose shape is the convex hull of two circles of equal radii.
|
||||
|
||||
|
|
@ -352,14 +283,10 @@ class ObroundAperture(Aperture):
|
|||
h : Length(float)
|
||||
#: float with the hole diameter of this aperture in :py:attr:`unit` units. ``0`` for no hole.
|
||||
hole_dia : Length(float) = None
|
||||
#: float or None. If not None, specifies a rectangular hole of size `hole_dia * hole_rect_h` instead of a round hole.
|
||||
hole_rect_h : Length(float) = None
|
||||
#: Rotation in radians. This rotates both the aperture and the rectangular hole if it has one.
|
||||
rotation : float = 0
|
||||
|
||||
def _primitives(self, x, y, unit=None, polarity_dark=True):
|
||||
return [ gp.Line.from_obround(x, y, self.unit.convert_to(unit, self.w), self.unit.convert_to(unit, self.h),
|
||||
rotation=self.rotation, polarity_dark=polarity_dark) ]
|
||||
polarity_dark=polarity_dark) ]
|
||||
|
||||
def __str__(self):
|
||||
return f'<obround aperture {self.w:.3}x{self.h:.3} [{self.unit}]>'
|
||||
|
|
@ -368,13 +295,14 @@ class ObroundAperture(Aperture):
|
|||
|
||||
def dilated(self, offset, unit=MM):
|
||||
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)
|
||||
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None)
|
||||
|
||||
@lru_cache()
|
||||
def rotated(self, angle=0):
|
||||
if math.isclose((angle + self.rotation) % math.pi, 0, abs_tol=1e-6):
|
||||
if math.isclose(angle % math.pi, 0, abs_tol=1e-6):
|
||||
return self
|
||||
elif math.isclose((angle + self.rotation) % math.pi, math.pi/2, abs_tol=1e-6):
|
||||
return replace(self, w=self.h, h=self.w, **self._rotate_hole_90(), rotation=0)
|
||||
elif math.isclose(angle % math.pi, math.pi/2, abs_tol=1e-6):
|
||||
return replace(self, w=self.h, h=self.w, hole_dia=self.hole_dia)
|
||||
else:
|
||||
return self.to_macro(angle)
|
||||
|
||||
|
|
@ -382,32 +310,31 @@ class ObroundAperture(Aperture):
|
|||
return replace(self,
|
||||
w=self.w*scale,
|
||||
h=self.h*scale,
|
||||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale,
|
||||
hole_rect_h=None if self.hole_rect_h is None else self.hole_rect_h*scale)
|
||||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale)
|
||||
|
||||
def to_macro(self, rotation=0):
|
||||
# generic macro only supports w > h so flip x/y if h > w
|
||||
if self.w > self.h:
|
||||
inst = self
|
||||
else:
|
||||
inst = replace(self, w=self.h, h=self.w, **self._rotate_hole_90(), rotation=self.rotation-math.pi/2)
|
||||
rotation -= -math.pi/2
|
||||
inst = replace(self, w=self.h, h=self.w, hole_dia=self.hole_dia)
|
||||
|
||||
return ApertureMacroInstance(GenericMacros.obround,
|
||||
[MM(inst.w, self.unit),
|
||||
MM(inst.h, self.unit),
|
||||
MM(inst.hole_dia, self.unit) or 0,
|
||||
MM(inst.hole_rect_h, self.unit) or 0,
|
||||
0,
|
||||
inst.rotation + rotation])
|
||||
|
||||
def _params(self, unit=None):
|
||||
return _strip_right(
|
||||
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))
|
||||
self.unit.convert_to(unit, self.hole_dia))
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PolygonAperture(Aperture):
|
||||
""" Aperture whose shape is a regular n-sided polygon (e.g. pentagon, hexagon etc.). Note that this only supports
|
||||
round holes.
|
||||
|
|
@ -439,6 +366,7 @@ class PolygonAperture(Aperture):
|
|||
|
||||
flash = _flash_hole
|
||||
|
||||
@lru_cache()
|
||||
def rotated(self, angle=0):
|
||||
if angle != 0:
|
||||
return replace(self, rotatio=self.rotation + angle)
|
||||
|
|
@ -465,7 +393,7 @@ class PolygonAperture(Aperture):
|
|||
else:
|
||||
return self.unit.convert_to(unit, self.diameter), self.n_vertices
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApertureMacroInstance(Aperture):
|
||||
""" One instance of an aperture macro. An aperture macro defined with an ``AM`` statement can be instantiated by
|
||||
multiple ``AD`` aperture definition statements using different parameters. An :py:class:`.ApertureMacroInstance` is
|
||||
|
|
@ -477,10 +405,7 @@ class ApertureMacroInstance(Aperture):
|
|||
macro : object
|
||||
#: The parameters to the :py:class:`.ApertureMacro`. All elements should be floats or ints. The first item in the
|
||||
#: list is parameter ``$1``, the second is ``$2`` etc.
|
||||
parameters : list = field(default_factory=list)
|
||||
#: Aperture rotation in radians. When saving, a copy of the :py:class:`.ApertureMacro` is re-written with this
|
||||
#: rotation.
|
||||
rotation : float = 0
|
||||
parameters : tuple = ()
|
||||
|
||||
@property
|
||||
def _gerber_shape_code(self):
|
||||
|
|
@ -488,30 +413,26 @@ class ApertureMacroInstance(Aperture):
|
|||
|
||||
def _primitives(self, x, y, unit=None, polarity_dark=True):
|
||||
out = list(self.macro.to_graphic_primitives(
|
||||
offset=(x, y), rotation=self.rotation,
|
||||
offset=(x, y), rotation=0,
|
||||
parameters=self.parameters, unit=unit, polarity_dark=polarity_dark))
|
||||
return out
|
||||
|
||||
def dilated(self, offset, unit=MM):
|
||||
return replace(self, macro=self.macro.dilated(offset, unit))
|
||||
|
||||
@lru_cache()
|
||||
def rotated(self, angle=0):
|
||||
if math.isclose((self.rotation+angle) % (2*math.pi), 0):
|
||||
if math.isclose(angle % (2*math.pi), 0):
|
||||
return self
|
||||
else:
|
||||
return self.to_macro(angle)
|
||||
|
||||
def to_macro(self, rotation=0):
|
||||
return replace(self, macro=self.macro.rotated(self.rotation+rotation), rotation=0)
|
||||
return replace(self, macro=self.macro.rotated(rotation))
|
||||
|
||||
def scaled(self, scale):
|
||||
return replace(self, macro=self.macro.scaled(scale))
|
||||
|
||||
def __eq__(self, other):
|
||||
return hasattr(other, 'macro') and self.macro == other.macro and \
|
||||
hasattr(other, 'parameters') and self.parameters == other.parameters and \
|
||||
hasattr(other, 'rotation') and self.rotation == other.rotation
|
||||
|
||||
def _params(self, unit=None):
|
||||
# We ignore "unit" here as we convert the actual macro, not this instantiation.
|
||||
# We do this because here we do not have information about which parameter has which physical units.
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class Text:
|
|||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
tstamp: Timestamp = None
|
||||
|
||||
def render(self, variables={}):
|
||||
def render(self, variables={}, cache=None):
|
||||
if self.hide: # why
|
||||
return
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ class TextBox:
|
|||
stroke: Stroke = field(default_factory=Stroke)
|
||||
render_cache: RenderCache = None
|
||||
|
||||
def render(self, variables={}):
|
||||
def render(self, variables={}, cache=None):
|
||||
yield from gr.TextBox.render(self, variables=variables)
|
||||
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ class Line:
|
|||
locked: Flag() = False
|
||||
tstamp: Timestamp = None
|
||||
|
||||
def render(self, variables=None):
|
||||
def render(self, variables=None, cache=None):
|
||||
dasher = Dasher(self)
|
||||
dasher.move(self.start.x, self.start.y)
|
||||
dasher.line(self.end.x, self.end.y)
|
||||
|
|
@ -110,7 +110,7 @@ class Rectangle:
|
|||
locked: Flag() = False
|
||||
tstamp: Timestamp = None
|
||||
|
||||
def render(self, variables=None):
|
||||
def render(self, variables=None, cache=None):
|
||||
x1, y1 = self.start.x, self.start.y
|
||||
x2, y2 = self.end.x, self.end.y
|
||||
x1, x2 = min(x1, x2), max(x1, x2)
|
||||
|
|
@ -143,7 +143,7 @@ class Circle:
|
|||
locked: Flag() = False
|
||||
tstamp: Timestamp = None
|
||||
|
||||
def render(self, variables=None):
|
||||
def render(self, variables=None, cache=None):
|
||||
x, y = self.center.x, self.center.y
|
||||
r = math.dist((x, y), (self.end.x, self.end.y)) # insane
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ class Arc:
|
|||
tstamp: Timestamp = None
|
||||
|
||||
|
||||
def render(self, variables=None):
|
||||
def render(self, variables=None, cache=None):
|
||||
mx, my = self.mid.x, self.mid.y
|
||||
x1, y1 = self.start.x, self.start.y
|
||||
x2, y2 = self.end.x, self.end.y
|
||||
|
|
@ -230,7 +230,7 @@ class Polygon:
|
|||
locked: Flag() = False
|
||||
tstamp: Timestamp = None
|
||||
|
||||
def render(self, variables=None):
|
||||
def render(self, variables=None, cache=None):
|
||||
if len(self.pts.xy) < 2:
|
||||
return
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ class Curve:
|
|||
locked: Flag() = False
|
||||
tstamp: Timestamp = None
|
||||
|
||||
def render(self, variables=None):
|
||||
def render(self, variables=None, cache=None):
|
||||
raise NotImplementedError('Bezier rendering is not yet supported. Please raise an issue and provide an example file.')
|
||||
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ class Dimension:
|
|||
format: DimensionFormat = field(default_factory=DimensionFormat)
|
||||
style: DimensionStyle = field(default_factory=DimensionStyle)
|
||||
|
||||
def render(self, variables=None):
|
||||
def render(self, variables=None, cache=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
|
|
@ -383,7 +383,7 @@ class Pad:
|
|||
options: OmitDefault(CustomPadOptions) = None
|
||||
primitives: OmitDefault(CustomPadPrimitives) = None
|
||||
|
||||
def render(self, variables=None, margin=None):
|
||||
def render(self, variables=None, margin=None, cache=None):
|
||||
#if self.type in (Atom.connect, Atom.np_thru_hole):
|
||||
# return
|
||||
if self.drill and self.drill.offset:
|
||||
|
|
@ -391,7 +391,17 @@ class Pad:
|
|||
else:
|
||||
ox, oy = 0, 0
|
||||
|
||||
yield go.Flash(self.at.x+ox, self.at.y+oy, self.aperture(margin), unit=MM)
|
||||
cache_key = id(self), margin
|
||||
if cache and cache_key in cache:
|
||||
aperture = cache[cache_key]
|
||||
|
||||
elif cache is not None:
|
||||
aperture = cache[cache_key] = self.aperture(margin)
|
||||
|
||||
else:
|
||||
aperture = self.aperture(margin)
|
||||
|
||||
yield go.Flash(self.at.x+ox, self.at.y+oy, aperture, unit=MM)
|
||||
|
||||
def aperture(self, margin=None):
|
||||
rotation = -math.radians(self.at.rotation)
|
||||
|
|
@ -403,10 +413,10 @@ class Pad:
|
|||
elif self.shape == Atom.rect:
|
||||
if margin > 0:
|
||||
return ap.ApertureMacroInstance(GenericMacros.rounded_rect,
|
||||
[self.size.x+2*margin, self.size.y+2*margin,
|
||||
(self.size.x+2*margin, self.size.y+2*margin,
|
||||
margin,
|
||||
0, 0, # no hole
|
||||
rotation], unit=MM)
|
||||
rotation), unit=MM)
|
||||
else:
|
||||
return ap.RectangleAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(rotation)
|
||||
|
||||
|
|
@ -434,27 +444,27 @@ class Pad:
|
|||
|
||||
alpha = math.atan(y / dy) if dy > 0 else 0
|
||||
return ap.ApertureMacroInstance(GenericMacros.isosceles_trapezoid,
|
||||
[x+dy+2*margin*math.cos(alpha), y+2*margin,
|
||||
(x+dy+2*margin*math.cos(alpha), y+2*margin,
|
||||
2*dy,
|
||||
0, 0, # no hole
|
||||
rotation], unit=MM)
|
||||
rotation), unit=MM)
|
||||
|
||||
else:
|
||||
return ap.ApertureMacroInstance(GenericMacros.rounded_isosceles_trapezoid,
|
||||
[x+dy, y,
|
||||
(x+dy, y,
|
||||
2*dy, margin,
|
||||
0, 0, # no hole
|
||||
rotation], unit=MM)
|
||||
rotation), unit=MM)
|
||||
|
||||
elif self.shape == Atom.roundrect:
|
||||
x, y = self.size.x, self.size.y
|
||||
r = min(x, y) * self.roundrect_rratio
|
||||
if margin > -r:
|
||||
return ap.ApertureMacroInstance(GenericMacros.rounded_rect,
|
||||
[x+2*margin, y+2*margin,
|
||||
(x+2*margin, y+2*margin,
|
||||
r+margin,
|
||||
0, 0, # no hole
|
||||
rotation], unit=MM)
|
||||
rotation), unit=MM)
|
||||
else:
|
||||
return ap.RectangleAperture(x+margin, y+margin, unit=MM).rotated(rotation)
|
||||
|
||||
|
|
@ -485,20 +495,20 @@ class Pad:
|
|||
if self.options:
|
||||
if self.options.anchor == Atom.rect and self.size.x > 0 and self.size.y > 0:
|
||||
if margin <= 0:
|
||||
primitives.append(amp.CenterLine(MM, [1, self.size.x+2*margin, self.size.y+2*margin, 0, 0, 0]))
|
||||
primitives.append(amp.CenterLine(MM, 1, self.size.x+2*margin, self.size.y+2*margin, 0, 0, 0))
|
||||
|
||||
else: # margin > 0
|
||||
primitives.append(amp.CenterLine(MM, [1, self.size.x+2*margin, self.size.y, 0, 0, 0]))
|
||||
primitives.append(amp.CenterLine(MM, [1, self.size.x, self.size.y+2*margin, 0, 0, 0]))
|
||||
primitives.append(amp.Circle(MM, [1, 2*margin, -self.size.x/2, -self.size.y/2]))
|
||||
primitives.append(amp.Circle(MM, [1, 2*margin, -self.size.x/2, +self.size.y/2]))
|
||||
primitives.append(amp.Circle(MM, [1, 2*margin, +self.size.x/2, -self.size.y/2]))
|
||||
primitives.append(amp.Circle(MM, [1, 2*margin, +self.size.x/2, +self.size.y/2]))
|
||||
primitives.append(amp.CenterLine(MM, 1, self.size.x+2*margin, self.size.y, 0, 0, 0))
|
||||
primitives.append(amp.CenterLine(MM, 1, self.size.x, self.size.y+2*margin, 0, 0, 0))
|
||||
primitives.append(amp.Circle(MM, 1, 2*margin, -self.size.x/2, -self.size.y/2))
|
||||
primitives.append(amp.Circle(MM, 1, 2*margin, -self.size.x/2, +self.size.y/2))
|
||||
primitives.append(amp.Circle(MM, 1, 2*margin, +self.size.x/2, -self.size.y/2))
|
||||
primitives.append(amp.Circle(MM, 1, 2*margin, +self.size.x/2, +self.size.y/2))
|
||||
|
||||
elif self.options.anchor == Atom.circle and self.size.x > 0:
|
||||
primitives.append(amp.Circle(MM, [1, self.size.x+2*margin, 0, 0, 0]))
|
||||
primitives.append(amp.Circle(MM, 1, self.size.x+2*margin, 0, 0, 0))
|
||||
|
||||
macro = ApertureMacro(primitives=primitives).rotated(rotation)
|
||||
macro = ApertureMacro(primitives=tuple(primitives)).rotated(rotation)
|
||||
return ap.ApertureMacroInstance(macro, unit=MM)
|
||||
|
||||
def render_drill(self):
|
||||
|
|
@ -645,7 +655,7 @@ class Footprint:
|
|||
(self.dimensions if text else []),
|
||||
(self.pads if pads else []))
|
||||
|
||||
def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, side=None, variables={}):
|
||||
def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, side=None, variables={}, cache=None):
|
||||
x += self.at.x
|
||||
y += self.at.y
|
||||
rotation += math.radians(self.at.rotation)
|
||||
|
|
@ -687,7 +697,7 @@ class Footprint:
|
|||
else:
|
||||
margin = None
|
||||
|
||||
for fe in obj.render(margin=margin):
|
||||
for fe in obj.render(margin=margin, cache=cache):
|
||||
fe.rotate(rotation)
|
||||
fe.offset(x, y, MM)
|
||||
if isinstance(fe, go.Flash) and fe.aperture:
|
||||
|
|
@ -745,7 +755,7 @@ class FootprintInstance(Positioned):
|
|||
value: str = None
|
||||
variables: dict = field(default_factory=lambda: {})
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
x, y, rotation = self.abs_pos
|
||||
x, y = MM(x, self.unit), MM(y, self.unit)
|
||||
|
||||
|
|
@ -763,7 +773,7 @@ class FootprintInstance(Positioned):
|
|||
x=x, y=y, rotation=rotation,
|
||||
side=self.side,
|
||||
text=(not self.hide_text),
|
||||
variables=variables)
|
||||
variables=variables, cache=cache)
|
||||
|
||||
def bounding_box(self, unit=MM):
|
||||
return offset_bounds(self.sexp.bounding_box(unit), unit(self.x, self.unit), unit(self.y, self.unit))
|
||||
|
|
|
|||
|
|
@ -117,8 +117,9 @@ class Board:
|
|||
if layer_stack is None:
|
||||
layer_stack = LayerStack()
|
||||
|
||||
cache = {}
|
||||
for obj in chain(self.objects):
|
||||
obj.render(layer_stack)
|
||||
obj.render(layer_stack, cache)
|
||||
|
||||
layer_stack['mechanical', 'outline'].objects.extend(self.outline)
|
||||
layer_stack['top', 'silk'].objects.extend(self.extra_silk_top)
|
||||
|
|
@ -189,13 +190,13 @@ class ObjectGroup(Positioned):
|
|||
drill_pth: list = field(default_factory=list)
|
||||
objects: list = field(default_factory=list)
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
x, y, rotation = self.abs_pos
|
||||
top, bottom = ('bottom', 'top') if self.side == 'bottom' else ('top', 'bottom')
|
||||
|
||||
for obj in self.objects:
|
||||
obj.parent = self
|
||||
obj.render(layer_stack)
|
||||
obj.render(layer_stack, cache=cache)
|
||||
|
||||
for target, source in [
|
||||
(layer_stack[top, 'copper'], self.top_copper),
|
||||
|
|
@ -251,7 +252,7 @@ class Text(Positioned):
|
|||
layer: str = 'silk'
|
||||
polarity_dark: bool = True
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
obj_x, obj_y, rotation = self.abs_pos
|
||||
global newstroke_font
|
||||
|
||||
|
|
@ -299,6 +300,26 @@ class Text(Positioned):
|
|||
obj.offset(obj_x, obj_y)
|
||||
layer_stack[self.side, self.layer].objects.append(obj)
|
||||
|
||||
def bounding_box(self, unit=MM):
|
||||
approx_w = len(self.text)*self.font_size*0.75 + self.stroke_width
|
||||
approx_h = self.font_size + self.stroke_width
|
||||
|
||||
if self.h_align == 'left':
|
||||
x0 = 0
|
||||
elif self.h_align == 'center':
|
||||
x0 = -approx_w/2
|
||||
elif self.h_align == 'right':
|
||||
x0 = -approx_w
|
||||
|
||||
if self.v_align == 'top':
|
||||
y0 = -approx_h
|
||||
elif self.v_align == 'middle':
|
||||
y0 = -approx_h/2
|
||||
elif self.v_align == 'bottom':
|
||||
y0 = 0
|
||||
|
||||
return (self.x+x0, self.y+y0), (self.x+x0+approx_w, self.y+y0+approx_h)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pad(Positioned):
|
||||
|
|
@ -312,7 +333,7 @@ class SMDPad(Pad):
|
|||
paste_aperture: Aperture
|
||||
silk_features: list = field(default_factory=list)
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
x, y, rotation = self.abs_pos
|
||||
layer_stack[self.side, 'copper'].objects.append(Flash(x, y, self.copper_aperture.rotated(rotation), unit=self.unit))
|
||||
layer_stack[self.side, 'mask' ].objects.append(Flash(x, y, self.mask_aperture.rotated(rotation), unit=self.unit))
|
||||
|
|
@ -356,7 +377,7 @@ class THTPad(Pad):
|
|||
if (self.pad_top.side, self.pad_bottom.side) != ('top', 'bottom'):
|
||||
raise ValueError(f'The top and bottom pads must have side set to top and bottom, respectively. Currently, the top pad side is set to "{self.pad_top.side}" and the bottom pad side to "{self.pad_bottom.side}".')
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
x, y, rotation = self.abs_pos
|
||||
self.pad_top.parent = self
|
||||
self.pad_top.render(layer_stack)
|
||||
|
|
@ -415,7 +436,7 @@ class Hole(Positioned):
|
|||
diameter: float
|
||||
mask_copper_margin: float = 0.2
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
x, y, rotation = self.abs_pos
|
||||
|
||||
hole = Flash(x, y, ExcellonTool(self.diameter, plated=False, unit=self.unit), unit=self.unit)
|
||||
|
|
@ -436,7 +457,7 @@ class Via(Positioned):
|
|||
diameter: float
|
||||
hole: float
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
x, y, rotation = self.abs_pos
|
||||
|
||||
aperture = CircleAperture(diameter=self.diameter, unit=self.unit)
|
||||
|
|
@ -627,7 +648,7 @@ class Trace:
|
|||
|
||||
return self._round_over(points, aperture)
|
||||
|
||||
def render(self, layer_stack):
|
||||
def render(self, layer_stack, cache=None):
|
||||
layer_stack[self.side, 'copper'].objects.extend(self._to_graphic_objects())
|
||||
|
||||
def _route_demo():
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ class Region(GraphicObject):
|
|||
if points[-1] != points[0]:
|
||||
points.append(points[0])
|
||||
|
||||
yield amp.Outline(self.unit, [int(self.polarity_dark), len(points)-1, *(coord for p in points for coord in p)])
|
||||
yield amp.Outline(self.unit, int(self.polarity_dark), len(points)-1, tuple(coord for p in points for coord in p))
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
if unit == self.unit:
|
||||
|
|
@ -503,9 +503,9 @@ class Line(GraphicObject):
|
|||
def _aperture_macro_primitives(self):
|
||||
obj = self.converted(MM) # Gerbonara aperture macros use MM units.
|
||||
width = obj.aperture.equivalent_width(MM)
|
||||
yield amp.VectorLine(MM, [int(self.polarity_dark), width, obj.x1, obj.y1, obj.x2, obj.y2, 0])
|
||||
yield amp.Circle(MM, [int(self.polarity_dark), width, obj.x1, obj.y1])
|
||||
yield amp.Circle(MM, [int(self.polarity_dark), width, obj.x2, obj.y2])
|
||||
yield amp.VectorLine(MM, int(self.polarity_dark), width, obj.x1, obj.y1, obj.x2, obj.y2, 0)
|
||||
yield amp.Circle(MM, int(self.polarity_dark), width, obj.x1, obj.y1)
|
||||
yield amp.Circle(MM, int(self.polarity_dark), width, obj.x2, obj.y2)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ gerber.utils
|
|||
This module provides utility functions for working with Gerber and Excellon files.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import re
|
||||
import textwrap
|
||||
|
|
@ -57,6 +58,7 @@ class RegexMatcher:
|
|||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LengthUnit:
|
||||
""" Convenience length unit class. Used in :py:class:`.GraphicObject` and :py:class:`.Aperture` to store lenght
|
||||
information. Provides a number of useful unit conversion functions.
|
||||
|
|
@ -64,13 +66,9 @@ class LengthUnit:
|
|||
Singleton, use only global instances ``utils.MM`` and ``utils.Inch``.
|
||||
"""
|
||||
|
||||
def __init__(self, name, shorthand, this_in_mm):
|
||||
self.name = name
|
||||
self.shorthand = shorthand
|
||||
self.factor = this_in_mm
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.shorthand, self.factor))
|
||||
name: str
|
||||
shorthand: str
|
||||
this_in_mm: float
|
||||
|
||||
def convert_from(self, unit, value):
|
||||
""" Convert ``value`` from ``unit`` into this unit.
|
||||
|
|
@ -112,6 +110,19 @@ class LengthUnit:
|
|||
max_y = self.convert_from(unit, max_y)
|
||||
return (min_x, min_y), (max_x, max_y)
|
||||
|
||||
def convert_bounds_to(self, unit, value):
|
||||
""" :py:meth:`.LengthUnit.convert_to` but for ((min_x, min_y), (max_x, max_y)) bounding box tuples. """
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
(min_x, min_y), (max_x, max_y) = value
|
||||
min_x = self.convert_to(unit, min_x)
|
||||
min_y = self.convert_to(unit, min_y)
|
||||
max_x = self.convert_to(unit, max_x)
|
||||
max_y = self.convert_to(unit, max_y)
|
||||
return (min_x, min_y), (max_x, max_y)
|
||||
|
||||
def format(self, value):
|
||||
""" Return a human-readdable string representing value in this unit.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue