summaryrefslogtreecommitdiff
path: root/gerbonara/gerber
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2021-11-10 21:39:03 +0100
committerjaseg <git@jaseg.de>2021-11-10 21:39:03 +0100
commitd21a2e67ff34d3f29e37a01f926b9e8f72003637 (patch)
tree8218f339a3336283c060aa5657a76a463b8e1982 /gerbonara/gerber
parent125eb821b9f5d4c58b17d43e318e9a6829120d03 (diff)
downloadgerbonara-d21a2e67ff34d3f29e37a01f926b9e8f72003637.tar.gz
gerbonara-d21a2e67ff34d3f29e37a01f926b9e8f72003637.tar.bz2
gerbonara-d21a2e67ff34d3f29e37a01f926b9e8f72003637.zip
WIP
Diffstat (limited to 'gerbonara/gerber')
-rw-r--r--gerbonara/gerber/am_read.py255
-rw-r--r--gerbonara/gerber/am_statements.py1010
-rw-r--r--gerbonara/gerber/cam.py3
-rw-r--r--gerbonara/gerber/gerber_statements.py219
-rw-r--r--gerbonara/gerber/pcb.py3
-rw-r--r--gerbonara/gerber/primitives.py332
-rw-r--r--gerbonara/gerber/rs274x.py274
-rw-r--r--gerbonara/gerber/utils.py145
8 files changed, 207 insertions, 2034 deletions
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 <ham@hamiltonkib.be>
-# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com>
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-""" 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 <ham@hamiltonkib.be> and Paulo Henrique Silva
-# <ph.silva@gmail.com>
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from math import asin
-import math
-
-from .primitives import *
-from .utils import validate_coordinates, inch, metric, rotate_point
-from .am_expression import AMConstantExpression
-
-
-
-# TODO: Add support for aperture macro variables
-__all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive',
- 'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive',
- 'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive',
- 'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive']
-
-
-class AMPrimitive(object):
- """ Aperture Macro Primitive Base Class
-
- Parameters
- ----------
- code : int
- primitive shape code
-
- exposure : str
- on or off Primitives with exposure on create a slid part of
- the macro aperture, and primitives with exposure off erase the
- solid part created previously in the aperture macro definition.
- .. note::
- The erasing effect is limited to the aperture definition in
- which it occurs.
-
- Returns
- -------
- primitive : :class: `gerber.am_statements.AMPrimitive`
-
- Raises
- ------
- TypeError, ValueError
- """
-
- def __init__(self, code, exposure=None, rotation=AMConstantExpression(0)):
- VALID_CODES = (0, 1, 2, 4, 5, 7, 20, 21, 22, 9999)
- if not isinstance(code, int):
- raise TypeError('Aperture Macro Primitive code must be an integer')
- elif code not in VALID_CODES:
- raise ValueError('Invalid Code. Valid codes are %s.' %
- ', '.join(map(str, VALID_CODES)))
- if exposure is not None and exposure.lower() not in ('on', 'off'):
- raise ValueError('Exposure must be either on or off')
- self.code = code
- self.exposure = exposure.lower() if exposure is not None else None
- self.rotation = rotation
-
- def rotate(self, angle, center=None):
- self.rotation = AMOperatorExpression(AMOperatorExpression.ADD,
- self.rotation,
- AMConstantExpression(float(angle)))
- self.rotation = self.rotation.optimize()
-
- #def to_inch(self):
- # raise NotImplementedError('Subclass must implement to_inch')
-
- #def to_metric(self):
- # raise NotImplementedError('Subclass must implement to_metric')
-
- #def to_gerber(self, settings=None):
- # raise NotImplementedError('Subclass must implement to_gerber')
-
- #def to_instructions(self):
- # raise NotImplementedError('Subclass must implement to_instructions')
-
- #def to_primitive(self, units):
- # """ Return a Primitive instance based on the specified macro params.
- # """
- # raise NotImplementedError('Subclass must implement to_primitive')
-
- @property
- def _level_polarity(self):
- if self.exposure == 'off':
- return 'clear'
- return 'dark'
-
- def __eq__(self, other):
- return self.__dict__ == other.__dict__
-
-
-class AMCommentPrimitive(AMPrimitive):
- """ Aperture Macro Comment primitive. Code 0
-
- The comment primitive has no image meaning. It is used to include human-
- readable comments into the AM command.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.1:** Comment, primitive code 0
-
- Parameters
- ----------
- code : int
- Aperture Macro primitive code. 0 Indicates an AMCommentPrimitive
-
- comment : str
- The comment as a string.
-
- Returns
- -------
- CommentPrimitive : :class:`gerber.am_statements.AMCommentPrimitive`
- An Initialized AMCommentPrimitive
-
- Raises
- ------
- ValueError
- """
- @classmethod
- def from_gerber(cls, primitive):
- primitive = primitive.strip()
- code = int(primitive[0])
- comment = primitive[1:]
- return cls(code, comment)
-
- def __init__(self, code, comment):
- if code != 0:
- raise ValueError('Not a valid Aperture Macro Comment statement')
- super().__init__(code)
- self.comment = comment.strip(' *')
-
- def to_inch(self):
- pass
-
- def to_metric(self):
- pass
-
- def to_gerber(self, settings=None):
- return f'0 {self.comment} *'
-
- def to_primitive(self, units):
- """
- Returns None - has not primitive representation
- """
- return None
-
- def to_instructions(self):
- return [(OpCode.PUSH, self.comment), (OpCode.PRIM, self.code)]
-
- def __str__(self):
- return '<Aperture Macro Comment: %s>' % self.comment
-
-
-class AMCirclePrimitive(AMPrimitive):
- """ Aperture macro Circle primitive. Code 1
-
- A circle primitive is defined by its center point and diameter.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.2:** Circle, primitive code 1
-
- Parameters
- ----------
- code : int
- Circle Primitive code. Must be 1
-
- exposure : string
- 'on' or 'off'
-
- diameter : float
- Circle diameter
-
- position : tuple (<float>, <float>)
- Position of the circle relative to the macro origin
-
- Returns
- -------
- CirclePrimitive : :class:`gerber.am_statements.AMCirclePrimitive`
- An initialized AMCirclePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(',')
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- diameter = float(modifiers[2])
- position = (float(modifiers[3]), float(modifiers[4]))
- return cls(code, exposure, diameter, position)
-
- @classmethod
- def from_primitive(cls, primitive):
- return cls(1, 'on', primitive.diameter, primitive.position)
-
- def __init__(self, code, exposure, diameter, position):
- validate_coordinates(position)
- if code != 1:
- raise ValueError('CirclePrimitive code is 1')
- super(AMCirclePrimitive, self).__init__(code, exposure)
- self.diameter = diameter
- self.position = position
-
- def to_inch(self):
- self.diameter = inch(self.diameter)
- self.position = tuple([inch(x) for x in self.position])
-
- def to_metric(self):
- self.diameter = metric(self.diameter)
- self.position = tuple([metric(x) for x in self.position])
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.position
- return f'{self.code},{exposure},{self.diameter},{x},{y}*'
-
- def to_primitive(self, units):
- return Circle((self.position), self.diameter, units=units, level_polarity=self._level_polarity)
-
-
-class AMVectorLinePrimitive(AMPrimitive):
- """ Aperture Macro Vector Line primitive. Code 2 or 20.
-
- A vector line is a rectangle defined by its line width, start, and end
- points. The line ends are rectangular.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.3:** Vector Line, primitive code 2 or 20.
-
- Parameters
- ----------
- code : int
- Vector Line Primitive code. Must be either 2 or 20.
-
- exposure : string
- 'on' or 'off'
-
- width : float
- Line width
-
- start : tuple (<float>, <float>)
- coordinate of line start point
-
- end : tuple (<float>, <float>)
- coordinate of line end point
-
- rotation : float
- Line rotation about the origin.
-
- Returns
- -------
- LinePrimitive : :class:`gerber.am_statements.AMVectorLinePrimitive`
- An initialized AMVectorLinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
- return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(',')
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- width = float(modifiers[2])
- start = (float(modifiers[3]), float(modifiers[4]))
- end = (float(modifiers[5]), float(modifiers[6]))
- rotation = float(modifiers[7])
- return cls(code, exposure, width, start, end, rotation)
-
- def __init__(self, code, exposure, width, start, end, rotation):
- validate_coordinates(start)
- validate_coordinates(end)
- if code not in (2, 20):
- raise ValueError('VectorLinePrimitive codes are 2 or 20')
- super(AMVectorLinePrimitive, self).__init__(code, exposure)
- self.width = width
- self.start = start
- self.end = end
- self.rotation = rotation
-
- def to_inch(self):
- self.width = inch(self.width)
- self.start = tuple([inch(x) for x in self.start])
- self.end = tuple([inch(x) for x in self.end])
-
- def to_metric(self):
- self.width = metric(self.width)
- self.start = tuple([metric(x) for x in self.start])
- self.end = tuple([metric(x) for x in self.end])
-
- def to_gerber(self, settings=None):
- exp = 1 if self.exposure == 'on' else 0
- start_x, start_y = self.start
- end_x, end_y = self.end
- return f'{self.code},{exp},{self.width},{start_x},{start_y},{end_x},{end_y},{self.rotation}*'
-
- def to_primitive(self, units):
- """
- Convert this to a primitive. We use the Outline to represent this (instead of Line)
- because the behaviour of the end caps is different for aperture macros compared to Lines
- when rotated.
- """
-
- # Use a line to generate our vertices easily
- line = Line(self.start, self.end, Rectangle(None, self.width, self.width))
- vertices = line.vertices
-
- aperture = Circle((0, 0), 0)
-
- lines = []
- prev_point = rotate_point(vertices[-1], self.rotation, (0, 0))
- for point in vertices:
- cur_point = rotate_point(point, self.rotation, (0, 0))
-
- lines.append(Line(prev_point, cur_point, aperture))
-
- return Outline(lines, units=units, level_polarity=self._level_polarity)
-
-
-class AMOutlinePrimitive(AMPrimitive):
- """ Aperture Macro Outline primitive. Code 4.
-
- An outline primitive is an area enclosed by an n-point polygon defined by
- its start point and n subsequent points. The outline must be closed, i.e.
- the last point must be equal to the start point. Self intersecting
- outlines are not allowed.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.6:** Outline, primitive code 4.
-
- Parameters
- ----------
- code : int
- OutlinePrimitive code. Must be 6.
-
- exposure : string
- 'on' or 'off'
-
- start_point : tuple (<float>, <float>)
- coordinate of outline start point
-
- points : list of tuples (<float>, <float>)
- coordinates of subsequent points
-
- rotation : float
- outline rotation about the origin.
-
- Returns
- -------
- OutlinePrimitive : :class:`gerber.am_statements.AMOutlineinePrimitive`
- An initialized AMOutlinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
-
- start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6))
- points = []
- for prim in primitive.primitives:
- points.append((round(prim.end[0], 6), round(prim.end[1], 6)))
-
- rotation = 0.0
-
- return cls(4, 'on', start_point, points, rotation)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
-
- code = int(modifiers[0])
- exposure = "on" if float(modifiers[1]) == 1 else "off"
- n = int(float(modifiers[2]))
- start_point = (float(modifiers[3]), float(modifiers[4]))
- points = []
- for i in range(n):
- points.append((float(modifiers[5 + i * 2]),
- float(modifiers[5 + i * 2 + 1])))
- rotation = float(modifiers[-1])
- return cls(code, exposure, start_point, points, rotation)
-
- def __init__(self, code, exposure, start_point, points, rotation):
- """ Initialize AMOutlinePrimitive
- """
- validate_coordinates(start_point)
- for point in points:
- validate_coordinates(point)
- if code != 4:
- raise ValueError('OutlinePrimitive code is 4')
- super(AMOutlinePrimitive, self).__init__(code, exposure)
- self.start_point = start_point
- if points[-1] != start_point:
- raise ValueError('OutlinePrimitive must be closed')
- self.points = points
- self.rotation = rotation
-
- def to_inch(self):
- self.start_point = tuple([inch(x) for x in self.start_point])
- self.points = tuple([(inch(x), inch(y)) for x, y in self.points])
-
- def to_metric(self):
- self.start_point = tuple([metric(x) for x in self.start_point])
- self.points = tuple([(metric(x), metric(y)) for x, y in self.points])
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x0, y0 = self.start_point
- points = ",\n".join([ f'{x:.6f},{y:.6f}' for x, y in self.points ])
- return f'{self.code},{exposure},{len(self.points)},{x0:.6f},{y0:.6f},{points},{self.rotation}*'
-
- def to_primitive(self, units):
- """
- Convert this to a drawable primitive. This uses the Outline instead of Line
- primitive to handle differences in end caps when rotated.
- """
-
- lines = []
- prev_point = rotate_point(self.start_point, self.rotation)
- for point in self.points:
- cur_point = rotate_point(point, self.rotation)
-
- lines.append(Line(prev_point, cur_point, Circle((0,0), 0)))
-
- prev_point = cur_point
-
- if lines[0].start != lines[-1].end:
- raise ValueError('Outline must be closed')
-
- return Outline(lines, units=units, level_polarity=self._level_polarity)
-
-
-class AMPolygonPrimitive(AMPrimitive):
- """ Aperture Macro Polygon primitive. Code 5.
-
- A polygon primitive is a regular polygon defined by the number of
- vertices, the center point, and the diameter of the circumscribed circle.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.8:** Polygon, primitive code 5.
-
- Parameters
- ----------
- code : int
- PolygonPrimitive code. Must be 5.
-
- exposure : string
- 'on' or 'off'
-
- vertices : int, 3 <= vertices <= 12
- Number of vertices
-
- position : tuple (<float>, <float>)
- X and Y coordinates of polygon center
-
- diameter : float
- diameter of circumscribed circle.
-
- rotation : float
- polygon rotation about the origin.
-
- Returns
- -------
- PolygonPrimitive : :class:`gerber.am_statements.AMPolygonPrimitive`
- An initialized AMPolygonPrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
- return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- exposure = "on" if float(modifiers[1]) == 1 else "off"
- vertices = int(float(modifiers[2]))
- position = (float(modifiers[3]), float(modifiers[4]))
- try:
- diameter = float(modifiers[5])
- except:
- diameter = 0
-
- rotation = float(modifiers[6])
- return cls(code, exposure, vertices, position, diameter, rotation)
-
- def __init__(self, code, exposure, vertices, position, diameter, rotation):
- """ Initialize AMPolygonPrimitive
- """
- if code != 5:
- raise ValueError('PolygonPrimitive code is 5')
- super(AMPolygonPrimitive, self).__init__(code, exposure)
- if vertices < 3 or vertices > 12:
- raise ValueError('Number of vertices must be between 3 and 12')
- self.vertices = vertices
- validate_coordinates(position)
- self.position = position
- self.diameter = diameter
- self.rotation = rotation
-
- def to_inch(self):
- self.position = tuple([inch(x) for x in self.position])
- self.diameter = inch(self.diameter)
-
- def to_metric(self):
- self.position = tuple([metric(x) for x in self.position])
- self.diameter = metric(self.diameter)
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.position
- return f'{self.code},{exposure},{self.vertices},{x:.4f},{y:.4f},{self.diameter:.4f},{self.rotation}*'
-
- def to_primitive(self, units):
- return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity)
-
-
-class AMMoirePrimitive(AMPrimitive):
- """ Aperture Macro Moire primitive. Code 6.
-
- The moire primitive is a cross hair centered on concentric rings (annuli).
- Exposure is always on.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.9:** Moire, primitive code 6.
-
- Parameters
- ----------
- code : int
- Moire Primitive code. Must be 6.
-
- position : tuple (<float>, <float>)
- X and Y coordinates of moire center
-
- diameter : float
- outer diameter of outer ring.
-
- ring_thickness : float
- thickness of concentric rings.
-
- gap : float
- gap between concentric rings.
-
- max_rings : float
- maximum number of rings
-
- crosshair_thickness : float
- thickness of crosshairs
-
- crosshair_length : float
- length of crosshairs
-
- rotation : float
- moire rotation about the origin.
-
- Returns
- -------
- MoirePrimitive : :class:`gerber.am_statements.AMMoirePrimitive`
- An initialized AMMoirePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- position = (float(modifiers[1]), float(modifiers[2]))
- diameter = float(modifiers[3])
- ring_thickness = float(modifiers[4])
- gap = float(modifiers[5])
- max_rings = int(float(modifiers[6]))
- crosshair_thickness = float(modifiers[7])
- crosshair_length = float(modifiers[8])
- rotation = float(modifiers[9])
- return cls(code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation)
-
- def __init__(self, code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation):
- """ Initialize AMoirePrimitive
- """
- if code != 6:
- raise ValueError('MoirePrimitive code is 6')
- super(AMMoirePrimitive, self).__init__(code, 'on')
- validate_coordinates(position)
- self.position = position
- self.diameter = diameter
- self.ring_thickness = ring_thickness
- self.gap = gap
- self.max_rings = max_rings
- self.crosshair_thickness = crosshair_thickness
- self.crosshair_length = crosshair_length
- self.rotation = rotation
-
- def to_inch(self):
- self.position = tuple([inch(x) for x in self.position])
- self.diameter = inch(self.diameter)
- self.ring_thickness = inch(self.ring_thickness)
- self.gap = inch(self.gap)
- self.crosshair_thickness = inch(self.crosshair_thickness)
- self.crosshair_length = inch(self.crosshair_length)
-
- def to_metric(self):
- self.position = tuple([metric(x) for x in self.position])
- self.diameter = metric(self.diameter)
- self.ring_thickness = metric(self.ring_thickness)
- self.gap = metric(self.gap)
- self.crosshair_thickness = metric(self.crosshair_thickness)
- self.crosshair_length = metric(self.crosshair_length)
-
-
- def to_gerber(self, settings=None):
- x, y = self.position
- return f'{self.code},{x:.4f},{y:.4f},{self.diameter},{self.ring_thickness},{self.gap},{self.max_rings},{self.crosshair_thickness},{self.crosshair_length},{self.rotation}*'
-
- def to_primitive(self, units):
- #raise NotImplementedError()
- return None
-
-
-class AMThermalPrimitive(AMPrimitive):
- """ Aperture Macro Thermal primitive. Code 7.
-
- The thermal primitive is a ring (annulus) interrupted by four gaps.
- Exposure is always on.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.10:** Thermal, primitive code 7.
-
- Parameters
- ----------
- code : int
- Thermal Primitive code. Must be 7.
-
- position : tuple (<float>, <float>)
- X and Y coordinates of thermal center
-
- outer_diameter : float
- outer diameter of thermal.
-
- inner_diameter : float
- inner diameter of thermal.
-
- gap : float
- gap thickness
-
- rotation : float
- thermal rotation about the origin.
-
- Returns
- -------
- ThermalPrimitive : :class:`gerber.am_statements.AMThermalPrimitive`
- An initialized AMThermalPrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- position = (float(modifiers[1]), float(modifiers[2]))
- outer_diameter = float(modifiers[3])
- inner_diameter = float(modifiers[4])
- gap = float(modifiers[5])
- rotation = float(modifiers[6])
- return cls(code, position, outer_diameter, inner_diameter, gap, rotation)
-
- def __init__(self, code, position, outer_diameter, inner_diameter, gap, rotation):
- if code != 7:
- raise ValueError('ThermalPrimitive code is 7')
- super(AMThermalPrimitive, self).__init__(code, 'on')
- validate_coordinates(position)
- self.position = position
- self.outer_diameter = outer_diameter
- self.inner_diameter = inner_diameter
- self.gap = gap
- self.rotation = rotation
-
- def to_inch(self):
- self.position = tuple([inch(x) for x in self.position])
- self.outer_diameter = inch(self.outer_diameter)
- self.inner_diameter = inch(self.inner_diameter)
- self.gap = inch(self.gap)
-
- def to_metric(self):
- self.position = tuple([metric(x) for x in self.position])
- self.outer_diameter = metric(self.outer_diameter)
- self.inner_diameter = metric(self.inner_diameter)
- self.gap = metric(self.gap)
-
- def to_gerber(self, settings=None):
- x, y = self.position
- return f'{self.code},{x:.4f},{y:.4f},{self.outer_diameter},{self.inner_diameter},{self.gap},{self.rotation}*'
-
- def _approximate_arc_cw(self, start_angle, end_angle, radius, center):
- """
- Get an arc as a series of points
-
- Parameters
- ----------
- start_angle : The start angle in radians
- end_angle : The end angle in radians
- radius`: Radius of the arc
- center : The center point of the arc (x, y) tuple
-
- Returns
- -------
- array of point tuples
- """
-
- # The total sweep
- sweep_angle = end_angle - start_angle
- num_steps = 10
-
- angle_step = sweep_angle / num_steps
-
- radius = radius
- center = center
-
- points = []
-
- for i in range(num_steps + 1):
- current_angle = start_angle + (angle_step * i)
-
- nextx = (center[0] + math.cos(current_angle) * radius)
- nexty = (center[1] + math.sin(current_angle) * radius)
-
- points.append((nextx, nexty))
-
- return points
-
- def to_primitive(self, units):
-
- # We start with calculating the top right section, then duplicate it
-
- inner_radius = self.inner_diameter / 2.0
- outer_radius = self.outer_diameter / 2.0
-
- # Calculate the start angle relative to the horizontal axis
- inner_offset_angle = asin(self.gap / 2.0 / inner_radius)
- outer_offset_angle = asin(self.gap / 2.0 / outer_radius)
-
- rotation_rad = math.radians(self.rotation)
- inner_start_angle = inner_offset_angle + rotation_rad
- inner_end_angle = math.pi / 2 - inner_offset_angle + rotation_rad
-
- outer_start_angle = outer_offset_angle + rotation_rad
- outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad
-
- outlines = []
- aperture = Circle((0, 0), 0)
-
- points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position)
- + list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position))))
- # Add in the last point since outlines should be closed
- points.append(points[0])
-
- # There are four outlines at rotated sections
- for rotation in [0, 90.0, 180.0, 270.0]:
-
- lines = []
- prev_point = rotate_point(points[0], rotation, self.position)
- for point in points[1:]:
- cur_point = rotate_point(point, rotation, self.position)
-
- lines.append(Line(prev_point, cur_point, aperture))
-
- prev_point = cur_point
-
- outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity))
-
- return outlines
-
-
-class AMCenterLinePrimitive(AMPrimitive):
- """ Aperture Macro Center Line primitive. Code 21.
-
- The center line primitive is a rectangle defined by its width, height, and center point.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.4:** Center Line, primitive code 21.
-
- Parameters
- ----------
- code : int
- Center Line Primitive code. Must be 21.
-
- exposure : str
- 'on' or 'off'
-
- width : float
- Width of rectangle
-
- height : float
- Height of rectangle
-
- center : tuple (<float>, <float>)
- X and Y coordinates of line center
-
- rotation : float
- rectangle rotation about its center.
-
- Returns
- -------
- CenterLinePrimitive : :class:`gerber.am_statements.AMCenterLinePrimitive`
- An initialized AMCenterLinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
-
- @classmethod
- def from_primitive(cls, primitive):
- width = primitive.width
- height = primitive.height
- center = primitive.position
- rotation = math.degrees(primitive.rotation)
- return cls(21, 'on', width, height, center, rotation)
-
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- width = float(modifiers[2])
- height = float(modifiers[3])
- center = (float(modifiers[4]), float(modifiers[5]))
- rotation = float(modifiers[6])
- return cls(code, exposure, width, height, center, rotation)
-
- def __init__(self, code, exposure, width, height, center, rotation):
- if code != 21:
- raise ValueError('CenterLinePrimitive code is 21')
- super(AMCenterLinePrimitive, self).__init__(code, exposure)
- self.width = width
- self.height = height
- validate_coordinates(center)
- self.center = center
- self.rotation = rotation
-
- def to_inch(self):
- self.center = tuple([inch(x) for x in self.center])
- self.width = inch(self.width)
- self.height = inch(self.height)
-
- def to_metric(self):
- self.center = tuple([metric(x) for x in self.center])
- self.width = metric(self.width)
- self.height = metric(self.height)
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.center
- return f'{self.code},{exposure},{self.width},{self.height},{x:.4f},{y:.4f},{self.rotation}*'
-
- def to_primitive(self, units):
-
- x = self.center[0]
- y = self.center[1]
- half_width = self.width / 2.0
- half_height = self.height / 2.0
-
- points = []
- points.append((x - half_width, y + half_height))
- points.append((x - half_width, y - half_height))
- points.append((x + half_width, y - half_height))
- points.append((x + half_width, y + half_height))
-
- aperture = Circle((0, 0), 0)
-
- lines = []
- prev_point = rotate_point(points[3], self.rotation, self.center)
- for point in points:
- cur_point = rotate_point(point, self.rotation, self.center)
-
- lines.append(Line(prev_point, cur_point, aperture))
-
- return Outline(lines, units=units, level_polarity=self._level_polarity)
-
-
-class AMLowerLeftLinePrimitive(AMPrimitive):
- """ Aperture Macro Lower Left Line primitive. Code 22.
-
- The lower left line primitive is a rectangle defined by its width, height, and the lower left point.
-
- .. seealso::
- `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
- **Section 4.12.3.5:** Lower Left Line, primitive code 22.
-
- Parameters
- ----------
- code : int
- Center Line Primitive code. Must be 22.
-
- exposure : str
- 'on' or 'off'
-
- width : float
- Width of rectangle
-
- height : float
- Height of rectangle
-
- lower_left : tuple (<float>, <float>)
- X and Y coordinates of lower left corner
-
- rotation : float
- rectangle rotation about its origin.
-
- Returns
- -------
- LowerLeftLinePrimitive : :class:`gerber.am_statements.AMLowerLeftLinePrimitive`
- An initialized AMLowerLeftLinePrimitive
-
- Raises
- ------
- ValueError, TypeError
- """
- @classmethod
- def from_gerber(cls, primitive):
- modifiers = primitive.strip(' *').split(",")
- code = int(modifiers[0])
- exposure = 'on' if float(modifiers[1]) == 1 else 'off'
- width = float(modifiers[2])
- height = float(modifiers[3])
- lower_left = (float(modifiers[4]), float(modifiers[5]))
- rotation = float(modifiers[6])
- return cls(code, exposure, width, height, lower_left, rotation)
-
- def __init__(self, code, exposure, width, height, lower_left, rotation):
- if code != 22:
- raise ValueError('LowerLeftLinePrimitive code is 22')
- super (AMLowerLeftLinePrimitive, self).__init__(code, exposure)
- self.width = width
- self.height = height
- validate_coordinates(lower_left)
- self.lower_left = lower_left
- self.rotation = rotation
-
- def to_inch(self):
- self.lower_left = tuple([inch(x) for x in self.lower_left])
- self.width = inch(self.width)
- self.height = inch(self.height)
-
- def to_metric(self):
- self.lower_left = tuple([metric(x) for x in self.lower_left])
- self.width = metric(self.width)
- self.height = metric(self.height)
-
- def to_gerber(self, settings=None):
- exposure = 1 if self.exposure == 'on' else 0
- x, y = self.lower_left
- return f'{self.code},{exposure},{self.width},{self.height},{x:.4f},{y:.4f},{self.rotation}*'
-
-
-class AMUnsupportPrimitive(AMPrimitive):
- @classmethod
- def from_gerber(cls, primitive):
- return cls(primitive)
-
- def __init__(self, primitive):
- super(AMUnsupportPrimitive, self).__init__(9999)
- self.primitive = primitive
-
- def to_inch(self):
- pass
-
- def to_metric(self):
- pass
-
- def to_gerber(self, settings=None):
- return self.primitive
-
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'<LP Level Polarity: {lp}>'
+ 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 '<AM Aperture Macro %s: %s>' % (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 '<AS Axis Select>'
-
class ImagePolarityStmt(ParamStmt):
""" IP - Image Polarity Statement. (Deprecated) """
@@ -326,157 +295,49 @@ class ImagePolarityStmt(ParamStmt):
return '<IP Image Polarity>'
-class ImageRotationStmt(ParamStmt):
- """ IR - Image Rotation Statement. (Deprecated) """
-
- def to_gerber(self, settings):
- return f'%IR{settings.image_rotation}*%'
-
- def __str__(self):
- return '<IR Image Rotation>'
-
-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 '<MI Mirror Image>'
-
-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'<OF Offset a={self.a} b={self.b}>'
-
-
-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 '<SF Scale Factor>'
-
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 '<Coordinate Statement: %s>' % 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
@@ -367,15 +167,6 @@ class Arc(Primitive):
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
in zip(self.start, self.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<comment>[^*]*)(\*)?",
'format_spec': r"FS(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*",
'load_polarity': r"LP(?P<polarity>(D|C))",
+ # FIXME LM, LR, LS
'load_name': r"LN(?P<name>.*)",
'offset': fr"OF(A(?P<a>{DECIMAL}))?(B(?P<b>{DECIMAL}))?",
'include_file': r"IF(?P<filename>.*)",
@@ -271,8 +323,8 @@ class GerberParser:
'scale_factor': fr"SF(A(?P<a>{DECIMAL}))?(B(?P<b>{DECIMAL}))?",
'aperture_definition': fr"ADD(?P<number>\d+)(?P<shape>C|R|O|P|{NAME})[,]?(?P<modifiers>[^,%]*)",
'aperture_macro': fr"AM(?P<name>{NAME})\*(?P<macro>[^%]*)",
- 'region_mode': r'(?P<mode>G3[67])\*',
- 'quadrant_mode': r'(?P<mode>G7[45])\*',
+ 'region_start': r'G36\*',
+ 'region_end': r'G37\*',
'old_unit':r'(?P<mode>G7[01])\*',
'old_notation': r'(?P<mode>G9[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[minxp][0]):
- minxp=i;
- 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(sdiff<pi):sdiff+=2*pi
- if(sdiff>pi):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]