From f833483b72cd4fdf56cd4130dc9174ebfac8673d Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 11 Nov 2021 11:46:08 +0100 Subject: WIP --- gerbonara/gerber/aperture_macros/am_expression.py | 244 +++++ gerbonara/gerber/aperture_macros/am_primitive.py | 172 ++++ gerbonara/gerber/aperture_macros/am_statements.py | 1010 +++++++++++++++++++++ 3 files changed, 1426 insertions(+) create mode 100644 gerbonara/gerber/aperture_macros/am_expression.py create mode 100644 gerbonara/gerber/aperture_macros/am_primitive.py create mode 100644 gerbonara/gerber/aperture_macros/am_statements.py (limited to 'gerbonara/gerber') 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 + +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 + +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 and Paulo Henrique Silva +# + +# 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 `_ + **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 '' % 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 `_ + **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 (, ) + 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 `_ + **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 (, ) + coordinate of line start point + + end : tuple (, ) + 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 `_ + **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 (, ) + coordinate of outline start point + + points : list of tuples (, ) + 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 `_ + **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 (, ) + 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 `_ + **Section 4.12.3.9:** Moire, primitive code 6. + + Parameters + ---------- + code : int + Moire Primitive code. Must be 6. + + position : tuple (, ) + 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 `_ + **Section 4.12.3.10:** Thermal, primitive code 7. + + Parameters + ---------- + code : int + Thermal Primitive code. Must be 7. + + position : tuple (, ) + 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 `_ + **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 (, ) + 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 `_ + **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 (, ) + 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 + -- cgit