summaryrefslogtreecommitdiff
path: root/gerbonara
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara')
-rw-r--r--gerbonara/gerber/aperture_macros/am_expression.py244
-rw-r--r--gerbonara/gerber/aperture_macros/am_primitive.py172
-rw-r--r--gerbonara/gerber/aperture_macros/am_statements.py1010
3 files changed, 1426 insertions, 0 deletions
diff --git a/gerbonara/gerber/aperture_macros/am_expression.py b/gerbonara/gerber/aperture_macros/am_expression.py
new file mode 100644
index 0000000..809f60e
--- /dev/null
+++ b/gerbonara/gerber/aperture_macros/am_expression.py
@@ -0,0 +1,244 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2021 Jan Götte <gerbonara@jaseg.de>
+
+import operator
+import re
+
+class Expression(object):
+ @property
+ def value(self):
+ return self
+
+ def optimized(self):
+ return self
+
+
+class UnitExpression(Expression):
+ def __init__(self, expr, unit):
+ self._expr = expr
+ self.unit = unit
+
+ def to_gerber(self, unit=None):
+ return self.converted(unit).optimized().to_gerber()
+
+ def __eq__(self, other):
+ return type(other) == type(self) and \
+ self.unit == other.unit and\
+ self._expr == other._expr
+
+ def __str__(self):
+ return f'<{str(self.expr)[1:-1]} {self.unit}>'
+
+ def converted(self, unit):
+ if unit is None or self.unit == unit:
+ return self._expr
+
+ elif unit == 'mm':
+ return OperatorExpression.mul(self._expr, MILLIMETERS_PER_INCH)
+
+ elif unit == 'inch':
+ return OperatorExpression.div(self._expr, MILLIMETERS_PER_INCH)
+
+ else:
+ raise ValueError('invalid unit, must be "inch" or "mm".')
+
+ def calculate(self, variable_binding={}, unit=None):
+ expr = self.converted(unit).optimized(variable_binding)
+ if not isinstance(expr, ConstantExpression):
+ raise IndexError(f'Cannot fully resolve expression due to unresolved variables: {expr} with variables {variable_binding}')
+
+
+class ConstantExpression(Expression):
+ def __init__(self, value):
+ self._value = value
+
+ @property
+ def value(self):
+ return self._value
+
+ def __float__(self):
+ return float(self._value)
+
+ def __eq__(self, other):
+ return type(self) == type(other) and self._value == other._value
+
+ def to_gerber(self, _unit=None):
+ if isinstance(self._value, str):
+ return self._value
+ return f'{self.value:.6f}'.rstrip('0').rstrip('.')
+
+ def __str__(self):
+ return f'<{self._value}>'
+
+
+class VariableExpression(Expression):
+ def __init__(self, number):
+ self.number = number
+
+ def optimized(variable_binding={}):
+ if self.number in variable_binding:
+ return ConstantExpression(variable_binding[self.number])
+ return self
+
+ def __eq__(self, other):
+ return type(self) == type(other) and \
+ self.number == other.number
+
+ def to_gerber(self, _unit=None):
+ return f'${self.number}'
+
+ def __str__(self):
+ return f'<@{self.number}>'
+
+
+class OperatorExpression(Expression):
+ def __init__(self, op, l, r):
+ super(OperatorExpression, self).__init__(Expression.OPERATOR)
+ self.op = op
+ self.l = ConstantExpression(l) if isinstance(l, (int, float)) else l
+ self.r = ConstantExpression(r) if isinstance(r, (int, float)) else r
+
+ def __eq__(self, other):
+ return type(self) == type(other) and \
+ self.op == other.op and \
+ self.lvalue == other.lvalue and \
+ self.rvalue == other.rvalue
+
+ def optimized(self, variable_binding={}):
+ l = self.lvalue.optimized(variable_binding)
+ r = self.rvalue.optimized(variable_binding)
+
+ if self.op in (operator.add, operator.mul):
+ if hash(r) < hash(l):
+ l, r = r, l
+
+ if isinstance(l, ConstantExpression) and isinstance(r, ConstantExpression):
+ return ConstantExpression(self.op(float(r), float(l)))
+
+ return OperatorExpression(self.op, l, r)
+
+ def to_gerber(self, unit=None):
+ lval = self.lvalue.to_gerber(unit)
+ rval = self.rvalue.to_gerber(unit)
+ op = {OperatorExpression.ADD: '+',
+ OperatorExpression.SUB: '-',
+ OperatorExpression.MUL: 'x',
+ OperatorExpression.DIV: '/'} [self.op]
+ return f'({lval}{op}{rval})'
+
+ def __str__(self):
+ op = {operator.add: '+', operator.sub: '-', operator.mul: '*', operator.truediv: '/'}[self.op]
+ return f'<{str(self.lvalue)[1:-1]} {op} {str(self.rvalue)[1:-1]}>'
+
+operator_map = {
+ '+': operator.add,
+ '-': operator.sub,
+ 'x': operator.mul,
+ 'X': operator.mul,
+ '/': operator.truediv,
+ }
+
+precedence_map = {
+ operator.add : 0,
+ operator.sub : 0,
+ operator.mul : 1,
+ operator.truediv : 1,
+ }
+
+def _parse_expression(expr_str):
+ output_stack = []
+ operator_stack = []
+
+ drop_unary = lambda s: (s[0] == '-', s[1:] if s[0] in '-+' else s)
+ negate = lambda expr: OperatorExpression(operator.sub, ConstantExpression(0), expr)
+
+ # See http://faculty.cs.niu.edu/~hutchins/csci241/eval.htm
+ # We handle the unary +/- operators by including them into variable/number/parenthesis tokens.
+ for variable, number, operator, parenthesis in re.findall(r'([-+]?\$[0-9]+)|([-+]?[0-9]+)|([-+]?\(|\))|([-+xX/])', expr_str):
+
+ if variable:
+ is_negative, variable = drop_unary(variable)
+ var_ex = VariableExpression(int(variable[1:]))
+ output_stack.append(negate(var_ex) if is_negative else var_ex)
+
+
+def _parse_expression(expr_str):
+ output_stack = []
+ operator_stack = []
+
+ drop_unary = lambda s: (s[0] == '-', s[1:] if s[0] in '-+' else s)
+ negate = lambda expr: OperatorExpression(operator.sub, ConstantExpression(0), expr)
+
+ # See http://faculty.cs.niu.edu/~hutchins/csci241/eval.htm
+ # We handle the unary +/- operators by including them into variable/number/parenthesis tokens.
+ for variable, number, operator, parenthesis in re.findall(r'([-+xX/])|([-+]?\$[0-9]+)|([-+]?[0-9]+\.?[0-9]*)|([()])', expr_str):
+
+ if variable:
+ is_negative, variable = drop_unary(variable)
+ var_ex = VariableExpression(int(variable[1:]))
+ output_stack.append(negate(var_ex) if is_negative else var_ex)
+
+ elif number:
+ output_stack.append(ConstantExpression(float(number)))
+
+ elif parenthesis[-1] == '(': # be careful, we might have a leading unary +/- here!
+ is_negative, parenthesis = drop_unary(parenthesis)
+ if is_negative:
+ operator_stack.push('-')
+ operator_stack.push('(')
+
+ elif parenthesis == ')': # here we cannot have a leading unary +/-
+ if not operator_stack:
+ raise SyntaxError('Unbalanced parenthesis in aperture macro expression')
+
+ while operator_stack and not operator_stack[-1] == '(':
+ op = operator_stack.pop()
+ l, r = output_stack.pop(), output_stack.pop()
+ output_stack.append(OperatorExpression(op, l, r))
+
+ assert output_stack.pop() == '('
+ if output_stack[-1] == '-':
+ output_stack.append(negate(output_stack.pop()))
+
+ elif operator:
+ operator = operator_map[operator]
+
+ if not operator_stack or operator_stack[-1] == '(':
+ operator_stack.push(operator)
+
+ else:
+ while operator_stack and operator_stack[-1] != '(' and\
+ precedence_map[operator] <= precedence_map[operator_stack[-1]]:
+ output_stack.append(OperatorExpression(operator_stack.pop(), output_stack.pop(), output_stack.pop()))
+ operator_stack.push(operator)
+
+ for operator in reversed(operator_stack):
+ if operator == '(':
+ raise SyntaxError('Unbalanced parenthesis in aperture macro expression')
+
+ output_stack.append(OperatorExpression(operator_stack.pop(), output_stack.pop(), output_stack.pop()))
+ print(output_stack, operator_stack)
+
+ if len(output_stack) != 1:
+ raise SyntaxError('Invalid aperture macro expression')
+
+ return output_stack[0]
+
+def parse_macro(macro, unit):
+ blocks = re.sub(r'\s', '', macro).split('*')
+ variables = {}
+ for block in blocks:
+ block = block.strip()
+ if block[0] == '$': # variable definition
+ name, expr = block.partition('=')
+ variables[int(name[1:])] = _parse_expression(expr)
+ else: # primitive
+ primitive, args = block.split(',')
+ yield PRIMITIVE_CLASSES[int(primitive)](unit=unit, args=list(map(_parse_expression, args)))
+
+if __name__ == '__main__':
+ import sys
+ for line in sys.stdin:
+ print(_parse_expression(line.strip()))
diff --git a/gerbonara/gerber/aperture_macros/am_primitive.py b/gerbonara/gerber/aperture_macros/am_primitive.py
new file mode 100644
index 0000000..88552c5
--- /dev/null
+++ b/gerbonara/gerber/aperture_macros/am_primitive.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+from dataclasses import dataclass, fields
+
+from .utils import *
+from .am_statements import *
+from .am_expression import *
+from .am_opcode import OpCode
+
+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()):
+ if fieldtype == UnitExpression:
+ setattr(self, name, UnitExpression(arg, unit))
+ else:
+ setattr(self, name, arg)
+
+ for name, _type in type(self).__annotations__.items():
+ 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 self.code + ',' + ','.join(
+ getattr(self, name).to_gerber(unit) for name, _type in type(self).__annotations__.items()) + '*'
+
+class CommentPrimitive(Primitive):
+ code = 0
+ comment : str
+
+class CirclePrimitive(Primitive):
+ code = 1
+ exposure : Expression
+ diameter : UnitExpression
+ center_x : UnitExpression
+ center_y : UnitExpression
+ rotation : Expression = ConstantExpression(0.0)
+
+class VectorLinePrimitive(Primitive):
+ code = 20
+ exposure : Expression
+ width : UnitExpression
+ start_x : UnitExpression
+ start_y : UnitExpression
+ end_x : UnitExpression
+ end_y : UnitExpression
+ rotation : Expression
+
+class CenterLinePrimitive(Primitive):
+ code = 21
+ exposure : Expression
+ width : UnitExpression
+ height : UnitExpression
+ x : UnitExpression
+ y : UnitExpression
+ rotation : Expression
+
+
+class PolygonPrimitive(Primitive):
+ code = 5
+ exposure : Expression
+ n_vertices : Expression
+ center_x : UnitExpression
+ center_y : UnitExpression
+ diameter : UnitExpression
+ rotation : Expression
+
+
+class ThermalPrimitive(Primitive):
+ code = 7
+ center_x : UnitExpression
+ center_y : UnitExpression
+ d_outer : UnitExpression
+ d_inner : UnitExpression
+ gap_w : UnitExpression
+ rotation : Expression
+
+
+class OutlinePrimitive(Primitive):
+ code = 4
+
+ def __init__(self, code, 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[0]
+
+ if args[1] != len(args)//2 - 2:
+ raise ValueError(f'Invalid aperture macro outline primitive, given size does not match length of coordinate list({len(args)}).')
+
+ if len(args) % 1 != 1:
+ self.rotation = args.pop()
+ else:
+ self.rotation = ConstantExpression(0.0)
+
+ if args[2] != args[-2] or args[3] != args[-1]:
+ raise ValueError(f'Invalid aperture macro outline primitive, polygon is not closed {args[2:4], args[-3:-1]}')
+
+ self.coords = [UnitExpression(arg, unit) for arg in args[1:]]
+
+ def to_gerber(self, unit=None):
+ coords = ','.join(coord.to_gerber(unit) for coord in self.coords)
+ return f'{self.code},{self.exposure.to_gerber()},{len(self.coords)//2-1},{coords},{self.rotation.to_gerber()}'
+
+
+class VariableDef(object):
+ def __init__(self, number, value):
+ self.number = number
+ self.value = value
+
+ def to_gerber(self, _unit=None):
+ return '$%d=%s*' % (self.number, self.value.to_gerber(settings))
+
+PRIMITIVE_CLASSES = {
+ **{cls.code: cls for cls in [
+ CommentPrimitive,
+ CirclePrimitive,
+ VectorLinePrimitive,
+ CenterLinePrimitive,
+ OutlinePrimitive,
+ PolygonPrimitive,
+ ThermalPrimitive,
+ ],
+ # alternative codes
+ 2: VectorLinePrimitive,
+}
+
+def eval_macro(instructions, unit):
+ stack = []
+ for opcode, argument in instructions:
+ if opcode == OpCode.PUSH:
+ stack.append(ConstantExpression(argument))
+
+ elif opcode == OpCode.LOAD:
+ stack.append(VariableExpression(argument))
+
+ elif opcode == OpCode.STORE:
+ yield VariableDef(code, stack.pop())
+
+ elif opcode == OpCode.ADD:
+ op1 = stack.pop()
+ op2 = stack.pop()
+ stack.append(OperatorExpression(OperatorExpression.ADD, op2, op1))
+
+ elif opcode == OpCode.SUB:
+ op1 = stack.pop()
+ op2 = stack.pop()
+ stack.append(OperatorExpression(OperatorExpression.SUB, op2, op1))
+
+ elif opcode == OpCode.MUL:
+ op1 = stack.pop()
+ op2 = stack.pop()
+ stack.append(OperatorExpression(OperatorExpression.MUL, op2, op1))
+
+ elif opcode == OpCode.DIV:
+ op1 = stack.pop()
+ op2 = stack.pop()
+ stack.append(OperatorExpression(OperatorExpression.DIV, op2, op1))
+
+ elif opcode == OpCode.PRIM:
+ yield PRIMITIVE_CLASSES[argument](unit=unit, args=stack)
+ stack = []
+
diff --git a/gerbonara/gerber/aperture_macros/am_statements.py b/gerbonara/gerber/aperture_macros/am_statements.py
new file mode 100644
index 0000000..61ddf42
--- /dev/null
+++ b/gerbonara/gerber/aperture_macros/am_statements.py
@@ -0,0 +1,1010 @@
+#!/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
+