summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/aperture_macros/am_statements.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber/aperture_macros/am_statements.py')
-rw-r--r--gerbonara/gerber/aperture_macros/am_statements.py1010
1 files changed, 1010 insertions, 0 deletions
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
+