#!/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 .utils import validate_coordinates # TODO: Add support for aperture macro variables __all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive', 'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive', 'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive', 'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive'] def metric(value): return value * 25.4 def inch(value): return value / 25.4 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) 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`') 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 __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 modifiers[1].strip() == '1' else 'off' diameter = float(modifiers[2]) position = (float(modifiers[3]), float(modifiers[4])) return cls(code, exposure, diameter, 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) 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_gerber(cls, primitive): modifiers = primitive.strip(' *').split(',') code = int(modifiers[0]) exposure = 'on' if modifiers[1].strip() == '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) 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_gerber(cls, primitive): modifiers = primitive.strip(' *').split(",") code = int(modifiers[0]) exposure = "on" if modifiers[1].strip() == "1" else "off" n = int(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="%.4g,%.4g" % self.start_point, points=",".join(["%.4g,%.4g" % point for point in self.points]), rotation=str(self.rotation) ) return "{code},{exposure},{n_points},{start_point},{points},{rotation}*".format(**data) 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_gerber(cls, primitive): modifiers = primitive.strip(' *').split(",") code = int(modifiers[0]) exposure = "on" if modifiers[1].strip() == "1" else "off" vertices = int(modifiers[2]) position = (float(modifiers[3]), float(modifiers[4])) diameter = float(modifiers[5]) 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) 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(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) 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]) return cls(code, position, outer_diameter, inner_diameter, gap) def __init__(self, code, position, outer_diameter, inner_diameter, gap): 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 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, ) fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap}*" return fmt.format(**data) 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_gerber(cls, primitive): modifiers = primitive.strip(' *').split(",") code = int(modifiers[0]) exposure = 'on' if modifiers[1].strip() == '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) 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 modifiers[1].strip() == '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): self.primitive = primitive def to_inch(self): pass def to_metric(self): pass def to_gerber(self, settings=None): return self.primitive