diff options
Diffstat (limited to 'gerbonara/gerber/aperture_macros/am_expression.py')
-rw-r--r-- | gerbonara/gerber/aperture_macros/am_expression.py | 244 |
1 files changed, 244 insertions, 0 deletions
diff --git a/gerbonara/gerber/aperture_macros/am_expression.py b/gerbonara/gerber/aperture_macros/am_expression.py new file mode 100644 index 0000000..809f60e --- /dev/null +++ b/gerbonara/gerber/aperture_macros/am_expression.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2021 Jan Götte <gerbonara@jaseg.de> + +import operator +import re + +class Expression(object): + @property + def value(self): + return self + + def optimized(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'<{str(self.expr)[1:-1]} {self.unit}>' + + def converted(self, unit): + if unit is None or self.unit == unit: + return self._expr + + elif unit == 'mm': + return OperatorExpression.mul(self._expr, MILLIMETERS_PER_INCH) + + elif unit == 'inch': + return OperatorExpression.div(self._expr, MILLIMETERS_PER_INCH) + + else: + raise ValueError('invalid unit, must be "inch" or "mm".') + + 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}') + + +class ConstantExpression(Expression): + def __init__(self, value): + self._value = value + + @property + def value(self): + return self._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): + if isinstance(self._value, str): + return self._value + return f'{self.value:.6f}'.rstrip('0').rstrip('.') + + def __str__(self): + return f'<{self._value}>' + + +class VariableExpression(Expression): + def __init__(self, number): + self.number = number + + def optimized(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}' + + def __str__(self): + return f'<@{self.number}>' + + +class OperatorExpression(Expression): + def __init__(self, op, l, r): + super(OperatorExpression, self).__init__(Expression.OPERATOR) + 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.lvalue == other.lvalue and \ + self.rvalue == other.rvalue + + def optimized(self, variable_binding={}): + l = self.lvalue.optimized(variable_binding) + r = self.rvalue.optimized(variable_binding) + + if self.op in (operator.add, operator.mul): + if hash(r) < hash(l): + l, r = r, l + + if isinstance(l, ConstantExpression) and isinstance(r, ConstantExpression): + return ConstantExpression(self.op(float(r), float(l))) + + return OperatorExpression(self.op, l, r) + + def to_gerber(self, unit=None): + lval = self.lvalue.to_gerber(unit) + rval = self.rvalue.to_gerber(unit) + op = {OperatorExpression.ADD: '+', + OperatorExpression.SUB: '-', + OperatorExpression.MUL: 'x', + OperatorExpression.DIV: '/'} [self.op] + return f'({lval}{op}{rval})' + + def __str__(self): + op = {operator.add: '+', operator.sub: '-', operator.mul: '*', operator.truediv: '/'}[self.op] + return f'<{str(self.lvalue)[1:-1]} {op} {str(self.rvalue)[1:-1]}>' + +operator_map = { + '+': operator.add, + '-': operator.sub, + 'x': operator.mul, + 'X': operator.mul, + '/': operator.truediv, + } + +precedence_map = { + operator.add : 0, + operator.sub : 0, + operator.mul : 1, + operator.truediv : 1, + } + +def _parse_expression(expr_str): + output_stack = [] + operator_stack = [] + + drop_unary = lambda s: (s[0] == '-', s[1:] if s[0] in '-+' else s) + negate = lambda expr: OperatorExpression(operator.sub, ConstantExpression(0), expr) + + # See http://faculty.cs.niu.edu/~hutchins/csci241/eval.htm + # We handle the unary +/- operators by including them into variable/number/parenthesis tokens. + for variable, number, operator, parenthesis in re.findall(r'([-+]?\$[0-9]+)|([-+]?[0-9]+)|([-+]?\(|\))|([-+xX/])', expr_str): + + if variable: + is_negative, variable = drop_unary(variable) + var_ex = VariableExpression(int(variable[1:])) + output_stack.append(negate(var_ex) if is_negative else var_ex) + + +def _parse_expression(expr_str): + output_stack = [] + operator_stack = [] + + drop_unary = lambda s: (s[0] == '-', s[1:] if s[0] in '-+' else s) + negate = lambda expr: OperatorExpression(operator.sub, ConstantExpression(0), expr) + + # See http://faculty.cs.niu.edu/~hutchins/csci241/eval.htm + # We handle the unary +/- operators by including them into variable/number/parenthesis tokens. + for variable, number, operator, parenthesis in re.findall(r'([-+xX/])|([-+]?\$[0-9]+)|([-+]?[0-9]+\.?[0-9]*)|([()])', expr_str): + + if variable: + is_negative, variable = drop_unary(variable) + var_ex = VariableExpression(int(variable[1:])) + output_stack.append(negate(var_ex) if is_negative else var_ex) + + elif number: + output_stack.append(ConstantExpression(float(number))) + + elif parenthesis[-1] == '(': # be careful, we might have a leading unary +/- here! + is_negative, parenthesis = drop_unary(parenthesis) + if is_negative: + operator_stack.push('-') + operator_stack.push('(') + + elif parenthesis == ')': # here we cannot have a leading unary +/- + if not operator_stack: + raise SyntaxError('Unbalanced parenthesis in aperture macro expression') + + while operator_stack and not operator_stack[-1] == '(': + op = operator_stack.pop() + l, r = output_stack.pop(), output_stack.pop() + output_stack.append(OperatorExpression(op, l, r)) + + assert output_stack.pop() == '(' + if output_stack[-1] == '-': + output_stack.append(negate(output_stack.pop())) + + elif operator: + operator = operator_map[operator] + + if not operator_stack or operator_stack[-1] == '(': + operator_stack.push(operator) + + else: + while operator_stack and operator_stack[-1] != '(' and\ + precedence_map[operator] <= precedence_map[operator_stack[-1]]: + output_stack.append(OperatorExpression(operator_stack.pop(), output_stack.pop(), output_stack.pop())) + operator_stack.push(operator) + + for operator in reversed(operator_stack): + if operator == '(': + raise SyntaxError('Unbalanced parenthesis in aperture macro expression') + + output_stack.append(OperatorExpression(operator_stack.pop(), output_stack.pop(), output_stack.pop())) + print(output_stack, operator_stack) + + if len(output_stack) != 1: + raise SyntaxError('Invalid aperture macro expression') + + return output_stack[0] + +def parse_macro(macro, unit): + blocks = re.sub(r'\s', '', macro).split('*') + variables = {} + for block in blocks: + block = block.strip() + if block[0] == '$': # variable definition + name, expr = block.partition('=') + variables[int(name[1:])] = _parse_expression(expr) + else: # primitive + primitive, args = block.split(',') + yield PRIMITIVE_CLASSES[int(primitive)](unit=unit, args=list(map(_parse_expression, args))) + +if __name__ == '__main__': + import sys + for line in sys.stdin: + print(_parse_expression(line.strip())) |