summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/aperture_macros
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2021-11-11 12:10:56 +0100
committerjaseg <git@jaseg.de>2021-11-11 12:10:56 +0100
commit7415f9a5848853fb7a2b5b91bbe438d85728e33a (patch)
tree8fe096b83b81e6bccbcfd3efe288af49f2d5995b /gerbonara/gerber/aperture_macros
parentf833483b72cd4fdf56cd4130dc9174ebfac8673d (diff)
downloadgerbonara-7415f9a5848853fb7a2b5b91bbe438d85728e33a.tar.gz
gerbonara-7415f9a5848853fb7a2b5b91bbe438d85728e33a.tar.bz2
gerbonara-7415f9a5848853fb7a2b5b91bbe438d85728e33a.zip
Aperture macro parser works
Diffstat (limited to 'gerbonara/gerber/aperture_macros')
-rw-r--r--gerbonara/gerber/aperture_macros/am_expression.py244
-rw-r--r--gerbonara/gerber/aperture_macros/am_statements.py1010
-rw-r--r--gerbonara/gerber/aperture_macros/expression.py185
-rw-r--r--gerbonara/gerber/aperture_macros/primitive.py (renamed from gerbonara/gerber/aperture_macros/am_primitive.py)0
4 files changed, 185 insertions, 1254 deletions
diff --git a/gerbonara/gerber/aperture_macros/am_expression.py b/gerbonara/gerber/aperture_macros/am_expression.py
deleted file mode 100644
index 809f60e..0000000
--- a/gerbonara/gerber/aperture_macros/am_expression.py
+++ /dev/null
@@ -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()))
diff --git a/gerbonara/gerber/aperture_macros/am_statements.py b/gerbonara/gerber/aperture_macros/am_statements.py
deleted file mode 100644
index 61ddf42..0000000
--- a/gerbonara/gerber/aperture_macros/am_statements.py
+++ /dev/null
@@ -1,1010 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be> and Paulo Henrique Silva
-# <ph.silva@gmail.com>
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from math import asin
-import math
-
-from .primitives import *
-from .utils import validate_coordinates, inch, metric, rotate_point
-from .am_expression import AMConstantExpression
-
-
-
-# TODO: Add support for aperture macro variables
-__all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive',
- 'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive',
- 'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive',
- 'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive']
-
-
-class AMPrimitive(object):
- """ Aperture Macro Primitive Base Class
-
- Parameters
- ----------
- code : int
- primitive shape code
-
- exposure : str
- on or off Primitives with exposure on create a slid part of
- the macro aperture, and primitives with exposure off erase the
- solid part created previously in the aperture macro definition.
- .. note::
- The erasing effect is limited to the aperture definition in
- which it occurs.
-
- Returns
- -------
- primitive : :class: `gerber.am_statements.AMPrimitive`
-
- Raises
- ------
- TypeError, ValueError
- """
-
- def __init__(self, code, exposure=None, rotation=AMConstantExpression(0)):
- VALID_CODES = (0, 1, 2, 4, 5, 7, 20, 21, 22, 9999)
- if not isinstance(code, int):
- raise TypeError('Aperture Macro Primitive code must be an integer')
- elif code not in VALID_CODES:
- raise ValueError('Invalid Code. Valid codes are %s.' %
- ', '.join(map(str, VALID_CODES)))
- if exposure is not None and exposure.lower() not in ('on', 'off'):
- raise ValueError('Exposure must be either on or off')
- self.code = code
- self.exposure = exposure.lower() if exposure is not None else None
- self.rotation = rotation
-
- def rotate(self, angle, center=None):
- self.rotation = AMOperatorExpression(AMOperatorExpression.ADD,
- self.rotation,
- AMConstantExpression(float(angle)))
- self.rotation = self.rotation.optimize()
-
- #def to_inch(self):
- # raise NotImplementedError('Subclass must implement to_inch')
-
- #def to_metric(self):
- # raise NotImplementedError('Subclass must implement to_metric')
-
- #def to_gerber(self, settings=None):
- # raise NotImplementedError('Subclass must implement to_gerber')
-
- #def to_instructions(self):
- # raise NotImplementedError('Subclass must implement to_instructions')
-
- #def to_primitive(self, units):
- # """ Return a Primitive instance based on the specified macro params.
- # """
- # raise NotImplementedError('Subclass must implement to_primitive')
-
- @property
- def _level_polarity(self):
- if self.exposure == 'off':
- return 'clear'
- return 'dark'
-
- def __eq__(self, other):
- return self.__dict__ == other.__dict__
-
-
-class AMCommentPrimitive(AMPrimitive):
- """ Aperture Macro Comment primitive. Code 0
-
- The comment primitive has no image meaning. It is used to include human-
- readable comments into the AM command.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.1:** Comment, primitive code 0
-
- Parameters
- ----------
- code : int
- Aperture Macro primitive code. 0 Indicates an AMCommentPrimitive
-
- comment : str
- The comment as a string.
-
- Returns
- -------
- CommentPrimitive : :class:`gerber.am_statements.AMCommentPrimitive`
- An Initialized AMCommentPrimitive
-
- Raises
- ------
- ValueError
- """
- @classmethod
- def from_gerber(cls, primitive):
- primitive = primitive.strip()
- code = int(primitive[0])
- comment = primitive[1:]
- return cls(code, comment)
-
- def __init__(self, code, comment):
- if code != 0:
- raise ValueError('Not a valid Aperture Macro Comment statement')
- super().__init__(code)
- self.comment = comment.strip(' *')
-
- def to_inch(self):
- pass
-
- def to_metric(self):
- pass
-
- def to_gerber(self, settings=None):
- return f'0 {self.comment} *'
-
- def to_primitive(self, units):
- """
- Returns None - has not primitive representation
- """
- return None
-
- def to_instructions(self):
- return [(OpCode.PUSH, self.comment), (OpCode.PRIM, self.code)]
-
- def __str__(self):
- return '<Aperture Macro Comment: %s>' % self.comment
-
-
-class AMCirclePrimitive(AMPrimitive):
- """ Aperture macro Circle primitive. Code 1
-
- A circle primitive is defined by its center point and diameter.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.2:** Circle, primitive code 1
-
- Parameters
- ----------
- code : int
- Circle Primitive code. Must be 1
-
- exposure : string
- 'on' or 'off'
-
- diameter : float
- Circle diameter
-
- position : tuple (<float>, <float>)
- Position of the circle relative to the macro origin
-
- Returns
- -------
- CirclePrimitive : :class:`gerber.am_statements.AMCirclePrimitive`
- An initialized AMCirclePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(',')
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- diameter = float(modifiers[2])
- position = (float(modifiers[3]), float(modifiers[4]))
- return cls(code, exposure, diameter, position)
-
- @classmethod
- def from_primitive(cls, primitive):
- return cls(1, 'on', primitive.diameter, primitive.position)
-
- def __init__(self, code, exposure, diameter, position):
- validate_coordinates(position)
- if code != 1:
- raise ValueError('CirclePrimitive code is 1')
- super(AMCirclePrimitive, self).__init__(code, exposure)
- self.diameter = diameter
- self.position = position
-
- def to_inch(self):
- self.diameter = inch(self.diameter)
- self.position = tuple([inch(x) for x in self.position])
-
- def to_metric(self):
- self.diameter = metric(self.diameter)
- self.position = tuple([metric(x) for x in self.position])
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.position
- return f'{self.code},{exposure},{self.diameter},{x},{y}*'
-
- def to_primitive(self, units):
- return Circle((self.position), self.diameter, units=units, level_polarity=self._level_polarity)
-
-
-class AMVectorLinePrimitive(AMPrimitive):
- """ Aperture Macro Vector Line primitive. Code 2 or 20.
-
- A vector line is a rectangle defined by its line width, start, and end
- points. The line ends are rectangular.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.3:** Vector Line, primitive code 2 or 20.
-
- Parameters
- ----------
- code : int
- Vector Line Primitive code. Must be either 2 or 20.
-
- exposure : string
- 'on' or 'off'
-
- width : float
- Line width
-
- start : tuple (<float>, <float>)
- coordinate of line start point
-
- end : tuple (<float>, <float>)
- coordinate of line end point
-
- rotation : float
- Line rotation about the origin.
-
- Returns
- -------
- LinePrimitive : :class:`gerber.am_statements.AMVectorLinePrimitive`
- An initialized AMVectorLinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
- return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(',')
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- width = float(modifiers[2])
- start = (float(modifiers[3]), float(modifiers[4]))
- end = (float(modifiers[5]), float(modifiers[6]))
- rotation = float(modifiers[7])
- return cls(code, exposure, width, start, end, rotation)
-
- def __init__(self, code, exposure, width, start, end, rotation):
- validate_coordinates(start)
- validate_coordinates(end)
- if code not in (2, 20):
- raise ValueError('VectorLinePrimitive codes are 2 or 20')
- super(AMVectorLinePrimitive, self).__init__(code, exposure)
- self.width = width
- self.start = start
- self.end = end
- self.rotation = rotation
-
- def to_inch(self):
- self.width = inch(self.width)
- self.start = tuple([inch(x) for x in self.start])
- self.end = tuple([inch(x) for x in self.end])
-
- def to_metric(self):
- self.width = metric(self.width)
- self.start = tuple([metric(x) for x in self.start])
- self.end = tuple([metric(x) for x in self.end])
-
- def to_gerber(self, settings=None):
- exp = 1 if self.exposure == 'on' else 0
- start_x, start_y = self.start
- end_x, end_y = self.end
- return f'{self.code},{exp},{self.width},{start_x},{start_y},{end_x},{end_y},{self.rotation}*'
-
- def to_primitive(self, units):
- """
- Convert this to a primitive. We use the Outline to represent this (instead of Line)
- because the behaviour of the end caps is different for aperture macros compared to Lines
- when rotated.
- """
-
- # Use a line to generate our vertices easily
- line = Line(self.start, self.end, Rectangle(None, self.width, self.width))
- vertices = line.vertices
-
- aperture = Circle((0, 0), 0)
-
- lines = []
- prev_point = rotate_point(vertices[-1], self.rotation, (0, 0))
- for point in vertices:
- cur_point = rotate_point(point, self.rotation, (0, 0))
-
- lines.append(Line(prev_point, cur_point, aperture))
-
- return Outline(lines, units=units, level_polarity=self._level_polarity)
-
-
-class AMOutlinePrimitive(AMPrimitive):
- """ Aperture Macro Outline primitive. Code 4.
-
- An outline primitive is an area enclosed by an n-point polygon defined by
- its start point and n subsequent points. The outline must be closed, i.e.
- the last point must be equal to the start point. Self intersecting
- outlines are not allowed.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.6:** Outline, primitive code 4.
-
- Parameters
- ----------
- code : int
- OutlinePrimitive code. Must be 6.
-
- exposure : string
- 'on' or 'off'
-
- start_point : tuple (<float>, <float>)
- coordinate of outline start point
-
- points : list of tuples (<float>, <float>)
- coordinates of subsequent points
-
- rotation : float
- outline rotation about the origin.
-
- Returns
- -------
- OutlinePrimitive : :class:`gerber.am_statements.AMOutlineinePrimitive`
- An initialized AMOutlinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
-
- start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6))
- points = []
- for prim in primitive.primitives:
- points.append((round(prim.end[0], 6), round(prim.end[1], 6)))
-
- rotation = 0.0
-
- return cls(4, 'on', start_point, points, rotation)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
-
- code = int(modifiers[0])
- exposure = "on" if float(modifiers[1]) == 1 else "off"
- n = int(float(modifiers[2]))
- start_point = (float(modifiers[3]), float(modifiers[4]))
- points = []
- for i in range(n):
- points.append((float(modifiers[5 + i * 2]),
- float(modifiers[5 + i * 2 + 1])))
- rotation = float(modifiers[-1])
- return cls(code, exposure, start_point, points, rotation)
-
- def __init__(self, code, exposure, start_point, points, rotation):
- """ Initialize AMOutlinePrimitive
- """
- validate_coordinates(start_point)
- for point in points:
- validate_coordinates(point)
- if code != 4:
- raise ValueError('OutlinePrimitive code is 4')
- super(AMOutlinePrimitive, self).__init__(code, exposure)
- self.start_point = start_point
- if points[-1] != start_point:
- raise ValueError('OutlinePrimitive must be closed')
- self.points = points
- self.rotation = rotation
-
- def to_inch(self):
- self.start_point = tuple([inch(x) for x in self.start_point])
- self.points = tuple([(inch(x), inch(y)) for x, y in self.points])
-
- def to_metric(self):
- self.start_point = tuple([metric(x) for x in self.start_point])
- self.points = tuple([(metric(x), metric(y)) for x, y in self.points])
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x0, y0 = self.start_point
- points = ",\n".join([ f'{x:.6f},{y:.6f}' for x, y in self.points ])
- return f'{self.code},{exposure},{len(self.points)},{x0:.6f},{y0:.6f},{points},{self.rotation}*'
-
- def to_primitive(self, units):
- """
- Convert this to a drawable primitive. This uses the Outline instead of Line
- primitive to handle differences in end caps when rotated.
- """
-
- lines = []
- prev_point = rotate_point(self.start_point, self.rotation)
- for point in self.points:
- cur_point = rotate_point(point, self.rotation)
-
- lines.append(Line(prev_point, cur_point, Circle((0,0), 0)))
-
- prev_point = cur_point
-
- if lines[0].start != lines[-1].end:
- raise ValueError('Outline must be closed')
-
- return Outline(lines, units=units, level_polarity=self._level_polarity)
-
-
-class AMPolygonPrimitive(AMPrimitive):
- """ Aperture Macro Polygon primitive. Code 5.
-
- A polygon primitive is a regular polygon defined by the number of
- vertices, the center point, and the diameter of the circumscribed circle.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.8:** Polygon, primitive code 5.
-
- Parameters
- ----------
- code : int
- PolygonPrimitive code. Must be 5.
-
- exposure : string
- 'on' or 'off'
-
- vertices : int, 3 <= vertices <= 12
- Number of vertices
-
- position : tuple (<float>, <float>)
- X and Y coordinates of polygon center
-
- diameter : float
- diameter of circumscribed circle.
-
- rotation : float
- polygon rotation about the origin.
-
- Returns
- -------
- PolygonPrimitive : :class:`gerber.am_statements.AMPolygonPrimitive`
- An initialized AMPolygonPrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
- return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- exposure = "on" if float(modifiers[1]) == 1 else "off"
- vertices = int(float(modifiers[2]))
- position = (float(modifiers[3]), float(modifiers[4]))
- try:
- diameter = float(modifiers[5])
- except:
- diameter = 0
-
- rotation = float(modifiers[6])
- return cls(code, exposure, vertices, position, diameter, rotation)
-
- def __init__(self, code, exposure, vertices, position, diameter, rotation):
- """ Initialize AMPolygonPrimitive
- """
- if code != 5:
- raise ValueError('PolygonPrimitive code is 5')
- super(AMPolygonPrimitive, self).__init__(code, exposure)
- if vertices < 3 or vertices > 12:
- raise ValueError('Number of vertices must be between 3 and 12')
- self.vertices = vertices
- validate_coordinates(position)
- self.position = position
- self.diameter = diameter
- self.rotation = rotation
-
- def to_inch(self):
- self.position = tuple([inch(x) for x in self.position])
- self.diameter = inch(self.diameter)
-
- def to_metric(self):
- self.position = tuple([metric(x) for x in self.position])
- self.diameter = metric(self.diameter)
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.position
- return f'{self.code},{exposure},{self.vertices},{x:.4f},{y:.4f},{self.diameter:.4f},{self.rotation}*'
-
- def to_primitive(self, units):
- return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity)
-
-
-class AMMoirePrimitive(AMPrimitive):
- """ Aperture Macro Moire primitive. Code 6.
-
- The moire primitive is a cross hair centered on concentric rings (annuli).
- Exposure is always on.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.9:** Moire, primitive code 6.
-
- Parameters
- ----------
- code : int
- Moire Primitive code. Must be 6.
-
- position : tuple (<float>, <float>)
- X and Y coordinates of moire center
-
- diameter : float
- outer diameter of outer ring.
-
- ring_thickness : float
- thickness of concentric rings.
-
- gap : float
- gap between concentric rings.
-
- max_rings : float
- maximum number of rings
-
- crosshair_thickness : float
- thickness of crosshairs
-
- crosshair_length : float
- length of crosshairs
-
- rotation : float
- moire rotation about the origin.
-
- Returns
- -------
- MoirePrimitive : :class:`gerber.am_statements.AMMoirePrimitive`
- An initialized AMMoirePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- position = (float(modifiers[1]), float(modifiers[2]))
- diameter = float(modifiers[3])
- ring_thickness = float(modifiers[4])
- gap = float(modifiers[5])
- max_rings = int(float(modifiers[6]))
- crosshair_thickness = float(modifiers[7])
- crosshair_length = float(modifiers[8])
- rotation = float(modifiers[9])
- return cls(code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation)
-
- def __init__(self, code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation):
- """ Initialize AMoirePrimitive
- """
- if code != 6:
- raise ValueError('MoirePrimitive code is 6')
- super(AMMoirePrimitive, self).__init__(code, 'on')
- validate_coordinates(position)
- self.position = position
- self.diameter = diameter
- self.ring_thickness = ring_thickness
- self.gap = gap
- self.max_rings = max_rings
- self.crosshair_thickness = crosshair_thickness
- self.crosshair_length = crosshair_length
- self.rotation = rotation
-
- def to_inch(self):
- self.position = tuple([inch(x) for x in self.position])
- self.diameter = inch(self.diameter)
- self.ring_thickness = inch(self.ring_thickness)
- self.gap = inch(self.gap)
- self.crosshair_thickness = inch(self.crosshair_thickness)
- self.crosshair_length = inch(self.crosshair_length)
-
- def to_metric(self):
- self.position = tuple([metric(x) for x in self.position])
- self.diameter = metric(self.diameter)
- self.ring_thickness = metric(self.ring_thickness)
- self.gap = metric(self.gap)
- self.crosshair_thickness = metric(self.crosshair_thickness)
- self.crosshair_length = metric(self.crosshair_length)
-
-
- def to_gerber(self, settings=None):
- x, y = self.position
- return f'{self.code},{x:.4f},{y:.4f},{self.diameter},{self.ring_thickness},{self.gap},{self.max_rings},{self.crosshair_thickness},{self.crosshair_length},{self.rotation}*'
-
- def to_primitive(self, units):
- #raise NotImplementedError()
- return None
-
-
-class AMThermalPrimitive(AMPrimitive):
- """ Aperture Macro Thermal primitive. Code 7.
-
- The thermal primitive is a ring (annulus) interrupted by four gaps.
- Exposure is always on.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.10:** Thermal, primitive code 7.
-
- Parameters
- ----------
- code : int
- Thermal Primitive code. Must be 7.
-
- position : tuple (<float>, <float>)
- X and Y coordinates of thermal center
-
- outer_diameter : float
- outer diameter of thermal.
-
- inner_diameter : float
- inner diameter of thermal.
-
- gap : float
- gap thickness
-
- rotation : float
- thermal rotation about the origin.
-
- Returns
- -------
- ThermalPrimitive : :class:`gerber.am_statements.AMThermalPrimitive`
- An initialized AMThermalPrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- position = (float(modifiers[1]), float(modifiers[2]))
- outer_diameter = float(modifiers[3])
- inner_diameter = float(modifiers[4])
- gap = float(modifiers[5])
- rotation = float(modifiers[6])
- return cls(code, position, outer_diameter, inner_diameter, gap, rotation)
-
- def __init__(self, code, position, outer_diameter, inner_diameter, gap, rotation):
- if code != 7:
- raise ValueError('ThermalPrimitive code is 7')
- super(AMThermalPrimitive, self).__init__(code, 'on')
- validate_coordinates(position)
- self.position = position
- self.outer_diameter = outer_diameter
- self.inner_diameter = inner_diameter
- self.gap = gap
- self.rotation = rotation
-
- def to_inch(self):
- self.position = tuple([inch(x) for x in self.position])
- self.outer_diameter = inch(self.outer_diameter)
- self.inner_diameter = inch(self.inner_diameter)
- self.gap = inch(self.gap)
-
- def to_metric(self):
- self.position = tuple([metric(x) for x in self.position])
- self.outer_diameter = metric(self.outer_diameter)
- self.inner_diameter = metric(self.inner_diameter)
- self.gap = metric(self.gap)
-
- def to_gerber(self, settings=None):
- x, y = self.position
- return f'{self.code},{x:.4f},{y:.4f},{self.outer_diameter},{self.inner_diameter},{self.gap},{self.rotation}*'
-
- def _approximate_arc_cw(self, start_angle, end_angle, radius, center):
- """
- Get an arc as a series of points
-
- Parameters
- ----------
- start_angle : The start angle in radians
- end_angle : The end angle in radians
- radius`: Radius of the arc
- center : The center point of the arc (x, y) tuple
-
- Returns
- -------
- array of point tuples
- """
-
- # The total sweep
- sweep_angle = end_angle - start_angle
- num_steps = 10
-
- angle_step = sweep_angle / num_steps
-
- radius = radius
- center = center
-
- points = []
-
- for i in range(num_steps + 1):
- current_angle = start_angle + (angle_step * i)
-
- nextx = (center[0] + math.cos(current_angle) * radius)
- nexty = (center[1] + math.sin(current_angle) * radius)
-
- points.append((nextx, nexty))
-
- return points
-
- def to_primitive(self, units):
-
- # We start with calculating the top right section, then duplicate it
-
- inner_radius = self.inner_diameter / 2.0
- outer_radius = self.outer_diameter / 2.0
-
- # Calculate the start angle relative to the horizontal axis
- inner_offset_angle = asin(self.gap / 2.0 / inner_radius)
- outer_offset_angle = asin(self.gap / 2.0 / outer_radius)
-
- rotation_rad = math.radians(self.rotation)
- inner_start_angle = inner_offset_angle + rotation_rad
- inner_end_angle = math.pi / 2 - inner_offset_angle + rotation_rad
-
- outer_start_angle = outer_offset_angle + rotation_rad
- outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad
-
- outlines = []
- aperture = Circle((0, 0), 0)
-
- points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position)
- + list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position))))
- # Add in the last point since outlines should be closed
- points.append(points[0])
-
- # There are four outlines at rotated sections
- for rotation in [0, 90.0, 180.0, 270.0]:
-
- lines = []
- prev_point = rotate_point(points[0], rotation, self.position)
- for point in points[1:]:
- cur_point = rotate_point(point, rotation, self.position)
-
- lines.append(Line(prev_point, cur_point, aperture))
-
- prev_point = cur_point
-
- outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity))
-
- return outlines
-
-
-class AMCenterLinePrimitive(AMPrimitive):
- """ Aperture Macro Center Line primitive. Code 21.
-
- The center line primitive is a rectangle defined by its width, height, and center point.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.4:** Center Line, primitive code 21.
-
- Parameters
- ----------
- code : int
- Center Line Primitive code. Must be 21.
-
- exposure : str
- 'on' or 'off'
-
- width : float
- Width of rectangle
-
- height : float
- Height of rectangle
-
- center : tuple (<float>, <float>)
- X and Y coordinates of line center
-
- rotation : float
- rectangle rotation about its center.
-
- Returns
- -------
- CenterLinePrimitive : :class:`gerber.am_statements.AMCenterLinePrimitive`
- An initialized AMCenterLinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
- width = primitive.width
- height = primitive.height
- center = primitive.position
- rotation = math.degrees(primitive.rotation)
- return cls(21, 'on', width, height, center, rotation)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- width = float(modifiers[2])
- height = float(modifiers[3])
- center = (float(modifiers[4]), float(modifiers[5]))
- rotation = float(modifiers[6])
- return cls(code, exposure, width, height, center, rotation)
-
- def __init__(self, code, exposure, width, height, center, rotation):
- if code != 21:
- raise ValueError('CenterLinePrimitive code is 21')
- super(AMCenterLinePrimitive, self).__init__(code, exposure)
- self.width = width
- self.height = height
- validate_coordinates(center)
- self.center = center
- self.rotation = rotation
-
- def to_inch(self):
- self.center = tuple([inch(x) for x in self.center])
- self.width = inch(self.width)
- self.height = inch(self.height)
-
- def to_metric(self):
- self.center = tuple([metric(x) for x in self.center])
- self.width = metric(self.width)
- self.height = metric(self.height)
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.center
- return f'{self.code},{exposure},{self.width},{self.height},{x:.4f},{y:.4f},{self.rotation}*'
-
- def to_primitive(self, units):
-
- x = self.center[0]
- y = self.center[1]
- half_width = self.width / 2.0
- half_height = self.height / 2.0
-
- points = []
- points.append((x - half_width, y + half_height))
- points.append((x - half_width, y - half_height))
- points.append((x + half_width, y - half_height))
- points.append((x + half_width, y + half_height))
-
- aperture = Circle((0, 0), 0)
-
- lines = []
- prev_point = rotate_point(points[3], self.rotation, self.center)
- for point in points:
- cur_point = rotate_point(point, self.rotation, self.center)
-
- lines.append(Line(prev_point, cur_point, aperture))
-
- return Outline(lines, units=units, level_polarity=self._level_polarity)
-
-
-class AMLowerLeftLinePrimitive(AMPrimitive):
- """ Aperture Macro Lower Left Line primitive. Code 22.
-
- The lower left line primitive is a rectangle defined by its width, height, and the lower left point.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.5:** Lower Left Line, primitive code 22.
-
- Parameters
- ----------
- code : int
- Center Line Primitive code. Must be 22.
-
- exposure : str
- 'on' or 'off'
-
- width : float
- Width of rectangle
-
- height : float
- Height of rectangle
-
- lower_left : tuple (<float>, <float>)
- X and Y coordinates of lower left corner
-
- rotation : float
- rectangle rotation about its origin.
-
- Returns
- -------
- LowerLeftLinePrimitive : :class:`gerber.am_statements.AMLowerLeftLinePrimitive`
- An initialized AMLowerLeftLinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- width = float(modifiers[2])
- height = float(modifiers[3])
- lower_left = (float(modifiers[4]), float(modifiers[5]))
- rotation = float(modifiers[6])
- return cls(code, exposure, width, height, lower_left, rotation)
-
- def __init__(self, code, exposure, width, height, lower_left, rotation):
- if code != 22:
- raise ValueError('LowerLeftLinePrimitive code is 22')
- super (AMLowerLeftLinePrimitive, self).__init__(code, exposure)
- self.width = width
- self.height = height
- validate_coordinates(lower_left)
- self.lower_left = lower_left
- self.rotation = rotation
-
- def to_inch(self):
- self.lower_left = tuple([inch(x) for x in self.lower_left])
- self.width = inch(self.width)
- self.height = inch(self.height)
-
- def to_metric(self):
- self.lower_left = tuple([metric(x) for x in self.lower_left])
- self.width = metric(self.width)
- self.height = metric(self.height)
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.lower_left
- return f'{self.code},{exposure},{self.width},{self.height},{x:.4f},{y:.4f},{self.rotation}*'
-
-
-class AMUnsupportPrimitive(AMPrimitive):
- @classmethod
- def from_gerber(cls, primitive):
- return cls(primitive)
-
- def __init__(self, primitive):
- super(AMUnsupportPrimitive, self).__init__(9999)
- self.primitive = primitive
-
- def to_inch(self):
- pass
-
- def to_metric(self):
- pass
-
- def to_gerber(self, settings=None):
- return self.primitive
-
diff --git a/gerbonara/gerber/aperture_macros/expression.py b/gerbonara/gerber/aperture_macros/expression.py
new file mode 100644
index 0000000..49df36c
--- /dev/null
+++ b/gerbonara/gerber/aperture_macros/expression.py
@@ -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())
diff --git a/gerbonara/gerber/aperture_macros/am_primitive.py b/gerbonara/gerber/aperture_macros/primitive.py
index 88552c5..88552c5 100644
--- a/gerbonara/gerber/aperture_macros/am_primitive.py
+++ b/gerbonara/gerber/aperture_macros/primitive.py