Aperture macro parser works
This commit is contained in:
parent
f833483b72
commit
7415f9a584
5 changed files with 270 additions and 1254 deletions
|
|
@ -1,244 +0,0 @@
|
|||
#!/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()))
|
||||
File diff suppressed because it is too large
Load diff
185
gerbonara/gerber/aperture_macros/expression.py
Normal file
185
gerbonara/gerber/aperture_macros/expression.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2021 Jan Götte <gerbonara@jaseg.de>
|
||||
|
||||
import operator
|
||||
import re
|
||||
import ast
|
||||
|
||||
class Expression(object):
|
||||
@property
|
||||
def value(self):
|
||||
return self
|
||||
|
||||
def optimized(self, variable_binding={}):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return f'<{self.to_gerber()}>'
|
||||
|
||||
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 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('.')
|
||||
|
||||
|
||||
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(r), float(l)))
|
||||
|
||||
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}'
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def parse_macro(macro, unit):
|
||||
blocks = re.sub(r'\s', '', macro).split('*')
|
||||
variables = {}
|
||||
for block in blocks:
|
||||
block = block.strip()
|
||||
|
||||
if block[0:1] == '0 ': # comment
|
||||
continue
|
||||
|
||||
elif 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:
|
||||
expr = _parse_expression(line.strip())
|
||||
print(expr, '->', expr.optimized())
|
||||
85
gerbonara/gerber/apertures.py
Normal file
85
gerbonara/gerber/apertures.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from primitives import Primitive
|
||||
|
||||
def _flash_hole(self, x, y):
|
||||
if self.hole_rect_h is not None:
|
||||
return self.primitives(x, y), Rectangle((x, y), (self.hole_dia, self.hole_rect_h), polarity_dark=False)
|
||||
else:
|
||||
return self.primitives(x, y), Circle((x, y), self.hole_dia, polarity_dark=False)
|
||||
|
||||
class Aperture:
|
||||
@property
|
||||
def hole_shape(self):
|
||||
if self.hole_rect_h is not None:
|
||||
return 'rect'
|
||||
else:
|
||||
return 'circle'
|
||||
|
||||
@property
|
||||
def hole_size(self):
|
||||
return (self.hole_dia, self.hole_rect_h)
|
||||
|
||||
def flash(self, x, y):
|
||||
return self.primitives(x, y)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApertureCircle(Aperture):
|
||||
diameter : float
|
||||
hole_dia : float = 0
|
||||
hole_rect_h : float = None
|
||||
|
||||
def primitives(self, x, y):
|
||||
return Circle((x, y), self.diameter, polarity_dark=True),
|
||||
|
||||
flash = _flash_hole
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApertureRectangle(Aperture):
|
||||
w : float
|
||||
h : float
|
||||
hole_dia : float = 0
|
||||
hole_rect_h : float = None
|
||||
|
||||
def primitives(self, x, y):
|
||||
return Rectangle((x, y), (self.w, self.h), polarity_dark=True),
|
||||
|
||||
flash = _flash_hole
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApertureObround(Aperture):
|
||||
w : float
|
||||
h : float
|
||||
hole_dia : float = 0
|
||||
hole_rect_h : float = None
|
||||
|
||||
def primitives(self, x, y):
|
||||
return Obround((x, y), self.w, self.h, polarity_dark=True)
|
||||
|
||||
flash = _flash_hole
|
||||
|
||||
|
||||
@dataclass
|
||||
class AperturePolygon(Aperture):
|
||||
diameter : float
|
||||
n_vertices : int
|
||||
hole_dia : float = 0
|
||||
hole_rect_h : float = None
|
||||
|
||||
def primitives(self, x, y):
|
||||
return Polygon((x, y), diameter, n_vertices, rotation, polarity_dark=True),
|
||||
|
||||
flash = _flash_hole
|
||||
|
||||
class MacroAperture(Aperture):
|
||||
parameters : [float]
|
||||
self.macro : ApertureMacro
|
||||
|
||||
def primitives(self, x, y):
|
||||
return self.macro.execute(x, y, self.parameters)
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue