diff options
Diffstat (limited to 'gerbonara/gerber/aperture_macros')
-rw-r--r-- | gerbonara/gerber/aperture_macros/expression.py | 211 | ||||
-rw-r--r-- | gerbonara/gerber/aperture_macros/parse.py | 181 | ||||
-rw-r--r-- | gerbonara/gerber/aperture_macros/primitive.py | 270 |
3 files changed, 0 insertions, 662 deletions
diff --git a/gerbonara/gerber/aperture_macros/expression.py b/gerbonara/gerber/aperture_macros/expression.py deleted file mode 100644 index 0cf055a..0000000 --- a/gerbonara/gerber/aperture_macros/expression.py +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2021 Jan Götte <gerbonara@jaseg.de> - -import operator -import re -import ast - -from ..utils import MM, Inch, MILLIMETERS_PER_INCH - - -def expr(obj): - return obj if isinstance(obj, Expression) else ConstantExpression(obj) - - -class Expression: - def optimized(self, variable_binding={}): - return self - - def __str__(self): - return f'<{self.to_gerber()}>' - - def __repr__(self): - return f'<E {self.to_gerber()}>' - - def converted(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}') - 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): - self._expr = expr - self.unit = unit - - def to_gerber(self, unit=None): - return self.converted(unit).optimized().to_gerber() - - def __eq__(self, other): - return type(other) == type(self) and \ - self.unit == other.unit and\ - self._expr == other._expr - - def __str__(self): - return f'<{self._expr.to_gerber()} {self.unit}>' - - def __repr__(self): - 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 - - elif MM == unit: - return self._expr * MILLIMETERS_PER_INCH - - elif Inch == unit: - return self._expr / MILLIMETERS_PER_INCH - - else: - raise ValueError(f'invalid unit {unit}, must be "inch" or "mm".') - - def __add__(self, other): - if not isinstance(other, UnitExpression): - 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) - - if other.unit == 'mm': # -> and self.unit == 'inch' - 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) - - def __radd__(self, other): - # left hand side cannot have been an UnitExpression or __radd__ would not have been called - raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.') - - def __sub__(self, other): - return (self + (-other)).optimize() - - def __rsub__(self, other): - # see __radd__ above - raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.') - - def __mul__(self, other): - return UnitExpression(self._expr * other, self.unit) - - def __rmul__(self, other): - return UnitExpression(other * self._expr, self.unit) - - def __truediv__(self, other): - return UnitExpression(self._expr / other, self.unit) - - def __rtruediv__(self, other): - return UnitExpression(other / self._expr, self.unit) - - def __neg__(self): - return UnitExpression(-self._expr, self.unit) - - def __pos__(self): - return self - - -class ConstantExpression(Expression): - def __init__(self, value): - self.value = value - - def __float__(self): - return float(self.value) - - def __eq__(self, other): - return type(self) == type(other) and self.value == other.value - - def to_gerber(self, _unit=None): - return f'{self.value:.6f}'.rstrip('0').rstrip('.') - - -class VariableExpression(Expression): - def __init__(self, number): - self.number = number - - def optimized(self, variable_binding={}): - if self.number in variable_binding: - return ConstantExpression(variable_binding[self.number]) - return self - - def __eq__(self, other): - return type(self) == type(other) and \ - self.number == other.number - - def to_gerber(self, _unit=None): - return f'${self.number}' - - -class OperatorExpression(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 - - def __eq__(self, other): - return type(self) == type(other) and \ - self.op == other.op and \ - self.l == other.l and \ - self.r == other.r - - def optimized(self, variable_binding={}): - l = self.l.optimized(variable_binding) - r = self.r.optimized(variable_binding) - - if self.op in (operator.add, operator.mul): - if id(r) < id(l): - l, r = r, l - - if isinstance(l, ConstantExpression) and isinstance(r, ConstantExpression): - return ConstantExpression(self.op(float(l), float(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) - - if isinstance(self.l, OperatorExpression): - lval = f'({lval})' - if isinstance(self.r, OperatorExpression): - rval = f'({rval})' - - op = {operator.add: '+', - operator.sub: '-', - operator.mul: 'X', - operator.truediv: '/'} [self.op] - - return f'{lval}{op}{rval}' - diff --git a/gerbonara/gerber/aperture_macros/parse.py b/gerbonara/gerber/aperture_macros/parse.py deleted file mode 100644 index 0fa936f..0000000 --- a/gerbonara/gerber/aperture_macros/parse.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2021 Jan Götte <gerbonara@jaseg.de> - -import operator -import re -import ast -import copy -import math - -from . import primitive as ap -from .expression import * -from ..utils import MM - -def rad_to_deg(x): - return (x / math.pi) * 180 - -def _map_expression(node): - 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)) - - elif isinstance(node, ast.UnaryOp): - if type(node.op) == ast.UAdd: - return _map_expression(node.operand) - else: - return OperatorExpression(operator.sub, ConstantExpression(0), _map_expression(node.operand)) - - elif isinstance(node, ast.Name): - return VariableExpression(int(node.id[3:])) # node.id has format var[0-9]+ - - else: - raise SyntaxError('Invalid aperture macro expression') - -def _parse_expression(expr): - 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) - -class ApertureMacro: - def __init__(self, name=None, primitives=None, variables=None): - self._name = name - self.comments = [] - self.variables = variables or {} - self.primitives = primitives or [] - - @classmethod - def parse_macro(cls, name, body, unit): - macro = cls(name) - - blocks = re.sub(r'\s', '', body).split('*') - for block in blocks: - if not (block := block.strip()): # empty block - continue - - if block[0:1] == '0 ': # comment - macro.comments.append(Comment(block[2:])) - - if block[0] == '$': # variable definition - name, expr = block.partition('=') - number = int(name[1:]) - if number in macro.variables: - raise SyntaxError(f'Re-definition of aperture macro variable {number} inside macro') - macro.variables[number] = _parse_expression(expr) - - else: # primitive - primitive, *args = block.split(',') - args = [ _parse_expression(arg) for arg in args ] - primitive = ap.PRIMITIVE_CLASSES[int(primitive)](unit=unit, args=args) - macro.primitives.append(primitive) - - return macro - - @property - def name(self): - if self._name is not None: - return self._name - else: - return f'gn_{hash(self)}' - - @name.setter - def name(self, name): - self._name = name - - def __str__(self): - return f'<Aperture macro {self.name}, variables {str(self.variables)}, primitives {self.primitives}>' - - 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: - try: - if primitive.exposure.calculate(): - primitive.dilate(offset, unit) - new_primitives.append(primitive) - except IndexError: - warnings.warn('Cannot dilate aperture macro primitive with exposure value computed from macro variable.') - pass - dup.primitives = new_primitives - return dup - - def to_gerber(self, unit=None): - comments = [ c.to_gerber() for c in self.comments ] - variable_defs = [ f'${var.to_gerber(unit)}={expr}' for var, expr in self.variables.items() ] - 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) - 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}') - 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 - - -cons, var = ConstantExpression, VariableExpression -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])] - - # 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)]) - - rect = ApertureMacro('GNR', [ - ap.CenterLine('mm', [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]), - *_generic_hole(3) ]) - - # w must be larger than h - obround = ApertureMacro('GNO', [ - ap.CenterLine('mm', [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]), - ap.Circle('mm', [1, var(2), +var(1)/2, 0, var(5) * -deg_per_rad]), - ap.Circle('mm', [1, var(2), -var(1)/2, 0, var(5) * -deg_per_rad]), - *_generic_hole(3) ]) - - polygon = ApertureMacro('GNP', [ - ap.Polygon('mm', [1, var(2), 0, 0, var(1), var(3) * -deg_per_rad]), - ap.Circle('mm', [0, var(4), 0, 0])]) - - -if __name__ == '__main__': - import sys - #for line in sys.stdin: - #expr = _parse_expression(line.strip()) - #print(expr, '->', expr.optimized()) - - for primitive in parse_macro(sys.stdin.read(), 'mm'): - print(primitive) diff --git a/gerbonara/gerber/aperture_macros/primitive.py b/gerbonara/gerber/aperture_macros/primitive.py deleted file mode 100644 index 8732520..0000000 --- a/gerbonara/gerber/aperture_macros/primitive.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> -# Copyright 2022 Jan Götte <gerbonara@jaseg.de> - -import warnings -import contextlib -import math - -from .expression import Expression, UnitExpression, ConstantExpression, expr - -from .. import graphic_primitives 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 - -class Primitive: - def __init__(self, unit, args): - self.unit = unit - - if len(args) > len(type(self).__annotations__): - raise ValueError(f'Too many arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})') - - for arg, (name, fieldtype) in zip(args, type(self).__annotations__.items()): - arg = expr(arg) # convert int/float to Expression object - - if fieldtype == UnitExpression: - setattr(self, name, UnitExpression(arg, unit)) - else: - setattr(self, name, arg) - - 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 to_gerber(self, unit=None): - return f'{self.code},' + ','.join( - getattr(self, name).to_gerber(unit) for name in type(self).__annotations__) - - def __str__(self): - attrs = ','.join(str(getattr(self, name)).strip('<>') for name in type(self).__annotations__) - return f'<{type(self).__name__} {attrs}>' - - def __repr__(self): - return str(self) - - 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 - - def __getattr__(self, name): - return getattr(self.instance, name).calculate(self.variable_binding, self.unit) - - def __call__(self, expr): - return expr.calculate(self.variable_binding, self.unit) - - -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) - - def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): - with self.Calculator(self, 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.diameter/2, polarity_dark=(bool(calc.exposure) == polarity_dark)) ] - - def dilate(self, offset, unit): - self.diameter += UnitExpression(offset, unit) - -class VectorLine(Primitive): - code = 20 - exposure : Expression - width : UnitExpression - start_x : UnitExpression - start_y : UnitExpression - end_x : UnitExpression - end_y : UnitExpression - rotation : Expression - - def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): - with self.Calculator(self, 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) == polarity_dark)) ] - - def dilate(self, offset, unit): - self.width += UnitExpression(2*offset, unit) - - -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, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): - with self.Calculator(self, 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) == polarity_dark)) ] - - def dilate(self, offset, unit): - self.width += UnitExpression(2*offset, unit) - - -class Polygon(Primitive): - code = 5 - exposure : Expression - n_vertices : Expression - # center x/y - x : UnitExpression - y : UnitExpression - diameter : UnitExpression - rotation : Expression - - 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) - 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) == polarity_dark)) ] - - def dilate(self, offset, unit): - self.diameter += UnitExpression(2*offset, unit) - - -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 - - 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) - x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0) - x, y = x+offset[0], y+offset[1] - - dark = (bool(calc.exposure) == polarity_dark) - - 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), - ] - - def dilate(self, offset, unit): - # I'd rather print a warning and produce graphically slightly incorrect output in these few cases here than - # producing macros that may evaluate to primitives with negative values. - warnings.warn('Attempted dilation of macro aperture thermal primitive. This is not supported.') - - -class Outline(Primitive): - code = 4 - - 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: - raise ValueError(f'Invalid aperture macro outline primitive, too many points ({len(args)//2-2}).') - - self.exposure = args.pop(0) - - # length arg must not contain variables (that would not make sense) - length_arg = args.pop(0).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 = args.pop() - else: - self.rotation = ConstantExpression(0.0) - - 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]}') - - self.coords = [(UnitExpression(x, unit), UnitExpression(y, unit)) for x, y in zip(args[0::2], args[1::2])] - - 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) - 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 = [ gp.rotate_point(calc(x), calc(y), rotation, 0, 0) for x, y in self.coords ] - 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): - # we would need a whole polygon offset/clipping library here - warnings.warn('Attempted dilation of macro aperture outline primitive. This is not supported.') - - -class Comment: - code = 0 - - def __init__(self, comment): - self.comment = comment - - def to_gerber(self, unit=None): - return f'0 {self.comment}' - -PRIMITIVE_CLASSES = { - **{cls.code: cls for cls in [ - Comment, - Circle, - VectorLine, - CenterLine, - Outline, - Polygon, - Thermal, - ]}, - # alternative codes - 2: VectorLine, -} - |