From 4eb0e063bcd34c21b737023aa6ed5baed80658d1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 13 Jun 2021 15:00:17 +0200 Subject: Repo re-org, make gerberex tests run --- gerbonara/gerber/am_statements.py | 1046 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1046 insertions(+) create mode 100644 gerbonara/gerber/am_statements.py (limited to 'gerbonara/gerber/am_statements.py') diff --git a/gerbonara/gerber/am_statements.py b/gerbonara/gerber/am_statements.py new file mode 100644 index 0000000..31c0ae4 --- /dev/null +++ b/gerbonara/gerber/am_statements.py @@ -0,0 +1,1046 @@ +#!/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 + + + +# 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): + VALID_CODES = (0, 1, 2, 4, 5, 6, 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 + + def to_inch(self): + raise NotImplementedError('Subclass must implement `to-inch`') + + def to_metric(self): + raise NotImplementedError('Subclass must implement `to-metric`') + + @property + def _level_polarity(self): + if self.exposure == 'off': + return 'clear' + return 'dark' + + def to_primitive(self, units): + """ Return a Primitive instance based on the specified macro params. + """ + print('Rendering {}s is not supported yet.'.format(str(self.__class__))) + + 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(AMCommentPrimitive, self).__init__(code) + self.comment = comment.strip(' *') + + def to_inch(self): + pass + + def to_metric(self): + pass + + def to_gerber(self, settings=None): + return '0 %s *' % self.comment + + def to_primitive(self, units): + """ + Returns None - has not primitive representation + """ + return None + + 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): + data = dict(code=self.code, + exposure='1' if self.exposure == 'on' else 0, + diameter=self.diameter, + x=self.position[0], + y=self.position[1]) + return '{code},{exposure},{diameter},{x},{y}*'.format(**data) + + 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): + fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*' + data = dict(code=self.code, + exp=1 if self.exposure == 'on' else 0, + width=self.width, + startx=self.start[0], + starty=self.start[1], + endx=self.end[0], + endy=self.end[1], + rotation=self.rotation) + return fmtstr.format(**data) + + 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): + data = dict( + code=self.code, + exposure="1" if self.exposure == "on" else "0", + n_points=len(self.points), + start_point="%.6g,%.6g" % self.start_point, + points=",\n".join(["%.6g,%.6g" % point for point in self.points]), + rotation=str(self.rotation) + ) + return "{code},{exposure},{n_points},{start_point},{points},{rotation}*".format(**data) + + 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): + data = dict( + code=self.code, + exposure="1" if self.exposure == "on" else "0", + vertices=self.vertices, + position="%.4g,%.4g" % self.position, + diameter='%.4g' % self.diameter, + rotation=str(self.rotation) + ) + fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*" + return fmt.format(**data) + + 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): + data = dict( + code=self.code, + position="%.4g,%.4g" % self.position, + diameter=self.diameter, + ring_thickness=self.ring_thickness, + gap=self.gap, + max_rings=self.max_rings, + crosshair_thickness=self.crosshair_thickness, + crosshair_length=self.crosshair_length, + rotation=self.rotation + ) + fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*" + return fmt.format(**data) + + 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): + data = dict( + code=self.code, + position="%.4g,%.4g" % self.position, + outer_diameter=self.outer_diameter, + inner_diameter=self.inner_diameter, + gap=self.gap, + rotation=self.rotation + ) + fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap},{rotation}*" + return fmt.format(**data) + + 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): + data = dict( + code=self.code, + exposure = '1' if self.exposure == 'on' else '0', + width = self.width, + height = self.height, + center="%.4g,%.4g" % self.center, + rotation=self.rotation + ) + fmt = "{code},{exposure},{width},{height},{center},{rotation}*" + return fmt.format(**data) + + 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): + data = dict( + code=self.code, + exposure = '1' if self.exposure == 'on' else '0', + width = self.width, + height = self.height, + lower_left="%.4g,%.4g" % self.lower_left, + rotation=self.rotation + ) + fmt = "{code},{exposure},{width},{height},{lower_left},{rotation}*" + return fmt.format(**data) + + +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