diff options
-rw-r--r-- | gerbonara/gerber/aperture_macros/am_expression.py | 244 | ||||
-rw-r--r-- | gerbonara/gerber/aperture_macros/am_statements.py | 1010 | ||||
-rw-r--r-- | gerbonara/gerber/aperture_macros/expression.py | 185 | ||||
-rw-r--r-- | gerbonara/gerber/aperture_macros/primitive.py (renamed from gerbonara/gerber/aperture_macros/am_primitive.py) | 0 | ||||
-rw-r--r-- | gerbonara/gerber/apertures.py | 85 |
5 files changed, 270 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 diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py new file mode 100644 index 0000000..aa2764e --- /dev/null +++ b/gerbonara/gerber/apertures.py @@ -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) + + |