aperture macros: work around gerbv/jlc wonkiness
This commit is contained in:
parent
9af0713445
commit
74fb384c4c
9 changed files with 216 additions and 78 deletions
|
|
@ -31,10 +31,13 @@ class Expression:
|
|||
def converted(self, unit):
|
||||
return self
|
||||
|
||||
def replace_mixed_subexpressions(self, unit):
|
||||
return self
|
||||
|
||||
def calculate(self, variable_binding={}, unit=None):
|
||||
expr = self.converted(unit).optimized(variable_binding)
|
||||
if not isinstance(expr, ConstantExpression):
|
||||
raise IndexError(f'Cannot fully resolve expression due to unresolved variables: {expr} with variables {variable_binding}')
|
||||
raise IndexError(f'Cannot fully resolve expression due to unresolved parameters: residual expression {expr} under parameters {variable_binding}')
|
||||
return expr.value
|
||||
|
||||
def __add__(self, other):
|
||||
|
|
@ -67,6 +70,13 @@ class Expression:
|
|||
def __pos__(self):
|
||||
return self
|
||||
|
||||
def parameters(self):
|
||||
return tuple()
|
||||
|
||||
@property
|
||||
def _operator(self):
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class UnitExpression(Expression):
|
||||
|
|
@ -80,8 +90,8 @@ class UnitExpression(Expression):
|
|||
object.__setattr__(self, 'expr', expr)
|
||||
object.__setattr__(self, 'unit', unit)
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
return self.converted(unit).optimized().to_gerber()
|
||||
def to_gerber(self, register_variable=None, unit=None):
|
||||
return self.converted(unit).optimized().to_gerber(register_variable)
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(other) == type(self) and \
|
||||
|
|
@ -94,6 +104,9 @@ class UnitExpression(Expression):
|
|||
def __repr__(self):
|
||||
return f'<UE {self.expr.to_gerber()} {self.unit}>'
|
||||
|
||||
def replace_mixed_subexpressions(self, unit):
|
||||
return self.converted(unit).replace_mixed_subexpressions(unit)
|
||||
|
||||
def converted(self, unit):
|
||||
if self.unit is None or unit is None or self.unit == unit:
|
||||
return self.expr
|
||||
|
|
@ -148,6 +161,10 @@ class UnitExpression(Expression):
|
|||
def __pos__(self):
|
||||
return self
|
||||
|
||||
def parameters(self):
|
||||
return self.expr.parameters()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ConstantExpression(Expression):
|
||||
value: float
|
||||
|
|
@ -161,12 +178,38 @@ class ConstantExpression(Expression):
|
|||
except TypeError:
|
||||
return False
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
def to_gerber(self, register_variable=None, unit=None):
|
||||
if self == 0: # Avoid producing "-0" for negative floating point zeros
|
||||
return '0'
|
||||
return f'{self.value:.6f}'.rstrip('0').rstrip('.')
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VariableExpression(Expression):
|
||||
expr: Expression
|
||||
|
||||
def optimized(self, variable_binding={}):
|
||||
opt = self.expr.optimized(variable_binding)
|
||||
if isinstance(opt, OperatorExpression):
|
||||
return self
|
||||
else:
|
||||
return opt
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.expr == other.expr
|
||||
|
||||
def replace_mixed_subexpressions(self, unit):
|
||||
return VariableExpression(self.expr.replace_mixed_subexpressions(unit))
|
||||
|
||||
def to_gerber(self, register_variable=None, unit=None):
|
||||
if register_variable is None:
|
||||
return self.expr.to_gerber(None, unit)
|
||||
else:
|
||||
num = register_variable(self.expr.converted(unit).optimized())
|
||||
return f'${num}'
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ParameterExpression(Expression):
|
||||
number: int
|
||||
|
||||
def optimized(self, variable_binding={}):
|
||||
|
|
@ -178,9 +221,13 @@ class VariableExpression(Expression):
|
|||
return type(self) == type(other) and \
|
||||
self.number == other.number
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
def to_gerber(self, register_variable=None, unit=None):
|
||||
return f'${self.number}'
|
||||
|
||||
def parameters(self):
|
||||
yield self
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NegatedExpression(Expression):
|
||||
value: Expression
|
||||
|
|
@ -196,17 +243,24 @@ class NegatedExpression(Expression):
|
|||
# -(x-y) == y-x
|
||||
case OperatorExpression(operator.sub, l, r):
|
||||
return OperatorExpression(operator.sub, r, l)
|
||||
|
||||
# Round very small values and negative floating point zeros to a (positive) zero
|
||||
case 0:
|
||||
return expr(0)
|
||||
# Default case
|
||||
case x:
|
||||
return NegatedExpression(x)
|
||||
|
||||
@property
|
||||
def _operator(self):
|
||||
return self.value._operator
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and \
|
||||
self.value == other.value
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
val_str = self.value.to_gerber(unit)
|
||||
if isinstance(self.value, VariableExpression):
|
||||
def to_gerber(self, register_variable=None, unit=None):
|
||||
val_str = self.value.to_gerber(register_variable, unit)
|
||||
if isinstance(self.value, (VariableExpression, ParameterExpression)):
|
||||
return f'-{val_str}'
|
||||
else:
|
||||
return f'-({val_str})'
|
||||
|
|
@ -229,6 +283,10 @@ class OperatorExpression(Expression):
|
|||
self.l == other.l and \
|
||||
self.r == other.r
|
||||
|
||||
@property
|
||||
def _operator(self):
|
||||
return self.op
|
||||
|
||||
def optimized(self, variable_binding={}):
|
||||
l = self.l.optimized(variable_binding)
|
||||
r = self.r.optimized(variable_binding)
|
||||
|
|
@ -297,10 +355,21 @@ class OperatorExpression(Expression):
|
|||
return OperatorExpression(self.op, l, r)
|
||||
|
||||
return expr(rv).optimized(variable_binding)
|
||||
|
||||
def replace_mixed_subexpressions(self, unit):
|
||||
l = self.l.replace_mixed_subexpressions(unit)
|
||||
if l._operator not in (None, self.op):
|
||||
l = VariableExpression(self.l)
|
||||
|
||||
r = self.r.replace_mixed_subexpressions(unit)
|
||||
if r._operator not in (None, self.op):
|
||||
r = VariableExpression(self.r)
|
||||
|
||||
return OperatorExpression(self.op, l, r)
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
lval = self.l.to_gerber(unit)
|
||||
rval = self.r.to_gerber(unit)
|
||||
def to_gerber(self, register_variable=None, unit=None):
|
||||
lval = self.l.to_gerber(register_variable, unit)
|
||||
rval = self.r.to_gerber(register_variable, unit)
|
||||
|
||||
if isinstance(self.l, OperatorExpression):
|
||||
lval = f'({lval})'
|
||||
|
|
@ -314,3 +383,7 @@ class OperatorExpression(Expression):
|
|||
|
||||
return f'{lval}{op}{rval}'
|
||||
|
||||
def parameters(self):
|
||||
yield from self.l.parameters()
|
||||
yield from self.r.parameters()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,40 +18,47 @@ from ..utils import MM
|
|||
def rad_to_deg(x):
|
||||
return (x / math.pi) * 180
|
||||
|
||||
def _map_expression(node):
|
||||
def _map_expression(node, variables={}, parameters=set()):
|
||||
if isinstance(node, ast.Num):
|
||||
return ConstantExpression(node.n)
|
||||
|
||||
elif isinstance(node, ast.BinOp):
|
||||
op_map = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv}
|
||||
return OperatorExpression(op_map[type(node.op)], _map_expression(node.left), _map_expression(node.right))
|
||||
return OperatorExpression(op_map[type(node.op)],
|
||||
_map_expression(node.left, variables, parameters),
|
||||
_map_expression(node.right, variables, parameters))
|
||||
|
||||
elif isinstance(node, ast.UnaryOp):
|
||||
if type(node.op) == ast.UAdd:
|
||||
return _map_expression(node.operand)
|
||||
return _map_expression(node.operand, variables, parameters)
|
||||
else:
|
||||
return NegatedExpression(_map_expression(node.operand))
|
||||
return NegatedExpression(_map_expression(node.operand, variables, parameters))
|
||||
|
||||
elif isinstance(node, ast.Name):
|
||||
return VariableExpression(int(node.id[3:])) # node.id has format var[0-9]+
|
||||
num = int(node.id[3:]) # node.id has format var[0-9]+
|
||||
if num in variables:
|
||||
return VariableExpression(variables[num])
|
||||
else:
|
||||
parameters.add(num)
|
||||
return ParameterExpression(num)
|
||||
|
||||
else:
|
||||
raise SyntaxError('Invalid aperture macro expression')
|
||||
|
||||
def _parse_expression(expr):
|
||||
def _parse_expression(expr, variables, parameters):
|
||||
expr = expr.lower().replace('x', '*')
|
||||
expr = re.sub(r'\$([0-9]+)', r'var\1', expr)
|
||||
try:
|
||||
parsed = ast.parse(expr, mode='eval').body
|
||||
except SyntaxError as e:
|
||||
raise SyntaxError('Invalid aperture macro expression') from e
|
||||
return _map_expression(parsed)
|
||||
return _map_expression(parsed, variables, parameters)
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApertureMacro:
|
||||
name: str = field(default=None, hash=False, compare=False)
|
||||
num_parameters: int = 0
|
||||
primitives: tuple = ()
|
||||
variables: tuple = ()
|
||||
comments: tuple = field(default=(), hash=False, compare=False)
|
||||
|
||||
def __post_init__(self):
|
||||
|
|
@ -66,6 +73,7 @@ class ApertureMacro:
|
|||
def parse_macro(kls, macro_name, body, unit):
|
||||
comments = []
|
||||
variables = {}
|
||||
parameters = set()
|
||||
primitives = []
|
||||
|
||||
blocks = body.split('*')
|
||||
|
|
@ -83,19 +91,18 @@ class ApertureMacro:
|
|||
name, expr = block.partition('=')
|
||||
number = int(name[1:])
|
||||
if number in variables:
|
||||
raise SyntaxError(f'Re-definition of aperture macro variable {number} inside macro')
|
||||
variables[number] = _parse_expression(expr)
|
||||
raise SyntaxError(f'Re-definition of aperture macro variable ${number} inside macro. Previous definition of ${number} was ${variables[number]}.')
|
||||
variables[number] = _parse_expression(expr, variables, parameters)
|
||||
|
||||
else: # primitive
|
||||
primitive, *args = block.split(',')
|
||||
args = [ _parse_expression(arg) for arg in args ]
|
||||
args = [ _parse_expression(arg, variables, parameters) for arg in args ]
|
||||
primitives.append(ap.PRIMITIVE_CLASSES[int(primitive)].from_arglist(unit, args))
|
||||
|
||||
variables = [variables.get(i+1) for i in range(max(variables.keys(), default=0))]
|
||||
return kls(macro_name, tuple(primitives), tuple(variables), tuple(comments))
|
||||
return kls(macro_name, max(parameters, default=0), tuple(primitives), tuple(comments))
|
||||
|
||||
def __str__(self):
|
||||
return f'<Aperture macro {self.name}, variables {str(self.variables)}, primitives {self.primitives}>'
|
||||
return f'<Aperture macro {self.name}, primitives {self.primitives}>'
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
|
@ -111,11 +118,32 @@ class ApertureMacro:
|
|||
pass
|
||||
return replace(self, primitives=tuple(new_primitives))
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
def to_gerber(self, settings):
|
||||
""" Serialize this macro's content (without the name) into Gerber using the given file unit """
|
||||
comments = [ f'0 {c.replace("*", "_").replace("%", "_")}' for c in self.comments ]
|
||||
variable_defs = [ f'${var}={str(expr)[1:-1]}' for var, expr in enumerate(self.variables, start=1) if expr is not None ]
|
||||
primitive_defs = [ prim.to_gerber(unit) for prim in self.primitives ]
|
||||
|
||||
subexpression_variables = {}
|
||||
def register_variable(expr):
|
||||
if not settings.allow_mixed_operators_in_aperture_macros:
|
||||
expr = expr.replace_mixed_subexpressions(unit=settings.unit)
|
||||
|
||||
expr_str = expr.to_gerber(register_variable, settings.unit)
|
||||
if expr_str not in subexpression_variables:
|
||||
subexpression_variables[expr_str] = self.num_parameters + 1 + len(subexpression_variables)
|
||||
|
||||
return subexpression_variables[expr_str]
|
||||
|
||||
primitive_defs = []
|
||||
for prim in self.primitives:
|
||||
if not settings.allow_mixed_operators_in_aperture_macros:
|
||||
prim = prim.replace_mixed_subexpressions(unit=settings.unit)
|
||||
|
||||
primitive_defs.append(prim.to_gerber(register_variable, settings))
|
||||
|
||||
variable_defs = []
|
||||
for expr_str, num in subexpression_variables.items():
|
||||
variable_defs.append(f'${num}={expr_str}')
|
||||
|
||||
return '*\n'.join(comments + variable_defs + primitive_defs)
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation, parameters : [float], unit=None, polarity_dark=True):
|
||||
|
|
@ -138,7 +166,7 @@ class ApertureMacro:
|
|||
primitive.scaled(scale) for primitive in self.primitives))
|
||||
|
||||
|
||||
var = VariableExpression
|
||||
var = ParameterExpression
|
||||
deg_per_rad = 180 / math.pi
|
||||
|
||||
class GenericMacros:
|
||||
|
|
@ -147,16 +175,16 @@ class GenericMacros:
|
|||
|
||||
# NOTE: All generic macros have rotation values specified in **clockwise radians** like the rest of the user-facing
|
||||
# API.
|
||||
circle = ApertureMacro('GNC', (
|
||||
circle = ApertureMacro('GNC', 4, (
|
||||
ap.Circle('mm', 1, var(1), 0, 0, var(4) * -deg_per_rad),
|
||||
*_generic_hole(2)))
|
||||
|
||||
rect = ApertureMacro('GNR', (
|
||||
rect = ApertureMacro('GNR', 5, (
|
||||
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', (
|
||||
rounded_rect = ApertureMacro('GRR', 6, (
|
||||
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),
|
||||
|
|
@ -166,7 +194,7 @@ class GenericMacros:
|
|||
*_generic_hole(4)))
|
||||
|
||||
# params: width, height, length difference between narrow side (top) and wide side (bottom), *hole, rotation
|
||||
isosceles_trapezoid = ApertureMacro('GTR', (
|
||||
isosceles_trapezoid = ApertureMacro('GTR', 6, (
|
||||
ap.Outline('mm', 1, 4,
|
||||
(var(1)/-2, var(2)/-2,
|
||||
var(1)/-2+var(3)/2, var(2)/2,
|
||||
|
|
@ -177,14 +205,14 @@ class GenericMacros:
|
|||
*_generic_hole(4)))
|
||||
|
||||
# params: width, height, length difference between narrow side (top) and wide side (bottom), margin, *hole, rotation
|
||||
rounded_isosceles_trapezoid = ApertureMacro('GRTR', (
|
||||
rounded_isosceles_trapezoid = ApertureMacro('GRTR', 7, (
|
||||
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),
|
||||
var(7) * -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,),
|
||||
|
|
@ -209,13 +237,13 @@ class GenericMacros:
|
|||
|
||||
# w must be larger than h
|
||||
# params: width, height, *hole, rotation
|
||||
obround = ApertureMacro('GNO', (
|
||||
obround = ApertureMacro('GNO', 5, (
|
||||
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', (
|
||||
polygon = ApertureMacro('GNP', 4, (
|
||||
ap.Polygon('mm', 1, var(2), 0, 0, var(1), var(3) * -deg_per_rad),
|
||||
ap.Circle('mm', 0, var(4), 0, 0)))
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import warnings
|
||||
import contextlib
|
||||
import math
|
||||
from dataclasses import dataclass, fields
|
||||
from dataclasses import dataclass, fields, replace
|
||||
|
||||
from .expression import Expression, UnitExpression, ConstantExpression, expr
|
||||
|
||||
|
|
@ -46,9 +46,20 @@ class Primitive:
|
|||
elif field.type == Expression:
|
||||
object.__setattr__(self, field.name, expr(getattr(self, field.name)))
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
def to_gerber(self, register_variable=None, settings=None):
|
||||
return f'{self.code},' + ','.join(
|
||||
getattr(self, field.name).optimized().to_gerber(unit) for field in fields(self) if field.name != 'unit')
|
||||
getattr(self, field.name).optimized().to_gerber(register_variable, settings.unit)
|
||||
for field in fields(self) if issubclass(field.type, Expression))
|
||||
|
||||
def replace_mixed_subexpressions(self, unit):
|
||||
print('prim rms')
|
||||
import pprint
|
||||
out = replace(self, **{
|
||||
field.name: getattr(self, field.name).optimized().replace_mixed_subexpressions(unit)
|
||||
for field in fields(self) if issubclass(field.type, Expression)})
|
||||
pprint.pprint(self)
|
||||
pprint.pprint(out)
|
||||
return out
|
||||
|
||||
def __str__(self):
|
||||
attrs = ','.join(str(getattr(self, name)).strip('<>') for name in type(self).__annotations__)
|
||||
|
|
@ -61,6 +72,11 @@ class Primitive:
|
|||
def from_arglist(kls, unit, arglist):
|
||||
return kls(unit, *arglist)
|
||||
|
||||
def parameters(self):
|
||||
for field in fields(self):
|
||||
if issubclass(field.type, Expression):
|
||||
yield from getattr(self, field.name).parameters()
|
||||
|
||||
class Calculator:
|
||||
def __init__(self, instance, variable_binding={}, unit=None):
|
||||
self.instance = instance
|
||||
|
|
@ -253,9 +269,6 @@ class Outline(Primitive):
|
|||
object.__setattr__(self, 'exposure', expr(self.exposure))
|
||||
|
||||
if self.length.calculate() != len(self.coords)//2-1:
|
||||
print(self.length, self.length.calculate(), len(self.coords))
|
||||
import pprint
|
||||
pprint.pprint(self.coords)
|
||||
raise ValueError('length must exactly equal number of segments, which is the number of points minus one')
|
||||
|
||||
if self.coords[-2:] != self.coords[:2]:
|
||||
|
|
@ -279,21 +292,33 @@ class Outline(Primitive):
|
|||
def __str__(self):
|
||||
return f'<Outline {len(self.coords)} points>'
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
def to_gerber(self, register_variable=None, settings=None):
|
||||
# Calculate out rotation since at least gerbv mis-renders Outlines with rotation other than zero.
|
||||
rotation = self.rotation.optimized()
|
||||
coords = self.coords
|
||||
if isinstance(rotation, ConstantExpression):
|
||||
if isinstance(rotation, ConstantExpression) and rotation != 0:
|
||||
rotation = math.radians(rotation.value)
|
||||
# This will work even with variables in x and y, we just need to pass in cx and cy as UnitExpressions
|
||||
unit_zero = UnitExpression(expr(0), MM)
|
||||
coords = [ rotate_point(x, y, -rotation, cx=unit_zero, cy=unit_zero) for x, y in self.points ]
|
||||
coords = [ e for point in coords for e in point ]
|
||||
if not settings.allow_mixed_operators_in_aperture_macros:
|
||||
coords = [e.replace_mixed_subexpressions(unit=settings.unit) for e in coords]
|
||||
|
||||
rotation = ConstantExpression(0)
|
||||
|
||||
coords = ','.join(coord.optimized().to_gerber(unit) for coord in coords)
|
||||
return f'{self.code},{self.exposure.optimized().to_gerber()},{len(self.coords)//2-1},{coords},{rotation.to_gerber()}'
|
||||
coords = ','.join(coord.optimized().to_gerber(register_variable, settings.unit) for coord in coords)
|
||||
return f'{self.code},{self.exposure.optimized().to_gerber(register_variable)},{len(self.coords)//2-1},{coords},{rotation.to_gerber(register_variable)}'
|
||||
|
||||
def replace_mixed_subexpressions(self, unit):
|
||||
return replace(Primitive.replace_mixed_subexpressions(self, unit),
|
||||
coords=[e.replace_mixed_subexpressions(unit) for e in self.coords])
|
||||
|
||||
def parameters(self):
|
||||
yield from Primitive.parameters(self)
|
||||
|
||||
for expr in self.coords:
|
||||
yield from expr.parameters()
|
||||
|
||||
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
|
||||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
|
|
@ -316,7 +341,7 @@ class Comment:
|
|||
code = 0
|
||||
comment: str
|
||||
|
||||
def to_gerber(self, unit=None):
|
||||
def to_gerber(self, register_variable=None, settings=None):
|
||||
return f'0 {self.comment}'
|
||||
|
||||
def dilated(self, offset, unit):
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
import warnings
|
||||
import math
|
||||
from dataclasses import dataclass, replace, field, fields, InitVar, KW_ONLY
|
||||
from functools import lru_cache
|
||||
|
|
@ -448,6 +449,10 @@ class ApertureMacroInstance(Aperture):
|
|||
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.
|
||||
return tuple(self.parameters)
|
||||
parameters = self.parameters
|
||||
if len(parameters) > self.macro.num_parameters:
|
||||
warnings.warn('Aperture definition using macro {self.macro.name} has more parameters than the macro uses.')
|
||||
parameters = parameters[:self.macro.num_parameters]
|
||||
return tuple(parameters)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ class FileSettings:
|
|||
zeros : bool = None
|
||||
#: Number format. ``(integer, decimal)`` tuple of number of integer and decimal digits. At most ``(6,7)`` by spec.
|
||||
number_format : tuple = (None, None)
|
||||
#: At least the aperture macro implementations of gerbv and whatever JLCPCB uses are severely broken and simply
|
||||
#: ignore parentheses in numeric expressions without throwing an error or a warning, leading to broken rendering.
|
||||
#: To avoid trouble with severely broken software like this, we split out any non-trivial numeric sub-expressions
|
||||
#: into separate internal macro variables by default.
|
||||
#: If you want to export the macros with their original formulaic expressions (which is completely fine by the
|
||||
#: Gerber standard, btw), set this parameter to ``True`` before exporting.
|
||||
allow_mixed_operators_in_aperture_macros: bool = False
|
||||
|
||||
# input validation
|
||||
def __setattr__(self, name, value):
|
||||
|
|
|
|||
|
|
@ -881,7 +881,7 @@ class LayerStack:
|
|||
|
||||
sc_y, tl_y = -1, (bounds[0][1] + bounds[1][1])
|
||||
if side == 'bottom':
|
||||
sc_x, sc_y = -1, (bounds[0][0] + bounds[1][0])
|
||||
sc_x, tl_x = -1, (bounds[0][0] + bounds[1][0])
|
||||
else:
|
||||
sc_x, tl_x = 1, 0
|
||||
layer_group = tag('g', layers, transform=f'translate({tl_x} {tl_y}) scale({sc_x} {sc_y})')
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class GerberFile(CamFile):
|
|||
nonlocal cache, settings
|
||||
if isinstance(aperture, apertures.ApertureMacroInstance):
|
||||
macro = aperture.macro
|
||||
macro_def = macro.to_gerber(unit=settings.unit)
|
||||
macro_def = macro.to_gerber(settings)
|
||||
if macro_def not in cache:
|
||||
cache[macro_def] = macro
|
||||
|
||||
|
|
@ -283,7 +283,7 @@ class GerberFile(CamFile):
|
|||
|
||||
self.dedup_apertures()
|
||||
|
||||
am_stmt = lambda macro: f'%AM{macro.name}*\n{macro.to_gerber(unit=settings.unit)}*\n%'
|
||||
am_stmt = lambda macro: f'%AM{macro.name}*\n{macro.to_gerber(settings)}*\n%'
|
||||
for macro in self.aperture_macros():
|
||||
yield am_stmt(macro)
|
||||
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ def kicad_fp_export(mod_file, out_svg):
|
|||
fp_name = mod_file.name[:-len('.kicad_mod')]
|
||||
cmd = ['podman', 'run', '--mount', f'type=bind,src={pretty_dir},dst=/{pretty_dir.name}',
|
||||
'--mount', f'type=bind,src={tmpdir},dst=/out',
|
||||
'registry.gitlab.com/kicad/kicad-ci/kicad-cli-docker/kicad:nightly',
|
||||
'registry.hub.docker.com/kicad/kicad:nightly',
|
||||
'kicad-cli', 'fp', 'export', 'svg', '--output', '/out', '--footprint', fp_name, f'/{pretty_dir.name}']
|
||||
subprocess.run(cmd, check=True) #, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
out_file = Path(tmpdir) / f'{fp_name}.svg'
|
||||
|
|
|
|||
|
|
@ -43,28 +43,28 @@ def file_mock():
|
|||
|
||||
|
||||
class TestRender:
|
||||
def invoke(self, *args):
|
||||
def invoke(self, outfile, *args):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(cli.render, list(map(str, args)))
|
||||
print(res.output)
|
||||
outfile.write_text(str(res.output))
|
||||
if res.exception:
|
||||
raise res.exception
|
||||
assert res.exit_code == 0
|
||||
return res.output
|
||||
|
||||
def test_basic(self):
|
||||
assert self.invoke('--version').startswith('Version ')
|
||||
def test_basic(self, tmpfile):
|
||||
assert self.invoke(tmpfile('Standard output', '.svg'), '--version').startswith('Version ')
|
||||
|
||||
@pytest.mark.parametrize('reference', ['example_flash_obround.gbr'], indirect=True)
|
||||
def test_warnings(self, reference):
|
||||
def test_warnings(self, reference, tmpfile):
|
||||
with pytest.warns(UserWarning):
|
||||
self.invoke(reference, '--warnings=once')
|
||||
self.invoke(tmpfile('Standard output', '.svg'), reference, '--warnings=once')
|
||||
|
||||
@pytest.mark.parametrize('reference', ['kicad-older'], indirect=True)
|
||||
def test_side(self, reference):
|
||||
without = self.invoke(reference, '--warnings=ignore')
|
||||
top = self.invoke(reference, '--top', '--warnings=ignore')
|
||||
bottom = self.invoke(reference, '--bottom', '--warnings=ignore')
|
||||
def test_side(self, reference, tmpfile):
|
||||
without = self.invoke(tmpfile('Standard output, without args', '.svg'), reference, '--warnings=ignore')
|
||||
top = self.invoke(tmpfile('Standard output, --top', '.svg'), reference, '--top', '--warnings=ignore')
|
||||
bottom = self.invoke(tmpfile('Standard output, --bottom', '.svg'), reference, '--bottom', '--warnings=ignore')
|
||||
assert top.strip().startswith('<?xml')
|
||||
assert bottom.strip().startswith('<?xml')
|
||||
assert '<path' in top
|
||||
|
|
@ -133,22 +133,22 @@ class TestRender:
|
|||
|
||||
|
||||
class TestRewrite:
|
||||
def invoke(self, *args):
|
||||
def invoke(self, outfile, *args):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(cli.rewrite, list(map(str, args)))
|
||||
print(res.output)
|
||||
outfile.write_text(res.output)
|
||||
if res.exception:
|
||||
raise res.exception
|
||||
assert res.exit_code == 0
|
||||
return res.output
|
||||
|
||||
def test_basic(self):
|
||||
assert self.invoke('--version').startswith('Version ')
|
||||
def test_basic(self, tmpfile):
|
||||
assert self.invoke(tmpfile('Standard output', '.svg'), '--version').startswith('Version ')
|
||||
|
||||
@pytest.mark.parametrize('reference', ['example_flash_obround.gbr'], indirect=True)
|
||||
def test_transforms(self, reference, file_mock):
|
||||
def test_transforms(self, reference, file_mock, tmpfile):
|
||||
with tempfile.NamedTemporaryFile() as tmpout:
|
||||
self.invoke(reference, tmpout.name, '--transform', 'rotate(90); translate(10, 10); rotate(-45.5); scale(2)')
|
||||
self.invoke(tmpfile('Standard output', '.svg'), reference, tmpout.name, '--transform', 'rotate(90); translate(10, 10); rotate(-45.5); scale(2)')
|
||||
file_mock.rotate.assert_has_calls([
|
||||
mock.call(math.radians(90), 0, 0, MM),
|
||||
mock.call(math.radians(-45.5), 0, 0, MM)])
|
||||
|
|
@ -158,9 +158,9 @@ class TestRewrite:
|
|||
assert file_mock.save.call_args[0][0] == tmpout.name
|
||||
|
||||
@pytest.mark.parametrize('reference', ['example_flash_obround.gbr'], indirect=True)
|
||||
def test_real_invocation(self, reference):
|
||||
def test_real_invocation(self, reference, tmpfile):
|
||||
with tempfile.NamedTemporaryFile() as tmpout:
|
||||
self.invoke(reference, tmpout.name, '--transform', 'rotate(45); translate(10, 0)')
|
||||
self.invoke(tmpfile('Standard output', '.svg'), reference, tmpout.name, '--transform', 'rotate(45); translate(10, 0)')
|
||||
assert tmpout.read()
|
||||
|
||||
|
||||
|
|
@ -188,19 +188,19 @@ class TestMerge:
|
|||
|
||||
|
||||
class TestMeta:
|
||||
def invoke(self, *args):
|
||||
def invoke(self, outfile, *args):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(cli.meta, list(map(str, args)))
|
||||
print(res.output)
|
||||
outfile.write_text(str(res.output))
|
||||
if res.exception:
|
||||
raise res.exception
|
||||
assert res.exit_code == 0
|
||||
return res.output
|
||||
|
||||
def test_basic(self):
|
||||
assert self.invoke('--version').startswith('Version ')
|
||||
def test_basic(self, tmpfile):
|
||||
assert self.invoke(tmpfile('Standard output', '.svg'), '--version').startswith('Version ')
|
||||
|
||||
@pytest.mark.parametrize('reference', ['example_flash_obround.gbr'], indirect=True)
|
||||
def test_real_invocation(self, reference):
|
||||
j = json.loads(self.invoke(reference, '--warnings', 'ignore'))
|
||||
def test_real_invocation(self, reference, tmpfile):
|
||||
j = json.loads(self.invoke(tmpfile('Standard output', '.svg'), reference, '--warnings', 'ignore'))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue