From d21a2e67ff34d3f29e37a01f926b9e8f72003637 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 10 Nov 2021 21:39:03 +0100 Subject: WIP --- gerbonara/gerber/am_read.py | 255 --------- gerbonara/gerber/am_statements.py | 1010 --------------------------------- gerbonara/gerber/cam.py | 3 + gerbonara/gerber/gerber_statements.py | 219 ++----- gerbonara/gerber/pcb.py | 3 +- gerbonara/gerber/primitives.py | 332 +---------- gerbonara/gerber/rs274x.py | 274 +++++---- gerbonara/gerber/utils.py | 145 +---- 8 files changed, 207 insertions(+), 2034 deletions(-) delete mode 100644 gerbonara/gerber/am_read.py delete mode 100644 gerbonara/gerber/am_statements.py diff --git a/gerbonara/gerber/am_read.py b/gerbonara/gerber/am_read.py deleted file mode 100644 index c50bd01..0000000 --- a/gerbonara/gerber/am_read.py +++ /dev/null @@ -1,255 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe -# copyright 2014 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. -""" This module provides RS-274-X AM macro modifiers parsing. -""" - -from .am_opcode import OpCode - -import string - -class Token: - ADD = "+" - SUB = "-" - # compatibility as many gerber writes do use non compliant X - MULT = ("x", "X") - DIV = "/" - OPERATORS = (ADD, SUB, *MULT, DIV) - LEFT_PARENS = "(" - RIGHT_PARENS = ")" - EQUALS = "=" - EOF = "EOF" - - -def token_to_opcode(token): - if token == Token.ADD: - return OpCode.ADD - elif token == Token.SUB: - return OpCode.SUB - elif token in Token.MULT: - return OpCode.MUL - elif token == Token.DIV: - return OpCode.DIV - else: - return None - - -def precedence(token): - if token == Token.ADD or token == Token.SUB: - return 1 - elif token in Token.MULT or token == Token.DIV: - return 2 - else: - return 0 - - -def is_op(token): - return token in Token.OPERATORS - - -class Scanner: - - def __init__(self, s): - self.buff = s - self.n = 0 - - def eof(self): - return self.n == len(self.buff) - - def peek(self): - if not self.eof(): - return self.buff[self.n] - - return Token.EOF - - def ungetc(self): - if self.n > 0: - self.n -= 1 - - def getc(self): - if self.eof(): - return "" - - c = self.buff[self.n] - self.n += 1 - return c - - def readint(self): - n = "" - while not self.eof() and (self.peek() in string.digits): - n += self.getc() - return int(n) - - def readfloat(self): - n = "" - while not self.eof() and (self.peek() in string.digits or self.peek() == "."): - n += self.getc() - # weird case where zero is ommited inthe last modifider, like in ',0.' - if n == ".": - return 0 - return float(n) - - def readstr(self, end="*"): - s = "" - while not self.eof() and self.peek() != end: - s += self.getc() - return s.strip() - - -def print_instructions(instructions): - for opcode, argument in instructions: - print("%s %s" % (OpCode.str(opcode), - str(argument) if argument is not None else "")) - - -def read_macro(macro): - instructions = [] - - for block in macro.split("*"): - - is_primitive = False - is_equation = False - - found_equation_left_side = False - found_primitive_code = False - - equation_left_side = 0 - primitive_code = 0 - - unary_minus_allowed = False - unary_minus = False - - if Token.EQUALS in block: - is_equation = True - else: - is_primitive = True - - scanner = Scanner(block) - - # inlined here for compactness and convenience - op_stack = [] - - def pop(): - return op_stack.pop() - - def push(op): - op_stack.append(op) - - def top(): - return op_stack[-1] - - def empty(): - return len(op_stack) == 0 - - while not scanner.eof(): - - c = scanner.getc() - - if c == ",": - found_primitive_code = True - - # add all instructions on the stack to finish last modifier - while not empty(): - instructions.append((token_to_opcode(pop()), None)) - - unary_minus_allowed = True - - elif c in Token.OPERATORS: - if c == Token.SUB and unary_minus_allowed: - unary_minus = True - unary_minus_allowed = False - continue - - while not empty() and is_op(top()) and precedence(top()) >= precedence(c): - instructions.append((token_to_opcode(pop()), None)) - - push(c) - - elif c == Token.LEFT_PARENS: - push(c) - - elif c == Token.RIGHT_PARENS: - while not empty() and top() != Token.LEFT_PARENS: - instructions.append((token_to_opcode(pop()), None)) - - if empty(): - raise ValueError("unbalanced parentheses") - - # discard "(" - pop() - - elif c.startswith("$"): - n = scanner.readint() - - if is_equation and not found_equation_left_side: - equation_left_side = n - else: - instructions.append((OpCode.LOAD, n)) - - elif c == Token.EQUALS: - found_equation_left_side = True - - elif c == "0": - if is_primitive and not found_primitive_code: - instructions.append((OpCode.PUSH, scanner.readstr("*"))) - found_primitive_code = True - else: - # decimal or integer disambiguation - if scanner.peek() not in '.' or scanner.peek() == Token.EOF: - instructions.append((OpCode.PUSH, 0)) - - elif c in "123456789.": - scanner.ungetc() - - if is_primitive and not found_primitive_code: - primitive_code = scanner.readint() - else: - n = scanner.readfloat() - if unary_minus: - unary_minus = False - n *= -1 - - instructions.append((OpCode.PUSH, n)) - else: - # whitespace or unknown char - pass - - # add all instructions on the stack to finish last modifier (if any) - while not empty(): - instructions.append((token_to_opcode(pop()), None)) - - # at end, we either have a primitive or a equation - if is_primitive and found_primitive_code: - instructions.append((OpCode.PRIM, primitive_code)) - - if is_equation: - instructions.append((OpCode.STORE, equation_left_side)) - - return instructions - -if __name__ == '__main__': - import sys - - instructions = read_macro(sys.argv[1]) - - print("insructions:") - print_instructions(instructions) - - print("eval:") - from .am_primitive import eval_macro - for primitive in eval_macro(instructions, 'mm'): - print(primitive) diff --git a/gerbonara/gerber/am_statements.py b/gerbonara/gerber/am_statements.py deleted file mode 100644 index 61ddf42..0000000 --- a/gerbonara/gerber/am_statements.py +++ /dev/null @@ -1,1010 +0,0 @@ -#!/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 - diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py index 5da8600..7d68ae2 100644 --- a/gerbonara/gerber/cam.py +++ b/gerbonara/gerber/cam.py @@ -24,6 +24,7 @@ class FileSettings: image_polarity : str = 'positive' image_rotation: int = 0 mirror_image : tuple = (False, False) + offset : tuple = (0, 0) scale_factor : tuple = (1.0, 1.0) # For deprecated SF statement notation : str = 'absolute' units : str = 'inch' @@ -41,6 +42,8 @@ class FileSettings: raise ValueError('image_polarity must be either "positive" or "negative"') elif name == 'mirror_image' and len(value) != 2: raise ValueError('mirror_image must be 2-tuple of bools: (mirror_a, mirror_b)') + elif name == 'offset' and len(value) != 2: + raise ValueError('offset must be 2-tuple of floats: (offset_a, offset_b)') elif name == 'scale_factor' and len(value) != 2: raise ValueError('scale_factor must be 2-tuple of floats: (scale_a, scale_b)') elif name == 'notation' and value not in ['inch', 'mm']: diff --git a/gerbonara/gerber/gerber_statements.py b/gerbonara/gerber/gerber_statements.py index d2f67c1..6faf15e 100644 --- a/gerbonara/gerber/gerber_statements.py +++ b/gerbonara/gerber/gerber_statements.py @@ -30,48 +30,23 @@ from .primitives import AMGroup class Statement: - """ Gerber statement Base class - - The statement class provides a type attribute. - - Parameters - ---------- - type : string - String identifying the statement type. - - Attributes - ---------- - type : string - String identifying the statement type. - """ - - def __str__(self): - s = "<{0} ".format(self.__class__.__name__) - - for key, value in self.__dict__.items(): - s += "{0}={1} ".format(key, value) - - s = s.rstrip() + ">" - return s + pass - def offset(self, x_offset=0, y_offset=0): + def update_graphics_state(self, _state): pass - def __eq__(self, other): - return self.__dict__ == other.__dict__ - + def render_primitives(self, _state): + pass class ParamStmt(Statement): pass class FormatSpecStmt(ParamStmt): """ FS - Gerber Format Specification Statement """ - code = 'FS' def to_gerber(self, settings): zeros = 'L' if settings.zero_suppression == 'leading' else 'T' notation = 'A' if settings.notation == 'absolute' else 'I' - fmt = settings.number_format number_format = str(settings.number_format[0]) + str(settings.number_format[1]) return f'%FS{zeros}{notation}X{number_format}Y{number_format}*%' @@ -104,8 +79,11 @@ class LoadPolarityStmt(ParamStmt): lp = 'dark' if self.dark else 'clear' return f'' + def update_graphics_state(self, state): + state.polarity_dark = self.dark + -class ADParamStmt(ParamStmt): +class ApertureDefStmt(ParamStmt): """ AD - Aperture Definition Statement """ @classmethod @@ -306,15 +284,6 @@ class AMParamStmt(ParamStmt): return '' % (self.name, self.macro) -class AxisSelectionStmt(ParamStmt): - """ AS - Axis Selection Statement. (Deprecated) """ - - def to_gerber(self, settings): - return f'%AS{settings.output_axes}*%' - - def __str__(self): - return '' - class ImagePolarityStmt(ParamStmt): """ IP - Image Polarity Statement. (Deprecated) """ @@ -326,157 +295,49 @@ class ImagePolarityStmt(ParamStmt): return '' -class ImageRotationStmt(ParamStmt): - """ IR - Image Rotation Statement. (Deprecated) """ - - def to_gerber(self, settings): - return f'%IR{settings.image_rotation}*%' - - def __str__(self): - return '' - -class MirrorImageStmt(ParamStmt): - """ MI - Mirror Image Statement. (Deprecated) """ - - def to_gerber(self, settings): - return f'%SFA{int(bool(settings.mirror_image[0]))}B{int(bool(settings.mirror_image[1]))}*%' - - def __str__(self): - return '' - -class OffsetStmt(ParamStmt): - """ OF - File Offset Statement. (Deprecated) """ - - def __init__(self, a, b): - self.a, self.b = a, b - - def to_gerber(self, settings=None): - # FIXME unit conversion - return f'%OFA{decimal_string(self.a, precision=5)}B{decimal_string(self.b, precision=5)}*%' - - def __str__(self): - return f'' - - -class SFParamStmt(ParamStmt): - """ SF - Scale Factor Statement. (Deprecated) """ - - def __init__(self, a, b): - self.a, self.b = a, b - - def to_gerber(self, settings=None): - return '%SFA{decimal_string(self.a, precision=5)}B{decimal_string(self.b, precision=5)}*%' - - def __str__(self): - return '' - class CoordStmt(Statement): """ D01 - D03 operation statements """ def __init__(self, x, y, i, j): - self.x = x - self.y = y - self.i = i - self.j = j - - @classmethod - def move(cls, func, point): - if point: - return cls(func, point[0], point[1], None, None, CoordStmt.OP_MOVE, None) - # No point specified, so just write the function. This is normally for ending a region (D02*) - return cls(func, None, None, None, None, CoordStmt.OP_MOVE, None) - - @classmethod - def line(cls, func, point): - return cls(func, point[0], point[1], None, None, CoordStmt.OP_DRAW, None) - - @classmethod - def mode(cls, func): - return cls(func, None, None, None, None, None, None) - - @classmethod - def arc(cls, func, point, center): - return cls(func, point[0], point[1], center[0], center[1], CoordStmt.OP_DRAW, None) - - @classmethod - def flash(cls, point): - if point: - return cls(None, point[0], point[1], None, None, CoordStmt.OP_FLASH, None) - else: - return cls(None, None, None, None, None, CoordStmt.OP_FLASH, None) + self.x, self.y, self.i, self.j = x, y, i, j def to_gerber(self, settings=None): ret = '' + for var in 'xyij': + val = getattr(self, var) + if val is not None: + ret += var.upper() + write_gerber_value(val, settings.number_format, settings.zero_suppression) + return ret + self.code + '*' + + def offset(self, x=0, y=0): if self.x is not None: - ret += 'X{0}'.format(write_gerber_value(self.x, settings.format, settings.zero_suppression)) - if self.y is not None: - ret += 'Y{0}'.format(write_gerber_value(self.y, settings.format, settings.zero_suppression)) - if self.i is not None: - ret += 'I{0}'.format(write_gerber_value(self.i, settings.format, settings.zero_suppression)) - if self.j is not None: - ret += 'J{0}'.format(write_gerber_value(self.j, settings.format, settings.zero_suppression)) - if self.op: - ret += self.op - return ret + '*' - - def offset(self, x_offset=0, y_offset=0): - if self.x is not None: - self.x += x_offset + self.x += x if self.y is not None: - self.y += y_offset - if self.i is not None: - self.i += x_offset - if self.j is not None: - self.j += y_offset + self.y += y def __str__(self): - coord_str = '' - if self.function: - coord_str += 'Fn: %s ' % self.function - if self.x is not None: - coord_str += 'X: %g ' % self.x - if self.y is not None: - coord_str += 'Y: %g ' % self.y - if self.i is not None: - coord_str += 'I: %g ' % self.i - if self.j is not None: - coord_str += 'J: %g ' % self.j - if self.op: - if self.op == 'D01': - op = 'Lights On' - elif self.op == 'D02': - op = 'Lights Off' - elif self.op == 'D03': - op = 'Flash' - else: - op = self.op - coord_str += 'Op: %s' % op - - return '' % coord_str - - @property - def only_function(self): - """ - Returns if the statement only set the function. - """ + if self.i is None: + return f'<{self.__name__.strip()} x={self.x} y={self.y}>' + else + return f'<{self.__name__.strip()} x={self.x} y={self.y} i={self.i} j={self.j]>' - # TODO I would like to refactor this so that the function is handled separately and then - # TODO this isn't required - return self.function != None and self.op == None and self.x == None and self.y == None and self.i == None and self.j == None + def render_primitives(self, state): + if state.interpolation_mode == InterpolateStmt: + yield Line(state.current_point, (self.x, self.y)) -class InterpolateStmt(CoordStmt): - """ D01 interpolation operation """ +class InterpolateStmt(Statement): + """ D01 Interpolation """ code = 'D01' class MoveStmt(CoordStmt): - """ D02 move operation """ + """ D02 Move """ code = 'D02' class FlashStmt(CoordStmt): - """ D03 flash operation """ + """ D03 Flash """ code = 'D03' -class InterpolationStmt(Statement): +class InterpolationModeStmt(Statement): """ G01 / G02 / G03 interpolation mode statement """ def to_gerber(self, **_kwargs): return self.code + '*' @@ -484,34 +345,34 @@ class InterpolationStmt(Statement): def __str__(self): return f'<{self.__doc__.strip()}>' -class LinearModeStmt(InterpolationStmt): +class LinearModeStmt(InterpolationModeStmt): """ G01 linear interpolation mode statement """ code = 'G01' -class CircularCWModeStmt(InterpolationStmt): +class CircularCWModeStmt(InterpolationModeStmt): """ G02 circular interpolation mode statement """ code = 'G02' -class CircularCCWModeStmt(InterpolationStmt): +class CircularCCWModeStmt(InterpolationModeStmt): """ G03 circular interpolation mode statement """ code = 'G03' -class SingleQuadrantModeStmt(InterpolationStmt): +class SingleQuadrantModeStmt(InterpolationModeStmt): """ G75 single-quadrant arc interpolation mode statement """ code = 'G75' -class MultiQuadrantModeStmt(InterpolationStmt): - """ G74 multi-quadrant arc interpolation mode statement """ - code = 'G74' - -class RegionStartStatement(InterpolationStmt): +class RegionStartStatement(InterpolationModeStmt): """ G36 Region Mode Start Statement. """ code = 'G36' -class RegionEndStatement(InterpolationStmt): +class RegionEndStatement(InterpolationModeStmt): """ G37 Region Mode End Statement. """ code = 'G37' +class RegionGroup: + def __init__(self): + self.outline = [] + class ApertureStmt(Statement): def __init__(self, d): self.d = int(d) diff --git a/gerbonara/gerber/pcb.py b/gerbonara/gerber/pcb.py index f00c4b7..8b11cf5 100644 --- a/gerbonara/gerber/pcb.py +++ b/gerbonara/gerber/pcb.py @@ -20,7 +20,6 @@ import os from .exceptions import ParseError from .layers import PCBLayer, sort_layers, layer_signatures from .common import read as gerber_read -from .utils import listdir class PCB(object): @@ -36,7 +35,7 @@ class PCB(object): raise TypeError('{} is not a directory.'.format(directory)) # Load gerber files - for filename in listdir(directory, True, True): + for filename in os.listdir(directory): try: camfile = gerber_read(os.path.join(directory, filename)) layer = PCBLayer.from_cam(camfile) diff --git a/gerbonara/gerber/primitives.py b/gerbonara/gerber/primitives.py index 445b605..25f8e06 100644 --- a/gerbonara/gerber/primitives.py +++ b/gerbonara/gerber/primitives.py @@ -24,239 +24,44 @@ from .utils import rotate_point, nearly_equal - -class Primitive(object): - """ Base class for all Cam file primitives - - Parameters - --------- - level_polarity : string - Polarity of the parameter. May be 'dark' or 'clear'. Dark indicates - a "positive" primitive, i.e. indicating where coppper should remain, - and clear indicates a negative primitive, such as where copper should - be removed. clear primitives are often used to create cutouts in region - pours. - - rotation : float - Rotation of a primitive about its origin in degrees. Positive rotation - is counter-clockwise as viewed from the board top. - - units : string - Units in which primitive was defined. 'inch' or 'metric' - - net_name : string - Name of the electrical net the primitive belongs to - """ - - def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None): - self.level_polarity = level_polarity - self.net_name = net_name - self._to_convert = list() - self._memoized = list() - self._units = units - self._rotation = rotation - self._cos_theta = math.cos(math.radians(rotation)) - self._sin_theta = math.sin(math.radians(rotation)) - self._bounding_box = None - self._vertices = None - self._segments = None - - @property - def flashed(self): - '''Is this a flashed primitive''' - raise NotImplementedError('Is flashed must be ' - 'implemented in subclass') +class Primitive: + def __init__(self, polarity_dark=True, rotation=0, **meta): + self.polarity_dark = polarity_dark + self.meta = meta + self.rotation = rotation def __eq__(self, other): return self.__dict__ == other.__dict__ - @property - def units(self): - return self._units - - @units.setter - def units(self, value): - self._changed() - self._units = value - - @property - def rotation(self): - return self._rotation - - @rotation.setter - def rotation(self, value): - self._changed() - self._rotation = value - self._cos_theta = math.cos(math.radians(value)) - self._sin_theta = math.sin(math.radians(value)) - - @property - def vertices(self): + def aperture(self): return None - @property - def segments(self): - if self._segments is None: - if self.vertices is not None and len(self.vertices): - self._segments = [segment for segment in - combinations(self.vertices, 2)] - return self._segments - - @property - def bounding_box(self): - """ Calculate axis-aligned bounding box - - will be helpful for sweep & prune during DRC clearance checks. - - Return ((min x, max x), (min y, max y)) - """ - raise NotImplementedError('Bounding box calculation must be ' - 'implemented in subclass') - - @property - def bounding_box_no_aperture(self): - """ Calculate bouxing box without considering the aperture - - for most objects, this is the same as the bounding_box, but is different for - Lines and Arcs (which are not flashed) - - Return ((min x, min y), (max x, max y)) - """ - return self.bounding_box - - def to_inch(self): - """ Convert primitive units to inches. - """ - if self.units == 'metric': - self.units = 'inch' - for attr, value in [(attr, getattr(self, attr)) - for attr in self._to_convert]: - if hasattr(value, 'to_inch'): - value.to_inch() - else: - try: - if len(value) > 1: - if hasattr(value[0], 'to_inch'): - for v in value: - v.to_inch() - elif isinstance(value[0], tuple): - setattr(self, attr, - [tuple(map(inch, point)) - for point in value]) - else: - setattr(self, attr, tuple(map(inch, value))) - except: - if value is not None: - setattr(self, attr, inch(value)) - - def to_metric(self): - """ Convert primitive units to metric. - """ - if self.units == 'inch': - self.units = 'metric' - for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]: - if hasattr(value, 'to_metric'): - value.to_metric() - else: - try: - if len(value) > 1: - if hasattr(value[0], 'to_metric'): - for v in value: - v.to_metric() - elif isinstance(value[0], tuple): - setattr(self, attr, - [tuple(map(metric, point)) - for point in value]) - else: - setattr(self, attr, tuple(map(metric, value))) - except: - if value is not None: - setattr(self, attr, metric(value)) - - def offset(self, x_offset=0, y_offset=0): - """ Move the primitive by the specified x and y offset amount. - - values are specified in the primitive's native units - """ - if hasattr(self, 'position'): - self._changed() - self.position = tuple([coord + offset for coord, offset - in zip(self.position, - (x_offset, y_offset))]) - - def to_statement(self): - pass - - def _changed(self): - """ Clear memoized properties. - - Forces a recalculation next time any memoized propery is queried. - This must be called from a subclass every time a parameter that affects - a memoized property is changed. The easiest way to do this is to call - _changed() from property.setter methods. - """ - self._bounding_box = None - self._vertices = None - self._segments = None - for attr in self._memoized: - setattr(self, attr, None) class Line(Primitive): - """ - """ - - def __init__(self, start, end, aperture, level_polarity=None, **kwargs): - super(Line, self).__init__(**kwargs) - self.level_polarity = level_polarity - self._start = start - self._end = end + def __init__(self, start, end, aperture, polarity_dark=True, rotation=0, **meta): + super().__init__(polarity_dark, rotation, **meta) + self.start = start + self.end = end self.aperture = aperture - self._to_convert = ['start', 'end', 'aperture'] - - @property - def flashed(self): - return False - - @property - def start(self): - return self._start - - @start.setter - def start(self, value): - self._changed() - self._start = value - - @property - def end(self): - return self._end - - @end.setter - def end(self, value): - self._changed() - self._end = value @property def angle(self): - delta_x, delta_y = tuple( - [end - start for end, start in zip(self.end, self.start)]) - angle = math.atan2(delta_y, delta_x) - return angle + delta_x, delta_y = tuple(end - start for end, start in zip(self.end, self.start)) + return math.atan2(delta_y, delta_x) @property def bounding_box(self): - if self._bounding_box is None: - if isinstance(self.aperture, Circle): - width_2 = self.aperture.radius - height_2 = width_2 - else: - width_2 = self.aperture.width / 2. - height_2 = self.aperture.height / 2. - min_x = min(self.start[0], self.end[0]) - width_2 - max_x = max(self.start[0], self.end[0]) + width_2 - min_y = min(self.start[1], self.end[1]) - height_2 - max_y = max(self.start[1], self.end[1]) + height_2 - self._bounding_box = ((min_x, min_y), (max_x, max_y)) - return self._bounding_box + if isinstance(self.aperture, Circle): + width_2 = self.aperture.radius + height_2 = width_2 + else: + width_2 = self.aperture.width / 2. + height_2 = self.aperture.height / 2. + min_x = min(self.start[0], self.end[0]) - width_2 + max_x = max(self.start[0], self.end[0]) + width_2 + min_y = min(self.start[1], self.end[1]) - height_2 + max_y = max(self.start[1], self.end[1]) + height_2 + return (min_x, min_y), (max_x, max_y) @property def bounding_box_no_aperture(self): @@ -320,11 +125,7 @@ class Line(Primitive): return str(self) class Arc(Primitive): - """ - """ - - def __init__(self, start, end, center, direction, aperture, quadrant_mode, - level_polarity=None, **kwargs): + def __init__(self, start, end, center, direction, aperture, level_polarity=None, **kwargs): super(Arc, self).__init__(**kwargs) self.level_polarity = level_polarity self._start = start @@ -332,7 +133,6 @@ class Arc(Primitive): self._center = center self.direction = direction self.aperture = aperture - self._quadrant_mode = quadrant_mode self._to_convert = ['start', 'end', 'center', 'aperture'] @property @@ -366,15 +166,6 @@ class Arc(Primitive): self._changed() self._center = value - @property - def quadrant_mode(self): - return self._quadrant_mode - - @quadrant_mode.setter - def quadrant_mode(self, quadrant_mode): - self._changed() - self._quadrant_mode = quadrant_mode - @property def radius(self): dy, dx = tuple([start - center for start, center @@ -411,39 +202,6 @@ class Arc(Primitive): theta0 = (self.start_angle + two_pi) % two_pi theta1 = (self.end_angle + two_pi) % two_pi points = [self.start, self.end] - if self.quadrant_mode == 'multi-quadrant': - if self.direction == 'counterclockwise': - # Passes through 0 degrees - if theta0 >= theta1: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if (((theta0 <= math.pi / 2.) and ((theta1 >= math.pi / 2.) or (theta1 <= theta0))) - or ((theta1 > math.pi / 2.) and (theta1 <= theta0))): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0)) - or ((theta1 > math.pi) and (theta1 <= theta0))): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if (theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 <= theta0) - or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))): - points.append((self.center[0], self.center[1] - self.radius)) - else: - # Passes through 0 degrees - if theta1 >= theta0: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if (((theta1 <= math.pi / 2.) and (theta0 >= math.pi / 2. or theta0 <= theta1)) - or ((theta0 > math.pi / 2.) and (theta0 <= theta1))): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1)) - or ((theta0 > math.pi) and (theta0 <= theta1))): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if (((theta1 <= math.pi * 1.5) and (theta0 >= math.pi * 1.5 or theta0 <= theta1)) - or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))): - points.append((self.center[0], self.center[1] - self.radius)) x, y = zip(*points) if hasattr(self.aperture, 'radius'): min_x = min(x) - self.aperture.radius @@ -466,43 +224,6 @@ class Arc(Primitive): theta0 = (self.start_angle + two_pi) % two_pi theta1 = (self.end_angle + two_pi) % two_pi points = [self.start, self.end] - if self.quadrant_mode == 'multi-quadrant': - if self.direction == 'counterclockwise': - # Passes through 0 degrees - if theta0 >= theta1: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if (((theta0 <= math.pi / 2.) and ( - (theta1 >= math.pi / 2.) or (theta1 <= theta0))) - or ((theta1 > math.pi / 2.) and (theta1 <= theta0))): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0)) - or ((theta1 > math.pi) and (theta1 <= theta0))): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if (theta0 <= math.pi * 1.5 and ( - theta1 >= math.pi * 1.5 or theta1 <= theta0) - or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))): - points.append((self.center[0], self.center[1] - self.radius)) - else: - # Passes through 0 degrees - if theta1 >= theta0: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if (((theta1 <= math.pi / 2.) and ( - theta0 >= math.pi / 2. or theta0 <= theta1)) - or ((theta0 > math.pi / 2.) and (theta0 <= theta1))): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1)) - or ((theta0 > math.pi) and (theta0 <= theta1))): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if (((theta1 <= math.pi * 1.5) and ( - theta0 >= math.pi * 1.5 or theta0 <= theta1)) - or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))): - points.append((self.center[0], self.center[1] - self.radius)) x, y = zip(*points) min_x = min(x) @@ -522,8 +243,7 @@ class Circle(Primitive): """ """ - def __init__(self, position, diameter, hole_diameter=None, - hole_width=0, hole_height=0, **kwargs): + def __init__(self, position, diameter, polarity_dark=True): super(Circle, self).__init__(**kwargs) validate_coordinates(position) self._position = position @@ -1087,7 +807,7 @@ class Region(Primitive): @property def bounding_box(self): if self._bounding_box is None: - xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives]) + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) minx, maxx = zip(*xlims) miny, maxy = zip(*ylims) min_x = min(minx) diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py index 1ab030c..0c7b1f4 100644 --- a/gerbonara/gerber/rs274x.py +++ b/gerbonara/gerber/rs274x.py @@ -145,22 +145,34 @@ class GerberFile(CamFile): return ((min_x, max_x), (min_y, max_y)) - # TODO: re-add settings arg - def write(self, filename): + def generate_statements(self): self.settings.notation = 'absolute' self.settings.zeros = 'trailing' self.settings.format = self.format self.units = self.units - with open(filename, 'w') as f: - print(UnitStmt().to_gerber(self.settings), file=f) - print(FormatSpecStmt().to_gerber(self.settings), file=f) - print(ImagePolarityStmt().to_gerber(self.settings), file=f) + yield UnitStmt() + yield FormatSpecStmt() + yield ImagePolarityStmt() + yield SingleQuadrantModeStmt() + + yield from self.aperture_macros.values() + yield from self.aperture_defs + yield from self.main_statements + + yield EofStmt() + + def __str__(self): + return '\n'.join(self.generate_statements()) - for thing in chain(self.aperture_macros.values(), self.aperture_defs, self.main_statements): - print(thing.to_gerber(self.settings), file=f) + def save(self, filename): + with open(filename, 'w', encoding='utf-8') as f: # Encoding is specified as UTF-8 by spec. + for stmt in self.generate_statements(): + print(stmt.to_gerber(self.settings), file=f) - print('M02*', file=f) + def render_primitives(self): + for stmt in self.main_statements: + yield from stmt.render_primitives() def to_inch(self): if self.units == 'metric': @@ -245,6 +257,45 @@ class GerberFile(CamFile): statement.shape = polygon +@dataclass +class GraphicsState: + polarity_dark : bool = True + point : tuple = None + aperture : ApertureDefStmt = None + interpolation_mode : InterpolationModeStmt = None + multi_quadrant_mode : bool = None # used only for syntax checking + + def flash(self, x, y): + self.point = (x, y) + return Aperture(self.aperture, x, y) + + def interpolate(self, x, y, i=None, j=None): + if self.interpolation_mode == LinearModeStmt: + if i is not None or j is not None: + raise SyntaxError("i/j coordinates given for linear D01 operation (which doesn't take i/j)") + + return self._create_line(x, y) + + else: + return self._create_arc(x, y, i, j) + + def _create_line(self, x, y): + old_point, self.point = self.point, (x, y) + return Line(old_point, self.point, self.aperture, self.polarity_dark) + + def _create_arc(self, x, y, i, j): + if self.multi_quadrant_mode is None: + warnings.warn('Circular arc interpolation without explicit G75 Single-Quadrant mode statement. '\ + 'This can cause problems with older gerber interpreters.', SyntaxWarning) + + elif self.multi_quadrant_mode: + raise SyntaxError('Circular arc interpolation in multi-quadrant mode (G74) is not implemented.') + + old_point, self.point = self.point, (x, y) + direction = 'ccw' if self.interpolation_mode == CircularCCWModeStmt else 'cw' + return Arc(old_point, self.point, (i, j), direction, self.aperture, self.polarity_dark): + + class GerberParser: NUMBER = r"[\+-]?\d+" DECIMAL = r"[\+-]?\d+([.]?\d+)?" @@ -260,6 +311,7 @@ class GerberParser: 'comment': r"G0?4(?P[^*]*)(\*)?", 'format_spec': r"FS(?P(L|T|D))?(?P(A|I))[NG0-9]*X(?P[0-7][0-7])Y(?P[0-7][0-7])[DM0-9]*", 'load_polarity': r"LP(?P(D|C))", + # FIXME LM, LR, LS 'load_name': r"LN(?P.*)", 'offset': fr"OF(A(?P{DECIMAL}))?(B(?P{DECIMAL}))?", 'include_file': r"IF(?P.*)", @@ -271,8 +323,8 @@ class GerberParser: 'scale_factor': fr"SF(A(?P{DECIMAL}))?(B(?P{DECIMAL}))?", 'aperture_definition': fr"ADD(?P\d+)(?PC|R|O|P|{NAME})[,]?(?P[^,%]*)", 'aperture_macro': fr"AM(?P{NAME})\*(?P[^%]*)", - 'region_mode': r'(?PG3[67])\*', - 'quadrant_mode': r'(?PG7[45])\*', + 'region_start': r'G36\*', + 'region_end': r'G37\*', 'old_unit':r'(?PG7[01])\*', 'old_notation': r'(?PG9[01])\*', 'eof': r"M0?[02]\*", @@ -287,11 +339,13 @@ class GerberParser: self.include_dir = include_dir self.include_stack = [] self.settings = FileSettings() + self.current_region = None + self.graphics_state = GraphicsState() + self.statements = [] self.primitives = [] self.apertures = {} self.macros = {} - self.current_region = None self.x = 0 self.y = 0 self.last_operation = None @@ -302,13 +356,15 @@ class GerberParser: self.image_polarity = 'positive' self.level_polarity = 'dark' self.region_mode = 'off' - self.quadrant_mode = 'multi-quadrant' self.step_and_repeat = (1, 1, 0, 0) def parse(self, data): for stmt in self._parse(data): + if self.current_region is None: + self.statements.append(stmt) + else: + self.current_region.append(stmt) self.evaluate(stmt) - self.statements.append(stmt) # Initialize statement units for stmt in self.statements: @@ -370,21 +426,26 @@ class GerberParser: def _parse_interpolation_mode(self, match): if match['code'] == 'G01': + self.graphics_state.interpolation_mode = LinearModeStmt yield LinearModeStmt() elif match['code'] == 'G02': + self.graphics_state.interpolation_mode = CircularCWModeStmt yield CircularCWModeStmt() elif match['code'] == 'G03': + self.graphics_state.interpolation_mode = CircularCCWModeStmt yield CircularCCWModeStmt() elif match['code'] == 'G74': - yield MultiQuadrantModeStmt() + self.graphics_state.multi_quadrant_mode = True # used only for syntax checking elif match['code'] == 'G75': - yield SingleQuadrantModeStmt() + self.graphics_state.multi_quadrant_mode = False + # we always emit a G75 at the beginning of the file. def _parse_coord(self, match): - x = parse_gerber_value(match.get('x'), self.settings) - y = parse_gerber_value(match.get('y'), self.settings) - i = parse_gerber_value(match.get('i'), self.settings) - j = parse_gerber_value(match.get('j'), self.settings) + x = parse_gerber_value(match['x'], self.settings) + y = parse_gerber_value(match['y'], self.settings) + i = parse_gerber_value(match['i'], self.settings) + j = parse_gerber_value(match['j'], self.settings) + if not (op := match['operation']): if self.last_operation == 'D01': warnings.warn('Coordinate statement without explicit operation code. This is forbidden by spec.', @@ -395,22 +456,28 @@ class GerberParser: 'mode and the last operation statement was not D01.') if op in ('D1', 'D01'): - yield InterpolateStmt(x, y, i, j) + yield self.graphics_state.interpolate(x, y, i, j) + + else: + if i is not None or j is not None: + raise SyntaxError("i/j coordinates given for D02/D03 operation (which doesn't take i/j)") + + if op in ('D2', 'D02'): + self.graphics_state.point = (x, y) - if i is not None or j is not None: - raise SyntaxError("i/j coordinates given for D02/D03 operation (which doesn't take i/j)") - - if op in ('D2', 'D02'): - yield MoveStmt(x, y, i, j) - else: # D03 - yield FlashStmt(x, y, i, j) + else: # D03 + yield self.graphics_state.flash(x, y) def _parse_aperture(self, match): number = int(match['number']) if number < 10: raise SyntaxError(f'Invalid aperture number {number}: Aperture number must be >= 10.') - yield ApertureStmt(number) + + if number not in self.apertures: + raise SyntaxError(f'Tried to access undefined aperture {number}') + + self.graphics_state.aperture = self.apertures[number] def _parse_format_spec(self, match): # This is a common problem in Eagle files, so just suppress it @@ -421,7 +488,7 @@ class GerberParser: raise SyntaxError(f'FS specifies different coordinate formats for X and Y ({match["x"]} != {match["y"]})') self.settings.number_format = int(match['x'][0]), int(match['x'][1]) - yield FormatSpecStmt() + yield from () # We always force a format spec statement at the beginning of the file def _parse_unit_mode(self, match): if match['unit'] == 'MM': @@ -429,16 +496,17 @@ class GerberParser: else: self.settings.units = 'inch' - yield MOParamStmt() + yield from () # We always force a unit mode statement at the beginning of the file def _parse_load_polarity(self, match): - yield LoadPolarityStmt(dark=(match['polarity'] == 'D')) + yield LoadPolarityStmt(dark=match['polarity'] == 'D') def _parse_offset(self, match): a, b = match['a'], match['b'] a = float(a) if a else 0 b = float(b) if b else 0 - yield OffsetStmt(a, b) + self.settings.offset = a, b + yield from () # Handled by coordinate normalization def _parse_include_file(self, match): if self.include_dir is None: @@ -470,25 +538,25 @@ class GerberParser: warnings.warn('Deprecated AS (axis selection) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.settings.output_axes = match['axes'] - yield AxisSelectionStmt() + yield from () # Handled by coordinate normalization def _parse_image_polarity(self, match): warnings.warn('Deprecated IP (image polarity) statement found. This deprecated since rev. I4 (Oct 2013).', DeprecationWarning) self.settings.image_polarity = match['polarity'] - yield ImagePolarityStmt() + yield from () # We always emit this in the header def _parse_image_rotation(self, match): warnings.warn('Deprecated IR (image rotation) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.settings.image_rotation = int(match['rotation']) - yield ImageRotationStmt() + yield from () # Handled by coordinate normalization def _parse_mirror_image(self, match): warnings.warn('Deprecated MI (mirror image) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.settings.mirror = bool(int(match['a'] or '0')), bool(int(match['b'] or '1')) - yield MirrorImageStmt() + yield from () # Handled by coordinate normalization def _parse_scale_factor(self, match): warnings.warn('Deprecated SF (scale factor) statement found. This deprecated since rev. I1 (Dec 2012).', @@ -496,26 +564,20 @@ class GerberParser: a = float(match['a']) if match['a'] else 1.0 b = float(match['b']) if match['b'] else 1.0 self.settings.scale_factor = a, b - yield ScaleFactorStmt() + yield from () # Handled by coordinate normalization def _parse_comment(self, match): yield CommentStmt(match["comment"]) - def _parse_region_mode(self, match): - yield RegionStartStatement() if match['mode'] == 'G36' else RegionEndStatement() - - elif param["param"] == "AM": - yield AMParamStmt.from_dict(param, units=self.settings.units) - elif param["param"] == "AD": - yield ADParamStmt.from_dict(param) + def _parse_region_start(self, _match): + current_region = RegionGroup() - def _parse_quadrant_mode(self, match): - if match['mode'] == 'G74': - warnings.warn('Deprecated G74 single quadrant mode statement found. This deprecated since 2021.', - DeprecationWarning) - yield SingleQuadrantModeStmt() - else: - yield MultiQuadrantModeStmt() + def _parse_region_end(self, _match): + if self.current_region is None: + raise SyntaxError('Region end command (G37) outside of region') + + yield self.current_region + self.current_region = None def _parse_old_unit(self, match): self.settings.units = 'inch' if match['mode'] == 'G70' else 'mm' @@ -531,12 +593,33 @@ class GerberParser: DeprecationWarning) yield CommentStmt(f'Replaced deprecated {match["mode"]} notation mode statement with FS statement') - def _parse_eof(self, match): + def _parse_eof(self, _match): yield EofStmt() def _parse_ignored(self, match): yield CommentStmt(f'Ignoring {match{"stmt"]} statement.') + def _parse_aperture_definition(self, match): + modifiers = [ float(mod) for mod in match['modifiers'].split(',') ] + if match['shape'] == 'C': + aperture = ApertureCircle(*modifiers) + + elif match['shape'] == 'R' + aperture = ApertureRectangle(*modifiers) + + elif shape == 'O': + aperture = ApertureObround(*modifiers) + + elif shape == 'P': + aperture = AperturePolygon(*modifiers) + + else: + aperture = self.macros[shape].build(modifiers) + + self.apertures[d] = aperture + + + def evaluate(self, stmt): """ Evaluate Gerber statement and update image accordingly. @@ -567,83 +650,6 @@ class GerberParser: else: raise Exception("Invalid statement to evaluate") - def _define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - diameter = modifiers[0][0] - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 2: - hole_diameter = modifiers[0][1] - elif len(modifiers[0]) == 3: - rectangular_hole = modifiers[0][1:3] - - aperture = Circle(position=None, diameter=diameter, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - units=self.settings.units) - - elif shape == 'R': - width = modifiers[0][0] - height = modifiers[0][1] - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 3: - hole_diameter = modifiers[0][2] - elif len(modifiers[0]) == 4: - rectangular_hole = modifiers[0][2:4] - - aperture = Rectangle(position=None, width=width, height=height, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - units=self.settings.units) - elif shape == 'O': - width = modifiers[0][0] - height = modifiers[0][1] - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 3: - hole_diameter = modifiers[0][2] - elif len(modifiers[0]) == 4: - rectangular_hole = modifiers[0][2:4] - - aperture = Obround(position=None, width=width, height=height, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - units=self.settings.units) - elif shape == 'P': - outer_diameter = modifiers[0][0] - number_vertices = int(modifiers[0][1]) - if len(modifiers[0]) > 2: - rotation = modifiers[0][2] - else: - rotation = 0 - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 4: - hole_diameter = modifiers[0][3] - elif len(modifiers[0]) >= 5: - rectangular_hole = modifiers[0][3:5] - - aperture = Polygon(position=None, sides=number_vertices, - radius=outer_diameter/2.0, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - rotation=rotation) - else: - aperture = self.macros[shape].build(modifiers) - - aperture.units = self.settings.units - self.apertures[d] = aperture - def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': @@ -658,14 +664,6 @@ class GerberParser: self.quadrant_mode = stmt.mode def _evaluate_param(self, stmt): - if stmt.param == "FS": - self.settings.zero_suppression = stmt.zero_suppression - self.settings.format = stmt.format - self.settings.notation = stmt.notation - elif stmt.param == "MO": - self.settings.units = stmt.mode - elif stmt.param == "IP": - self.image_polarity = stmt.ip elif stmt.param == "LP": self.level_polarity = stmt.lp elif stmt.param == "AM": diff --git a/gerbonara/gerber/utils.py b/gerbonara/gerber/utils.py index c2e81cd..492321a 100644 --- a/gerbonara/gerber/utils.py +++ b/gerbonara/gerber/utils.py @@ -61,7 +61,7 @@ def parse_gerber_value(value, settings): """ - if value is None: + if not value: return None # Handle excellon edge case with explicit decimal. "That was easy!" @@ -317,146 +317,3 @@ def sq_distance(point1, point2): return diff1 * diff1 + diff2 * diff2 -def listdir(directory, ignore_hidden=True, ignore_os=True): - """ List files in given directory. - Differs from os.listdir() in that hidden and OS-generated files are ignored - by default. - - Parameters - ---------- - directory : str - path to the directory for which to list files. - - ignore_hidden : bool - If True, ignore files beginning with a leading '.' - - ignore_os : bool - If True, ignore OS-generated files, e.g. Thumbs.db - - Returns - ------- - files : list - list of files in specified directory - """ - os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db') - files = os.listdir(directory) - if ignore_hidden: - files = [f for f in files if not f.startswith('.')] - if ignore_os: - files = [f for f in files if not f in os_files] - return files - -def ConvexHull_qh(points): - #a hull must be a planar shape with nonzero area, so there must be at least 3 points - if(len(points)<3): - raise Exception("not a planar shape") - #find points with lowest and highest X coordinates - minxp=0; - maxxp=0; - for i in range(len(points)): - if(points[i][0]points[maxxp][0]): - maxxp=i; - if minxp==maxxp: - #all points are collinear - raise Exception("not a planar shape") - #separate points into those above and those below the minxp-maxxp line - lpoints=[] - rpoints=[] - #to detemine if point X is on the left or right of dividing line A-B, compare slope of A-B to slope of A-X - #slope is (By-Ay)/(Bx-Ax) - a=points[minxp] - b=points[maxxp] - slopeab=atan2(b[1]-a[1],b[0]-a[0]) - for i in range(len(points)): - p=points[i] - if i == minxp or i == maxxp: - continue - slopep=atan2(p[1]-a[1],p[0]-a[0]) - sdiff=slopep-slopeab - if(sdiffpi):sdiff-=2*pi - if(sdiff>0): - lpoints+=[i] - if(sdiff<0): - rpoints+=[i] - hull=[minxp]+_findhull(rpoints, maxxp, minxp, points)+[maxxp]+_findhull(lpoints, minxp, maxxp, points) - hullo=_optimize(hull,points) - return hullo - -def _optimize(hull,points): - #find triplets that are collinear and remove middle point - toremove=[] - newhull=hull[:] - l=len(hull) - for i in range(l): - p1=hull[i] - p2=hull[(i+1)%l] - p3=hull[(i+2)%l] - #(p1.y-p2.y)*(p1.x-p3.x)==(p1.y-p3.y)*(p1.x-p2.x) - if (points[p1][1]-points[p2][1])*(points[p1][0]-points[p3][0])==(points[p1][1]-points[p3][1])*(points[p1][0]-points[p2][0]): - toremove+=[p2] - for i in toremove: - newhull.remove(i) - return newhull - -def _distance(a, b, x): - #find the distance between point x and line a-b - return abs((b[1]-a[1])*x[0]-(b[0]-a[0])*x[1]+b[0]*a[1]-a[0]*b[1])/sqrt((b[1]-a[1])**2 + (b[0]-a[0])**2 ); - -def _findhull(idxp, a_i, b_i, points): - #if no points in input, return no points in output - if(len(idxp)==0): - return []; - #find point c furthest away from line a-b - farpoint=-1 - fdist=-1.0; - for i in idxp: - d=_distance(points[a_i], points[b_i], points[i]) - if(d>fdist): - fdist=d; - farpoint=i - if(fdist<=0): - #none of the points have a positive distance from line, bad things have happened - return [] - #separate points into those inside triangle, those outside triangle left of far point, and those outside triangle right of far point - a=points[a_i] - b=points[b_i] - c=points[farpoint] - slopeac=atan2(c[1]-a[1],c[0]-a[0]) - slopecb=atan2(b[1]-c[1],b[0]-c[0]) - lpoints=[] - rpoints=[] - for i in idxp: - if i==farpoint: - #ignore triangle vertex - continue - x=points[i] - #if point x is left of line a-c it's in left set - slopeax=atan2(x[1]-a[1],x[0]-a[0]) - if slopeac==slopeax: - continue - sdiff=slopeac-slopeax - if(sdiff<-pi):sdiff+=2*pi - if(sdiff>pi):sdiff-=2*pi - if(sdiff<0): - lpoints+=[i] - else: - #if point x is right of line b-c it's in right set, otherwise it's inside triangle and can be ignored - slopecx=atan2(x[1]-c[1],x[0]-c[0]) - if slopecx==slopecb: - continue - sdiff=slopecx-slopecb - if(sdiff<-pi):sdiff+=2*pi - if(sdiff>pi):sdiff-=2*pi - if(sdiff>0): - rpoints+=[i] - #the hull segment between points a and b consists of the hull segment between a and c, the point c, and the hull segment between c and b - ret=_findhull(rpoints, farpoint, b_i, points)+[farpoint]+_findhull(lpoints, a_i, farpoint, points) - return ret - - -def convex_hull(points): - vertices = ConvexHull_qh(points) - return [points[idx] for idx in vertices] -- cgit