From 63e1eae8d81cb7940d3547511488f8ec4acd4d1c Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 28 Dec 2021 21:40:22 +0100 Subject: WIP --- gerbonara/gerber/aperture_macros/expression.py | 37 ++++- gerbonara/gerber/aperture_macros/primitive.py | 204 +++++++++++++++++-------- 2 files changed, 172 insertions(+), 69 deletions(-) (limited to 'gerbonara/gerber/aperture_macros') diff --git a/gerbonara/gerber/aperture_macros/expression.py b/gerbonara/gerber/aperture_macros/expression.py index 74fbd90..ddd8d53 100644 --- a/gerbonara/gerber/aperture_macros/expression.py +++ b/gerbonara/gerber/aperture_macros/expression.py @@ -8,6 +8,10 @@ import re import ast +def expr(obj): + return obj if isinstance(obj, Expression) else ConstantExpression(obj) + + class Expression(object): @property def value(self): @@ -28,6 +32,35 @@ class Expression(object): raise IndexError(f'Cannot fully resolve expression due to unresolved variables: {expr} with variables {variable_binding}') return expr.value + def __add__(self, other): + return OperatorExpression(operator.add, self, expr(other)).optimized() + + def __radd__(self, other): + return expr(other) + self + + def __sub__(self, other): + return OperatorExpression(operator.sub, self, expr(other)).optimized() + + def __rsub__(self, other): + return expr(other) - self + + def __mul__(self, other): + return OperatorExpression(operator.mul, self, expr(other)).optimized() + + def __rmul__(self, other): + return expr(other) * self + + def __truediv__(self, other): + return OperatorExpression(operator.truediv, self, expr(other)).optimized() + + def __rtruediv__(self, other): + return expr(other) / self + + def __neg__(self): + return 0 - self + + def __pos__(self): + return self class UnitExpression(Expression): def __init__(self, expr, unit): @@ -50,10 +83,10 @@ class UnitExpression(Expression): return self._expr elif unit == 'mm': - return OperatorExpression.mul(self._expr, MILLIMETERS_PER_INCH) + return self._expr * MILLIMETERS_PER_INCH elif unit == 'inch': - return OperatorExpression.div(self._expr, MILLIMETERS_PER_INCH) + return self._expr / MILLIMETERS_PER_INCH) else: raise ValueError('invalid unit, must be "inch" or "mm".') diff --git a/gerbonara/gerber/aperture_macros/primitive.py b/gerbonara/gerber/aperture_macros/primitive.py index f4400f5..aeb38c4 100644 --- a/gerbonara/gerber/aperture_macros/primitive.py +++ b/gerbonara/gerber/aperture_macros/primitive.py @@ -2,24 +2,37 @@ # -*- coding: utf-8 -*- # Copyright 2019 Hiroshi Murayama +# Copyright 2022 Jan Götte + +import contextlib +import math + +from expression import Expression, UnitExpression, ConstantExpression, expr + +from .. import graphic_primitivese as gp + + +def point_distance(a, b): + x1, y1 = a + x2, y2 = b + return math.sqrt((x2 - x1)**2 + (y2 - y1)**2) + +def deg_to_rad(a): + return (a / 180) * math.pi -from dataclasses import dataclass, fields -from expression import Expression, UnitExpression, ConstantExpression class Primitive: - def __init__(self, unit, args, is_abstract): + def __init__(self, unit, args): self.unit = unit - self.is_abstract = is_abstract 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()): - if is_abstract: - if fieldtype == UnitExpression: - setattr(self, name, UnitExpression(arg, unit)) - else: - setattr(self, name, arg) + arg = expr(arg) # convert int/float to Expression object + + if fieldtype == UnitExpression: + setattr(self, name, UnitExpression(arg, unit)) else: setattr(self, name, arg) @@ -28,8 +41,6 @@ class Primitive: raise ValueError(f'Too few arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})') def to_gerber(self, unit=None): - if not self.is_abstract: - raise TypeError(f"Something went wrong, tried to gerber'ize bound aperture macro primitive {self}") return self.code + ',' + ','.join( getattr(self, name).to_gerber(unit) for name in type(self).__annotations__) + '*' @@ -37,27 +48,42 @@ class Primitive: attrs = ','.join(str(getattr(self, name)).strip('<>') for name in type(self).__annotations__) return f'<{type(self).__name__} {attrs}>' - def bind(self, variable_binding={}): - if not self.is_abstract: - raise TypeError('{type(self).__name__} object is already instantiated, cannot bind again.') - # Return instance of the same class, but replace all attributes by their actual numeric values - return type(self)(unit=self.unit, is_abstract=False, args=[ - getattr(self, name).calculate(variable_binding) for name in type(self).__annotations__ - ]) + @contextlib.contextmanager + class Calculator: + def __init__(self, instance, variable_binding={}, unit=None): + self.instance = instance + self.variable_binding = variable_binding + self.unit = unit + + def __enter__(self): + return self + + def __exit__(self, _type, _value, _traceback): + pass -class CommentPrimitive(Primitive): - code = 0 - comment : str + def __getattr__(self, name): + return getattr(self.instance, name).calculate(self.variable_binding, self.unit) -class CirclePrimitive(Primitive): + def __call__(self, expr): + return expr.calculate(self.variable_binding, self.unit) + + +class Circle(Primitive): code = 1 exposure : Expression diameter : UnitExpression - center_x : UnitExpression - center_y : UnitExpression + # center x/y + x : UnitExpression + y : UnitExpression rotation : Expression = ConstantExpression(0.0) -class VectorLinePrimitive(Primitive): + def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None): + with self.Calculator(variable_binding, unit) as calc: + x, y = gp.rotate_point(calc.x, calc.y, deg_to_rad(calc.rotation) + rotation, 0, 0) + x, y = x+offset[0], y+offset[1] + return [ gp.Circle(x, y, calc.r, polarity_dark=bool(calc.exposure)) ] + +class VectorLine(Primitive): code = 20 exposure : Expression width : UnitExpression @@ -67,40 +93,90 @@ class VectorLinePrimitive(Primitive): end_y : UnitExpression rotation : Expression -class CenterLinePrimitive(Primitive): + def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None): + with self.Calculator(variable_binding, unit) as calc: + center_x = (calc.end_x + calc.start_x) / 2 + center_y = (calc.end_y + calc.start_y) / 2 + delta_x = calc.end_x - calc.start_x + delta_y = calc.end_y - calc.start_y + length = point_distance((calc.start_x, calc.start_y), (calc.end_x, calc.end_y)) + + center_x, center_y = center_x+offset[0], center_y+offset[1] + rotation += deg_to_rad(calc.rotation) + math.atan2(delta_y, delta_x) + + return [ gp.Rectangle(center_x, center_y, length, calc.width, rotation=rotation, + polarity_dark=bool(calc.exposure)) ] + + +class CenterLine(Primitive): code = 21 exposure : Expression width : UnitExpression height : UnitExpression + # center x/y x : UnitExpression y : UnitExpression rotation : Expression + def to_graphic_primitives(self, variable_binding={}, unit=None): + with self.Calculator(variable_binding, unit) as calc: + rotation += deg_to_rad(calc.rotation) + x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0) + x, y = x+offset[0], y+offset[1] + w, h = calc.width, calc.height + + return [ gp.Rectangle(x, y, w, h, rotation, polarity_dark=bool(calc.exposure)) ] + -class PolygonPrimitive(Primitive): +class Polygon(Primitive): code = 5 exposure : Expression n_vertices : Expression - center_x : UnitExpression - center_y : UnitExpression + # center x/y + x : UnitExpression + y : UnitExpression diameter : UnitExpression rotation : Expression + def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None): + with self.Calculator(variable_binding, unit) as calc: + rotation += deg_to_rad(calc.rotation) + x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0) + x, y = x+offset[0], y+offset[1] + return [ gp.RegularPolygon(calc.x, calc.y, calc.diameter/2, calc.n_vertices, rotation, + polarity_dark=bool(calc.exposure)) ] + -class ThermalPrimitive(Primitive): +class Thermal(Primitive): code = 7 - center_x : UnitExpression - center_y : UnitExpression + # center x/y + x : UnitExpression + y : UnitExpression d_outer : UnitExpression d_inner : UnitExpression gap_w : UnitExpression rotation : Expression + def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None): + with self.Calculator(variable_binding, unit) as calc: + rotation += deg_to_rad(calc.rotation) + x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0) + x, y = x+offset[0], y+offset[1] + + dark = bool(calc.exposure) + + return [ + gp.Circle(x, y, calc.d_outer/2, polarity_dark=dark), + gp.Circle(x, y, calc.d_inner/2, polarity_dark=not dark), + gp.Rectangle(x, y, d_outer, gap_w, rotation=rotation, polarity_dark=not dark), + gp.Rectangle(x, y, gap_w, d_outer, rotation=rotation, polarity_dark=not dark), + ] -class OutlinePrimitive(Primitive): + +class Outline(Primitive): code = 4 - def __init__(self, unit, args, is_abstract): + def __init__(self, unit, args): if len(args) < 11: raise ValueError(f'Invalid aperture macro outline primitive, not enough parameters ({len(args)}).') if len(args) > 5004: @@ -108,42 +184,36 @@ class OutlinePrimitive(Primitive): self.exposure = args[0] - if is_abstract: - # length arg must not contain variabels (that would not make sense) - length_arg = args[1].calculate() - - if length_arg != len(args)//2 - 2: - raise ValueError(f'Invalid aperture macro outline primitive, given size does not match length of coordinate list({len(args)}).') + # length arg must not contain variables (that would not make sense) + length_arg = args[1].calculate() - if len(args) % 1 != 1: - self.rotation = args.pop() - else: - self.rotation = ConstantExpression(0.0) + if length_arg != len(args)//2 - 2: + raise ValueError(f'Invalid aperture macro outline primitive, given size does not match length of coordinate list({len(args)}).') - if args[2] != args[-2] or args[3] != args[-1]: - raise ValueError(f'Invalid aperture macro outline primitive, polygon is not closed {args[2:4], args[-3:-1]}') + if len(args) % 1 != 1: + self.rotation = args.pop() + else: + self.rotation = ConstantExpression(0.0) - self.coords = [UnitExpression(arg, unit) for arg in args[1:]] + if args[2] != args[-2] or args[3] != args[-1]: + raise ValueError(f'Invalid aperture macro outline primitive, polygon is not closed {args[2:4], args[-3:-1]}') - else: - if len(args) % 1 != 1: - self.rotation = args.pop() - else: - self.rotation = 0 + self.coords = [(UnitExpression(x, unit), UnitExpression(y, unit)) for x, y in zip(args[1::2], args[2::2])] - self.coords = args[1:] - def to_gerber(self, unit=None): - if not self.is_abstract: - raise TypeError(f"Something went wrong, tried to gerber'ize bound aperture macro primitive {self}") coords = ','.join(coord.to_gerber(unit) for coord in self.coords) return f'{self.code},{self.exposure.to_gerber()},{len(self.coords)//2-1},{coords},{self.rotation.to_gerber()}' - def bind(self, variable_binding={}): - if not self.is_abstract: - raise TypeError('{type(self).__name__} object is already instantiated, cannot bind again.') + def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None): + with self.Calculator(variable_binding, unit) as calc: + bound_coords = [ (calc(x)+offset[0], calc(y)+offset[1]) for x, y in self.coords ] + bound_radii = [None] * len(bound_coords) + + rotation += deg_to_rad(calc.rotation) + bound_coords = [ rotate_point(*p, rotation, 0, 0) for p in bound_coords ] + + return gp.ArcPoly(bound_coords, bound_radii, polarity_dark=calc.exposure) - return OutlinePrimitive(self.unit, is_abstract=False, args=[None, *self.coords, self.rotation]) class Comment: def __init__(self, comment): @@ -154,13 +224,13 @@ class Comment: PRIMITIVE_CLASSES = { **{cls.code: cls for cls in [ - CommentPrimitive, - CirclePrimitive, - VectorLinePrimitive, - CenterLinePrimitive, - OutlinePrimitive, - PolygonPrimitive, - ThermalPrimitive, + Comment, + Circle, + VectorLine, + CenterLine, + Outline, + Polygon, + Thermal, ]}, # alternative codes 2: VectorLinePrimitive, -- cgit