summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/aperture_macros
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber/aperture_macros')
-rw-r--r--gerbonara/gerber/aperture_macros/expression.py211
-rw-r--r--gerbonara/gerber/aperture_macros/parse.py181
-rw-r--r--gerbonara/gerber/aperture_macros/primitive.py270
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,
-}
-