WIP
This commit is contained in:
parent
125eb821b9
commit
d21a2e67ff
8 changed files with 206 additions and 2033 deletions
|
|
@ -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)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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']:
|
||||
|
|
|
|||
|
|
@ -30,48 +30,23 @@ from .primitives import AMGroup
|
|||
|
||||
|
||||
class Statement:
|
||||
""" Gerber statement Base class
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
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 = ''
|
||||
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 + '*'
|
||||
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_offset=0, y_offset=0):
|
||||
def offset(self, x=0, y=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
|
||||
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]>'
|
||||
|
||||
return '<Coordinate Statement: %s>' % coord_str
|
||||
def render_primitives(self, state):
|
||||
if state.interpolation_mode == InterpolateStmt:
|
||||
yield Line(state.current_point, (self.x, self.y))
|
||||
|
||||
@property
|
||||
def only_function(self):
|
||||
"""
|
||||
Returns if the statement only set the function.
|
||||
"""
|
||||
|
||||
# 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
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
for thing in chain(self.aperture_macros.values(), self.aperture_defs, self.main_statements):
|
||||
print(thing.to_gerber(self.settings), file=f)
|
||||
yield from self.aperture_macros.values()
|
||||
yield from self.aperture_defs
|
||||
yield from self.main_statements
|
||||
|
||||
print('M02*', file=f)
|
||||
yield EofStmt()
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join(self.generate_statements())
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
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()
|
||||
def _parse_region_start(self, _match):
|
||||
current_region = RegionGroup()
|
||||
|
||||
elif param["param"] == "AM":
|
||||
yield AMParamStmt.from_dict(param, units=self.settings.units)
|
||||
elif param["param"] == "AD":
|
||||
yield ADParamStmt.from_dict(param)
|
||||
|
||||
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":
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue