Refactor panelize/rx274x, refactor bounding_box return value
This commit is contained in:
parent
e38a983c39
commit
dd8944709c
7 changed files with 316 additions and 428 deletions
|
|
@ -1,331 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
from ..cam import FileSettings
|
||||
from .. import rs274x
|
||||
from ..gerber_statements import *
|
||||
from .gerber_statements import AMParamStmt, AMParamStmtEx, ADParamStmtEx
|
||||
from .utility import rotate
|
||||
import re
|
||||
|
||||
def loads(data, filename=None):
|
||||
cls = rs274x.GerberParser
|
||||
cls.SF = \
|
||||
r"(?P<param>SF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=cls.DECIMAL)
|
||||
cls.PARAMS = (cls.FS, cls.MO, cls.LP, cls.AD_CIRCLE,
|
||||
cls.AD_RECT, cls.AD_OBROUND, cls.AD_POLY,
|
||||
cls.AD_MACRO, cls.AM, cls.AS, cls.IF, cls.IN,
|
||||
cls.IP, cls.IR, cls.MI, cls.OF, cls.SF, cls.LN)
|
||||
cls.PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in cls.PARAMS]
|
||||
return cls().parse_raw(data, filename)
|
||||
|
||||
def write_gerber_header(file, settings):
|
||||
file.write('%s\n%s\n%%IPPOS*%%\n' % (
|
||||
MOParamStmt('MO', settings.units).to_gerber(settings),
|
||||
FSParamStmt('FS', settings.zero_suppression,
|
||||
settings.notation, settings.format).to_gerber(settings)))
|
||||
|
||||
class GerberFile(rs274x.GerberFile):
|
||||
@classmethod
|
||||
def from_gerber_file(cls, gerber_file):
|
||||
if not isinstance(gerber_file, rs274x.GerberFile):
|
||||
raise Exception('only gerbonara.gerber.rs274x.GerberFile object is specified')
|
||||
|
||||
return cls(gerber_file.statements, gerber_file.settings, gerber_file.primitives,\
|
||||
gerber_file.apertures, gerber_file.filename)
|
||||
|
||||
def __init__(self, statements, settings, primitives, apertures, filename=None):
|
||||
super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename)
|
||||
self.context = GerberContext.from_settings(self.settings)
|
||||
self.aperture_macros = {}
|
||||
self.aperture_defs = []
|
||||
self.main_statements = []
|
||||
for stmt in self.statements:
|
||||
type, stmts = self.context.normalize_statement(stmt)
|
||||
if type == self.context.TYPE_AM:
|
||||
for mdef in stmts:
|
||||
self.aperture_macros[mdef.name] = mdef
|
||||
elif type == self.context.TYPE_AD:
|
||||
self.aperture_defs.extend(stmts)
|
||||
elif type == self.context.TYPE_MAIN:
|
||||
self.main_statements.extend(stmts)
|
||||
if self.context.angle != 0:
|
||||
self.rotate(self.context.angle)
|
||||
if self.context.is_negative:
|
||||
self.nagate_polarity()
|
||||
self.context.notation = 'absolute'
|
||||
self.context.zeros = 'trailing'
|
||||
|
||||
def write(self, filename=None):
|
||||
self.context.notation = 'absolute'
|
||||
self.context.zeros = 'trailing'
|
||||
self.context.format = self.format
|
||||
self.units = self.units
|
||||
filename=filename if filename is not None else self.filename
|
||||
with open(filename, 'w') as f:
|
||||
write_gerber_header(f, self.context)
|
||||
for macro in self.aperture_macros:
|
||||
f.write(self.aperture_macros[macro].to_gerber(self.context) + '\n')
|
||||
for aperture in self.aperture_defs:
|
||||
f.write(aperture.to_gerber(self.context) + '\n')
|
||||
for statement in self.main_statements:
|
||||
f.write(statement.to_gerber(self.context) + '\n')
|
||||
f.write('M02*\n')
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
for macro in self.aperture_macros:
|
||||
self.aperture_macros[macro].to_inch()
|
||||
for aperture in self.aperture_defs:
|
||||
aperture.to_inch()
|
||||
for statement in self.statements:
|
||||
statement.to_inch()
|
||||
self.units = 'inch'
|
||||
self.context.units = 'inch'
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
for macro in self.aperture_macros:
|
||||
self.aperture_macros[macro].to_metric()
|
||||
for aperture in self.aperture_defs:
|
||||
aperture.to_metric()
|
||||
for statement in self.statements:
|
||||
statement.to_metric()
|
||||
self.units='metric'
|
||||
self.context.units='metric'
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
for statement in self.main_statements:
|
||||
if isinstance(statement, CoordStmt):
|
||||
if statement.x is not None:
|
||||
statement.x += x_offset
|
||||
if statement.y is not None:
|
||||
statement.y += y_offset
|
||||
for primitive in self.primitives:
|
||||
primitive.offset(x_offset, y_offset)
|
||||
|
||||
def rotate(self, angle, center=(0,0)):
|
||||
if angle % 360 == 0:
|
||||
return
|
||||
self._generalize_aperture()
|
||||
last_x = 0
|
||||
last_y = 0
|
||||
last_rx = 0
|
||||
last_ry = 0
|
||||
for name in self.aperture_macros:
|
||||
self.aperture_macros[name].rotate(angle, center)
|
||||
for statement in self.main_statements:
|
||||
if isinstance(statement, CoordStmt) and statement.x != None and statement.y != None:
|
||||
if statement.i != None and statement.j != None:
|
||||
cx = last_x + statement.i
|
||||
cy = last_y + statement.j
|
||||
cx, cy = rotate(cx, cy, angle, center)
|
||||
statement.i = cx - last_rx
|
||||
statement.j = cy - last_ry
|
||||
last_x = statement.x
|
||||
last_y = statement.y
|
||||
last_rx, last_ry = rotate(statement.x, statement.y, angle, center)
|
||||
statement.x = last_rx
|
||||
statement.y = last_ry
|
||||
|
||||
def nagate_polarity(self):
|
||||
for statement in self.main_statements:
|
||||
if isinstance(statement, LPParamStmt):
|
||||
statement.lp = 'dark' if statement.lp == 'clear' else 'clear'
|
||||
|
||||
def _generalize_aperture(self):
|
||||
RECTANGLE = 0
|
||||
LANDSCAPE_OBROUND = 1
|
||||
PORTRATE_OBROUND = 2
|
||||
POLYGON = 3
|
||||
macro_defs = [
|
||||
('MACR', AMParamStmtEx.rectangle),
|
||||
('MACLO', AMParamStmtEx.landscape_obround),
|
||||
('MACPO', AMParamStmtEx.portrate_obround),
|
||||
('MACP', AMParamStmtEx.polygon)
|
||||
]
|
||||
|
||||
need_to_change = False
|
||||
for statement in self.aperture_defs:
|
||||
if isinstance(statement, ADParamStmt) and statement.shape in ['R', 'O', 'P']:
|
||||
need_to_change = True
|
||||
|
||||
if need_to_change:
|
||||
for idx in range(0, len(macro_defs)):
|
||||
macro_def = macro_defs[idx]
|
||||
name = macro_def[0]
|
||||
num = 1
|
||||
while name in self.aperture_macros:
|
||||
name = '%s_%d' % (macro_def[0], num)
|
||||
num += 1
|
||||
self.aperture_macros[name] = macro_def[1](name, self.units)
|
||||
macro_defs[idx] = (name, macro_def[1])
|
||||
for statement in self.aperture_defs:
|
||||
if isinstance(statement, ADParamStmt):
|
||||
if statement.shape == 'R':
|
||||
statement.shape = macro_defs[RECTANGLE][0]
|
||||
elif statement.shape == 'O':
|
||||
x = statement.modifiers[0][0] \
|
||||
if len(statement.modifiers[0]) > 0 else 0
|
||||
y = statement.modifiers[0][1] \
|
||||
if len(statement.modifiers[0]) > 1 else 0
|
||||
statement.shape = macro_defs[LANDSCAPE_OBROUND][0] \
|
||||
if x > y else macro_defs[PORTRATE_OBROUND][0]
|
||||
elif statement.shape == 'P':
|
||||
statement.shape = macro_defs[POLYGON][0]
|
||||
|
||||
class GerberContext(FileSettings):
|
||||
TYPE_NONE = 'none'
|
||||
TYPE_AM = 'am'
|
||||
TYPE_AD = 'ad'
|
||||
TYPE_MAIN = 'main'
|
||||
IP_LINEAR = 'lenear'
|
||||
IP_ARC = 'arc'
|
||||
DIR_CLOCKWISE = 'cw'
|
||||
DIR_COUNTERCLOCKWISE = 'ccw'
|
||||
|
||||
ignored_stmt = ('FSParamStmt', 'MOParamStmt', 'ASParamStmt',
|
||||
'INParamStmt', 'IPParamStmt', 'IRParamStmt',
|
||||
'MIParamStmt', 'OFParamStmt', 'SFParamStmt',
|
||||
'LNParamStmt', 'CommentStmt', 'EofStmt',)
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings):
|
||||
return cls(settings.notation, settings.units, settings.zero_suppression,
|
||||
settings.format, settings.zeros, settings.angle_units)
|
||||
|
||||
def __init__(self, notation='absolute', units='inch',
|
||||
zero_suppression=None, format=(2, 5), zeros=None,
|
||||
angle_units='degrees',
|
||||
name=None,
|
||||
mirror=(False, False), offset=(0., 0.), scale=(1., 1.),
|
||||
angle=0., axis='xy'):
|
||||
super(GerberContext, self).__init__(notation, units, zero_suppression,
|
||||
format, zeros, angle_units)
|
||||
self.name = name
|
||||
self.mirror = mirror
|
||||
self.offset = offset
|
||||
self.scale = scale
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
|
||||
self.matrix = (1, 0,
|
||||
1, 0,
|
||||
1, 1)
|
||||
|
||||
self.is_negative = False
|
||||
self.is_first_coordinate = True
|
||||
self.no_polarity = True
|
||||
self.in_single_quadrant_mode = False
|
||||
self.op = None
|
||||
self.interpolation = self.IP_LINEAR
|
||||
self.direction = self.DIR_CLOCKWISE
|
||||
self.x = 0.
|
||||
self.y = 0.
|
||||
|
||||
def normalize_statement(self, stmt):
|
||||
additional_stmts = None
|
||||
if isinstance(stmt, INParamStmt):
|
||||
self.name = stmt.name
|
||||
elif isinstance(stmt, MIParamStmt):
|
||||
self.mirror = (stmt.a, stmt.b)
|
||||
self._update_matrix()
|
||||
elif isinstance(stmt, OFParamStmt):
|
||||
self.offset = (stmt.a, stmt.b)
|
||||
self._update_matrix()
|
||||
elif isinstance(stmt, SFParamStmt):
|
||||
self.scale = (stmt.a, stmt.b)
|
||||
self._update_matrix()
|
||||
elif isinstance(stmt, ASParamStmt):
|
||||
self.axis = 'yx' if stmt.mode == 'AYBX' else 'xy'
|
||||
self._update_matrix()
|
||||
elif isinstance(stmt, IRParamStmt):
|
||||
self.angle = stmt.angle
|
||||
elif isinstance(stmt, AMParamStmt) and not isinstance(stmt, AMParamStmtEx):
|
||||
stmt = AMParamStmtEx.from_stmt(stmt)
|
||||
return (self.TYPE_AM, [stmt])
|
||||
elif isinstance(stmt, ADParamStmt) and not isinstance(stmt, AMParamStmtEx):
|
||||
stmt = ADParamStmtEx.from_stmt(stmt)
|
||||
return (self.TYPE_AD, [stmt])
|
||||
elif isinstance(stmt, QuadrantModeStmt):
|
||||
self.in_single_quadrant_mode = stmt.mode == 'single-quadrant'
|
||||
stmt.mode = 'multi-quadrant'
|
||||
elif isinstance(stmt, IPParamStmt):
|
||||
self.is_negative = stmt.ip == 'negative'
|
||||
elif isinstance(stmt, LPParamStmt):
|
||||
self.no_polarity = False
|
||||
elif isinstance(stmt, CoordStmt):
|
||||
self._normalize_coordinate(stmt)
|
||||
if self.is_first_coordinate:
|
||||
self.is_first_coordinate = False
|
||||
if self.no_polarity:
|
||||
additional_stmts = [LPParamStmt('LP', 'dark'), stmt]
|
||||
|
||||
if type(stmt).__name__ in self.ignored_stmt:
|
||||
return (self.TYPE_NONE, None)
|
||||
elif additional_stmts is not None:
|
||||
return (self.TYPE_MAIN, additional_stmts)
|
||||
else:
|
||||
return (self.TYPE_MAIN, [stmt])
|
||||
|
||||
def _update_matrix(self):
|
||||
if self.axis == 'xy':
|
||||
mx = -1 if self.mirror[0] else 1
|
||||
my = -1 if self.mirror[1] else 1
|
||||
self.matrix = (
|
||||
self.scale[0] * mx, self.offset[0],
|
||||
self.scale[1] * my, self.offset[1],
|
||||
self.scale[0] * mx, self.scale[1] * my)
|
||||
else:
|
||||
mx = -1 if self.mirror[1] else 1
|
||||
my = -1 if self.mirror[0] else 1
|
||||
self.matrix = (
|
||||
self.scale[1] * mx, self.offset[1],
|
||||
self.scale[0] * my, self.offset[0],
|
||||
self.scale[1] * mx, self.scale[0] * my)
|
||||
|
||||
def _normalize_coordinate(self, stmt):
|
||||
if stmt.function == 'G01' or stmt.function == 'G1':
|
||||
self.interpolation = self.IP_LINEAR
|
||||
elif stmt.function == 'G02' or stmt.function == 'G2':
|
||||
self.interpolation = self.IP_ARC
|
||||
self.direction = self.DIR_CLOCKWISE
|
||||
if self.mirror[0] != self.mirror[1]:
|
||||
stmt.function = 'G03'
|
||||
elif stmt.function == 'G03' or stmt.function == 'G3':
|
||||
self.interpolation = self.IP_ARC
|
||||
self.direction = self.DIR_COUNTERCLOCKWISE
|
||||
if self.mirror[0] != self.mirror[1]:
|
||||
stmt.function = 'G02'
|
||||
if stmt.only_function:
|
||||
return
|
||||
|
||||
last_x = self.x
|
||||
last_y = self.y
|
||||
if self.notation == 'absolute':
|
||||
x = stmt.x if stmt.x is not None else self.x
|
||||
y = stmt.y if stmt.y is not None else self.y
|
||||
else:
|
||||
x = self.x + stmt.x if stmt.x is not None else 0
|
||||
y = self.y + stmt.y if stmt.y is not None else 0
|
||||
self.x, self.y = x, y
|
||||
self.op = stmt.op if stmt.op is not None else self.op
|
||||
|
||||
stmt.op = self.op
|
||||
stmt.x = self.matrix[0] * x + self.matrix[1]
|
||||
stmt.y = self.matrix[2] * y + self.matrix[3]
|
||||
if stmt.op == 'D01' and self.interpolation == self.IP_ARC:
|
||||
qx, qy = 1, 1
|
||||
if self.in_single_quadrant_mode:
|
||||
if self.direction == self.DIR_CLOCKWISE:
|
||||
qx = 1 if y > last_y else -1
|
||||
qy = 1 if x < last_x else -1
|
||||
else:
|
||||
qx = 1 if y < last_y else -1
|
||||
qy = 1 if x > last_x else -1
|
||||
if last_x == x and last_y == y:
|
||||
qx, qy = 0, 0
|
||||
stmt.i = qx * self.matrix[4] * stmt.i if stmt.i is not None else 0
|
||||
stmt.j = qy * self.matrix[5] * stmt.j if stmt.j is not None else 0
|
||||
|
|
@ -5,12 +5,13 @@
|
|||
|
||||
from math import cos, sin, pi, sqrt
|
||||
|
||||
def rotate(x, y, angle, center):
|
||||
x0 = x - center[0]
|
||||
y0 = y - center[1]
|
||||
angle = angle * pi / 180.0
|
||||
return (cos(angle) * x0 - sin(angle) * y0 + center[0],
|
||||
sin(angle) * x0 + cos(angle) * y0 + center[1])
|
||||
# TODO: replace with ..utils.rotate
|
||||
#def rotate(x, y, angle, center):
|
||||
# x0 = x - center[0]
|
||||
# y0 = y - center[1]
|
||||
# angle = angle * pi / 180.0
|
||||
# return (cos(angle) * x0 - sin(angle) * y0 + center[0],
|
||||
# sin(angle) * x0 + cos(angle) * y0 + center[1])
|
||||
|
||||
def is_equal_value(a, b, error_range=0):
|
||||
return (a - b) * (a - b) <= error_range * error_range
|
||||
|
|
@ -24,4 +25,4 @@ def normalize_vec2d(vec):
|
|||
return (vec[0] / length, vec[1] / length)
|
||||
|
||||
def dot_vec2d(vec1, vec2):
|
||||
return vec1[0] * vec2[0] + vec1[1] * vec2[1]
|
||||
return vec1[0] * vec2[0] + vec1[1] * vec2[1]
|
||||
|
|
|
|||
|
|
@ -118,7 +118,9 @@ class PCB(object):
|
|||
def board_bounds(self):
|
||||
for layer in self.layers:
|
||||
if layer.layer_class == 'outline':
|
||||
return layer.bounds
|
||||
return layer.bounding_box
|
||||
|
||||
for layer in self.layers:
|
||||
if layer.layer_class == 'top':
|
||||
return layer.bounds
|
||||
return layer.bounding_box
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class Primitive(object):
|
|||
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, max x), (min y, max y))
|
||||
Return ((min x, min y), (max x, max y))
|
||||
"""
|
||||
return self.bounding_box
|
||||
|
||||
|
|
@ -154,8 +154,7 @@ class Primitive(object):
|
|||
"""
|
||||
if self.units == 'inch':
|
||||
self.units = 'metric'
|
||||
for attr, value in [(attr, getattr(self, attr))
|
||||
for attr in self._to_convert]:
|
||||
for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]:
|
||||
if hasattr(value, 'to_metric'):
|
||||
value.to_metric()
|
||||
else:
|
||||
|
|
@ -256,7 +255,7 @@ class Line(Primitive):
|
|||
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, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -266,7 +265,7 @@ class Line(Primitive):
|
|||
max_x = max(self.start[0], self.end[0])
|
||||
min_y = min(self.start[1], self.end[1])
|
||||
max_y = max(self.start[1], self.end[1])
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
return ((min_x, min_y), (max_x, max_y))
|
||||
|
||||
@property
|
||||
def vertices(self):
|
||||
|
|
@ -457,7 +456,7 @@ class Arc(Primitive):
|
|||
min_y = min(y) - self.aperture.height
|
||||
max_y = max(y) + self.aperture.height
|
||||
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -510,7 +509,7 @@ class Arc(Primitive):
|
|||
max_x = max(x)
|
||||
min_y = min(y)
|
||||
max_y = max(y)
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
return ((min_x, min_y), (max_x, max_y))
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self._changed()
|
||||
|
|
@ -573,7 +572,7 @@ class Circle(Primitive):
|
|||
max_x = self.position[0] + self.radius
|
||||
min_y = self.position[1] - self.radius
|
||||
max_y = self.position[1] + self.radius
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
|
|
@ -642,7 +641,7 @@ class Ellipse(Primitive):
|
|||
max_x = self.position[0] + (self.axis_aligned_width / 2.0)
|
||||
min_y = self.position[1] - (self.axis_aligned_height / 2.0)
|
||||
max_y = self.position[1] + (self.axis_aligned_height / 2.0)
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -739,7 +738,7 @@ class Rectangle(Primitive):
|
|||
self.position[1] - (self.axis_aligned_height / 2.))
|
||||
ur = (self.position[0] + (self.axis_aligned_width / 2.),
|
||||
self.position[1] + (self.axis_aligned_height / 2.))
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
self._bounding_box = ((ll[0], ll[1]), (ur[0], ur[1]))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -834,7 +833,7 @@ class Diamond(Primitive):
|
|||
self.position[1] - (self.axis_aligned_height / 2.))
|
||||
ur = (self.position[0] + (self.axis_aligned_width / 2.),
|
||||
self.position[1] + (self.axis_aligned_height / 2.))
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
self._bounding_box = ((ll[0], ll[1]), (ur[0], ur[1]))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -929,7 +928,7 @@ class ChamferRectangle(Primitive):
|
|||
self.position[1] - (self.axis_aligned_height / 2.))
|
||||
ur = (self.position[0] + (self.axis_aligned_width / 2.),
|
||||
self.position[1] + (self.axis_aligned_height / 2.))
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
self._bounding_box = ((ll[0], ll[1]), (ur[1], ur[1]))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -1049,7 +1048,7 @@ class RoundRectangle(Primitive):
|
|||
self.position[1] - (self.axis_aligned_height / 2.))
|
||||
ur = (self.position[0] + (self.axis_aligned_width / 2.),
|
||||
self.position[1] + (self.axis_aligned_height / 2.))
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
self._bounding_box = ((ll[0], ll[1]), (ur[0], ur[1]))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -1127,7 +1126,7 @@ class Obround(Primitive):
|
|||
self.position[1] - (self.axis_aligned_height / 2.))
|
||||
ur = (self.position[0] + (self.axis_aligned_width / 2.),
|
||||
self.position[1] + (self.axis_aligned_height / 2.))
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
self._bounding_box = ((ll[0], ll[1]), (ur[0], ur[1]))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
|
|
@ -1217,7 +1216,7 @@ class Polygon(Primitive):
|
|||
max_x = self.position[0] + self.radius
|
||||
min_y = self.position[1] - self.radius
|
||||
max_y = self.position[1] + self.radius
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
|
|
@ -1401,7 +1400,7 @@ class Outline(Primitive):
|
|||
@property
|
||||
def width(self):
|
||||
bounding_box = self.bounding_box()
|
||||
return bounding_box[0][1] - bounding_box[0][0]
|
||||
return bounding_box[1][0] - bounding_box[0][0]
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
'''
|
||||
|
|
@ -1441,7 +1440,7 @@ class Region(Primitive):
|
|||
max_x = max(maxx)
|
||||
min_y = min(miny)
|
||||
max_y = max(maxy)
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
|
|
@ -1478,7 +1477,7 @@ class RoundButterfly(Primitive):
|
|||
max_x = self.position[0] + self.radius
|
||||
min_y = self.position[1] - self.radius
|
||||
max_y = self.position[1] + self.radius
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
|
||||
|
|
@ -1506,7 +1505,7 @@ class SquareButterfly(Primitive):
|
|||
max_x = self.position[0] + (self.side / 2.)
|
||||
min_y = self.position[1] - (self.side / 2.)
|
||||
max_y = self.position[1] + (self.side / 2.)
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
|
||||
|
|
@ -1562,7 +1561,7 @@ class Donut(Primitive):
|
|||
self.position[1] - (self.height / 2.))
|
||||
ur = (self.position[0] + (self.width / 2.),
|
||||
self.position[1] + (self.height / 2.))
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
self._bounding_box = (ll, ur)
|
||||
return self._bounding_box
|
||||
|
||||
|
||||
|
|
@ -1590,7 +1589,7 @@ class SquareRoundDonut(Primitive):
|
|||
if self._bounding_box is None:
|
||||
ll = tuple([c - self.outer_diameter / 2. for c in self.position])
|
||||
ur = tuple([c + self.outer_diameter / 2. for c in self.position])
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
self._bounding_box = (ll, ur)
|
||||
return self._bounding_box
|
||||
|
||||
|
||||
|
|
@ -1637,7 +1636,7 @@ class Drill(Primitive):
|
|||
max_x = self.position[0] + self.radius
|
||||
min_y = self.position[1] - self.radius
|
||||
max_y = self.position[1] + self.radius
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
|
|
@ -1673,7 +1672,7 @@ class Slot(Primitive):
|
|||
max_x = max(self.start[0], self.end[0]) + radius
|
||||
min_y = min(self.start[1], self.end[1]) - radius
|
||||
max_y = max(self.start[1], self.end[1]) + radius
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
self._bounding_box = ((min_x, min_y), (max_x, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
|
|
|
|||
|
|
@ -608,9 +608,7 @@ class GerberCairoContext(GerberContext):
|
|||
"""
|
||||
class Clip:
|
||||
def __init__(clp, primitive):
|
||||
x_range, y_range = primitive.bounding_box
|
||||
xmin, xmax = x_range
|
||||
ymin, ymax = y_range
|
||||
(xmin, ymin), (xmax, ymax) = primitive.bounding_box
|
||||
|
||||
# Round bounds to the nearest pixel outside of the primitive
|
||||
clp.xmin = math.floor(self.scale[0] * xmin)
|
||||
|
|
|
|||
|
|
@ -408,8 +408,8 @@ class Rs274xContext(GerberContext):
|
|||
hash += primitive.__class__.__name__[0]
|
||||
|
||||
bbox = primitive.bounding_box
|
||||
hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2]
|
||||
hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2]
|
||||
hash += str((bbox[1][0] - bbox[0][0]) * 100000)[0:2]
|
||||
hash += str((bbox[1][1] - bbox[0][1]) * 100000)[0:2]
|
||||
|
||||
if hasattr(primitive, 'primitives'):
|
||||
hash += str(len(primitive.primitives))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
# Copyright 2021 Jan Götte <code@jaseg.de>
|
||||
# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
|
@ -23,6 +25,7 @@ import json
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from itertools import count, chain
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
|
|
@ -32,7 +35,7 @@ except(ImportError):
|
|||
from .gerber_statements import *
|
||||
from .primitives import *
|
||||
from .cam import CamFile, FileSettings
|
||||
from .utils import sq_distance
|
||||
from .utils import sq_distance, rotate_point
|
||||
|
||||
|
||||
def read(filename):
|
||||
|
|
@ -105,76 +108,166 @@ class GerberFile(CamFile):
|
|||
|
||||
self.apertures = apertures
|
||||
|
||||
# always explicitly set polarity
|
||||
self.statements.insert(0, LPParamStmt('LP', 'dark'))
|
||||
|
||||
self.aperture_macros = {}
|
||||
self.aperture_defs = []
|
||||
self.main_statements = []
|
||||
|
||||
self.context = GerberContext.from_settings(self.settings)
|
||||
|
||||
for stmt in self.statements:
|
||||
self.context.update_from_statement(stmt)
|
||||
|
||||
if isinstance(stmt, CoordStmt):
|
||||
self.context.normalize_coordinates(stmt)
|
||||
|
||||
if isinstance(stmt, AMParamStmt):
|
||||
for mdef in stmts:
|
||||
self.aperture_macros[mdef.name] = mdef
|
||||
|
||||
elif isinstance(stmt, ADParamStmt):
|
||||
self.aperture_defs.extend(stmts)
|
||||
|
||||
else:
|
||||
# ignore FS, MO, AS, IN, IP, IR, MI, OF, SF, LN statements
|
||||
if isinstance(stmt, ParamStmt) and not isinstance(stmt, LPParamStmt):
|
||||
continue
|
||||
|
||||
if isinstance(stmt, (CommentStmt, EofStmt)):
|
||||
continue
|
||||
|
||||
self.main_statements.extend(stmts)
|
||||
|
||||
if self.context.angle != 0:
|
||||
self.rotate(self.context.angle) # TODO is this correct/useful?
|
||||
|
||||
if self.context.is_negative:
|
||||
self.negate_polarity() # TODO is this correct/useful?
|
||||
|
||||
self.context.notation = 'absolute'
|
||||
self.context.zeros = 'trailing'
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return [comment.comment for comment in self.statements
|
||||
if isinstance(comment, CommentStmt)]
|
||||
return [comment.comment for comment in self.statements if isinstance(comment, CommentStmt)]
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
xbounds, ybounds = self.bounds
|
||||
return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0])
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
min_x = min_y = 1000000
|
||||
max_x = max_y = -1000000
|
||||
|
||||
for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]:
|
||||
if stmt.x is not None:
|
||||
min_x = min(stmt.x, min_x)
|
||||
max_x = max(stmt.x, max_x)
|
||||
|
||||
if stmt.y is not None:
|
||||
min_y = min(stmt.y, min_y)
|
||||
max_y = max(stmt.y, max_y)
|
||||
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
(x0, y0), (x1, y1)= self.bounding_box
|
||||
return (x1 - x0, y1 - y0)
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = min_y = 1000000
|
||||
max_x = max_y = -1000000
|
||||
bounds = [ p.bounding_box for p in self.primitives ]
|
||||
|
||||
for prim in self.primitives:
|
||||
bounds = prim.bounding_box
|
||||
min_x = min(bounds[0][0], min_x)
|
||||
max_x = max(bounds[0][1], max_x)
|
||||
|
||||
min_y = min(bounds[1][0], min_y)
|
||||
max_y = max(bounds[1][1], max_y)
|
||||
min_x = min(x0 for (x0, y0), (x1, y1) in bounds)
|
||||
min_y = min(y0 for (x0, y0), (x1, y1) in bounds)
|
||||
max_x = max(x1 for (x0, y0), (x1, y1) in bounds)
|
||||
max_y = max(y1 for (x0, y0), (x1, y1) in bounds)
|
||||
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def write(self, filename, settings=None):
|
||||
""" Write data out to a gerber file.
|
||||
"""
|
||||
with open(filename, 'w') as f:
|
||||
for statement in self.statements:
|
||||
f.write(statement.to_gerber(settings or self.settings))
|
||||
f.write("\n")
|
||||
# TODO: re-add settings arg
|
||||
def write(self, filename=None):
|
||||
self.context.notation = 'absolute'
|
||||
self.context.zeros = 'trailing'
|
||||
self.context.format = self.format
|
||||
self.units = self.units
|
||||
|
||||
with open(filename or self.filename, 'w') as f:
|
||||
print(MOParamStmt('MO', self.context.units).to_gerber(self.context), file=f)
|
||||
print(FSParamStmt('FS', self.context.zero_suppression, self.context.notation, self.context.format).to_gerber(self.context), file=f)
|
||||
print('%IPPOS*%', file=f)
|
||||
|
||||
for thing in chain(self.aperture_macros.values(), self.aperture_defs, self.main_statements):
|
||||
print(thing.to_gerber(self.context), file=f)
|
||||
|
||||
print('M02*', file=f)
|
||||
|
||||
def to_inch(self):
|
||||
if self.units != 'inch':
|
||||
if self.units == 'metric':
|
||||
for thing in chain(self.aperture_macros.values(), self.aperture_defs, self.statements, self.primitives):
|
||||
thing.to_inch()
|
||||
self.units = 'inch'
|
||||
for statement in self.statements:
|
||||
statement.to_inch()
|
||||
for primitive in self.primitives:
|
||||
primitive.to_inch()
|
||||
self.context.units = 'inch'
|
||||
|
||||
def to_metric(self):
|
||||
if self.units != 'metric':
|
||||
self.units = 'metric'
|
||||
for statement in self.statements:
|
||||
statement.to_metric()
|
||||
for primitive in self.primitives:
|
||||
primitive.to_metric()
|
||||
if self.units == 'inch':
|
||||
for thing in chain(self.aperture_macros.values(), self.aperture_defs, self.statements, self.primitives):
|
||||
thing.to_metric()
|
||||
self.units='metric'
|
||||
self.context.units='metric'
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
for statement in self.statements:
|
||||
statement.offset(x_offset, y_offset)
|
||||
for primitive in self.primitives:
|
||||
primitive.offset(x_offset, y_offset)
|
||||
for thing in chain(self.main_statements, self.primitives):
|
||||
thing.offset(x_offset, y_offset)
|
||||
|
||||
def rotate(self, angle, center=(0,0)):
|
||||
if angle % 360 == 0:
|
||||
return
|
||||
|
||||
self._generalize_apertures()
|
||||
|
||||
last_x = 0
|
||||
last_y = 0
|
||||
last_rx = 0
|
||||
last_ry = 0
|
||||
|
||||
for macro in self.aperture_macros.values():
|
||||
macro.rotate(angle, center)
|
||||
|
||||
for statement in self.main_statements:
|
||||
if isinstance(statement, CoordStmt) and statement.x != None and statement.y != None:
|
||||
|
||||
if statement.i is not None and statement.j is not None:
|
||||
cx, cy = last_x + statement.i, last_y + statement.j
|
||||
cx, cy = rotate_point((cx, cy), angle, center)
|
||||
statement.i, statement.j = cx - last_rx, cy - last_ry
|
||||
|
||||
last_x, last_y = statement.x, statement.y
|
||||
last_rx, last_ry = rotate_point((statement.x, statement.y), angle, center)
|
||||
statement.x, statement.y = last_rx, last_ry
|
||||
|
||||
def negate_polarity(self):
|
||||
for statement in self.main_statements:
|
||||
if isinstance(statement, LPParamStmt):
|
||||
statement.lp = 'dark' if statement.lp == 'clear' else 'clear'
|
||||
|
||||
def _generalize_apertures(self):
|
||||
# For rotation, replace standard apertures with macro apertures.
|
||||
|
||||
if not any(isinstance(stm, ADParamStmt) and stm.shape in 'ROP' for stm in self.aperture_defs):
|
||||
return
|
||||
|
||||
# find an unused macro name with the given prefix
|
||||
def free_name(prefix):
|
||||
return next(f'{prefix}_{i}' for i in count() if f'{prefix}_{i}' not in self.aperture_macros)
|
||||
|
||||
rect = free_name('MACR')
|
||||
self.aperture_macros[rect] = AMParamStmtEx.rectangle(rect, self.units)
|
||||
|
||||
obround_landscape = free_name('MACLO')
|
||||
self.aperture_macros[obround_landscape] = AMParamStmtEx.landscape_obround(obround_landscape, self.units)
|
||||
|
||||
obround_portrait = free_name('MACPO')
|
||||
self.aperture_macros[obround_portrait] = AMParamStmtEx.portrait_obround(obround_portrait, self.units)
|
||||
|
||||
polygon = free_name('MACP')
|
||||
self.aperture_macros[polygon] = AMParamStmtEx.polygon(polygon, self.units)
|
||||
|
||||
for statement in self.aperture_defs:
|
||||
if isinstance(statement, ADParamStmt):
|
||||
if statement.shape == 'R':
|
||||
statement.shape = rect
|
||||
|
||||
elif statement.shape == 'O':
|
||||
x, y, *_ = *statement.modifiers[0], 0, 0
|
||||
statement.shape = obround_landscape if x > y else obround_portrait
|
||||
|
||||
elif statement.shape == 'P':
|
||||
statement.shape = polygon
|
||||
|
||||
|
||||
class GerberParser(object):
|
||||
|
|
@ -205,7 +298,7 @@ class GerberParser(object):
|
|||
IR = r"(?P<param>IR)(?P<angle>{number})".format(number=NUMBER)
|
||||
MI = r"(?P<param>MI)(A(?P<a>0|1))?(B(?P<b>0|1))?"
|
||||
OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL)
|
||||
SF = r"(?P<param>SF)(?P<discarded>.*)"
|
||||
SF = r"(?P<param>SF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=cls.DECIMAL)
|
||||
LN = r"(?P<param>LN)(?P<name>.*)"
|
||||
DEPRECATED_UNIT = re.compile(r'(?P<mode>G7[01])\*')
|
||||
DEPRECATED_FORMAT = re.compile(r'(?P<format>G9[01])\*')
|
||||
|
|
@ -308,14 +401,10 @@ class GerberParser(object):
|
|||
in_header = False
|
||||
|
||||
def dump_json(self):
|
||||
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
|
||||
return json.dumps(stmts)
|
||||
return json.dumps({"statements": [stmt.__dict__ for stmt in self.statements]})
|
||||
|
||||
def dump_str(self):
|
||||
string = ""
|
||||
for stmt in self.statements:
|
||||
string += str(stmt) + "\n"
|
||||
return string
|
||||
return '\n'.join(str(stmt) for stmt in self.statements) + '\n'
|
||||
|
||||
def _parse(self, data):
|
||||
oldline = ''
|
||||
|
|
@ -798,3 +887,133 @@ def _match_one_from_many(exprs, data):
|
|||
return (match.groupdict(), data[match.end(0):])
|
||||
|
||||
return ({}, None)
|
||||
|
||||
class GerberContext(FileSettings):
|
||||
TYPE_NONE = 'none'
|
||||
TYPE_AM = 'am'
|
||||
TYPE_AD = 'ad'
|
||||
TYPE_MAIN = 'main'
|
||||
IP_LINEAR = 'linear'
|
||||
IP_ARC = 'arc'
|
||||
DIR_CLOCKWISE = 'cw'
|
||||
DIR_COUNTERCLOCKWISE = 'ccw'
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings):
|
||||
return cls(settings.notation, settings.units, settings.zero_suppression,
|
||||
settings.format, settings.zeros, settings.angle_units)
|
||||
|
||||
def __init__(self, notation='absolute', units='inch',
|
||||
zero_suppression=None, format=(2, 5), zeros=None,
|
||||
angle_units='degrees',
|
||||
mirror=(False, False), offset=(0., 0.), scale=(1., 1.),
|
||||
angle=0., axis='xy'):
|
||||
super(GerberContext, self).__init__(notation, units, zero_suppression,
|
||||
format, zeros, angle_units)
|
||||
self.mirror = mirror
|
||||
self.offset = offset
|
||||
self.scale = scale
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
|
||||
self.is_negative = False
|
||||
self.no_polarity = True
|
||||
self.in_single_quadrant_mode = False
|
||||
self.op = None
|
||||
self.interpolation = self.IP_LINEAR
|
||||
self.direction = self.DIR_CLOCKWISE
|
||||
self.x, self.y = 0, 0
|
||||
|
||||
def update_from_statement(self, stmt):
|
||||
elif isinstance(stmt, MIParamStmt):
|
||||
self.mirror = (stmt.a, stmt.b)
|
||||
|
||||
elif isinstance(stmt, OFParamStmt):
|
||||
self.offset = (stmt.a, stmt.b)
|
||||
|
||||
elif isinstance(stmt, SFParamStmt):
|
||||
self.scale = (stmt.a, stmt.b)
|
||||
|
||||
elif isinstance(stmt, ASParamStmt):
|
||||
self.axis = 'yx' if stmt.mode == 'AYBX' else 'xy'
|
||||
|
||||
elif isinstance(stmt, IRParamStmt):
|
||||
self.angle = stmt.angle
|
||||
|
||||
elif isinstance(stmt, QuadrantModeStmt):
|
||||
self.in_single_quadrant_mode = stmt.mode == 'single-quadrant'
|
||||
stmt.mode = 'multi-quadrant'
|
||||
|
||||
elif isinstance(stmt, IPParamStmt):
|
||||
self.is_negative = stmt.ip == 'negative'
|
||||
|
||||
elif isinstance(stmt, LPParamStmt):
|
||||
self.no_polarity = False
|
||||
|
||||
@property
|
||||
def matrix(self):
|
||||
if self.axis == 'xy':
|
||||
mx = -1 if self.mirror[0] else 1
|
||||
my = -1 if self.mirror[1] else 1
|
||||
return (
|
||||
self.scale[0] * mx, self.offset[0],
|
||||
self.scale[1] * my, self.offset[1],
|
||||
self.scale[0] * mx, self.scale[1] * my)
|
||||
else:
|
||||
mx = -1 if self.mirror[1] else 1
|
||||
my = -1 if self.mirror[0] else 1
|
||||
return (
|
||||
self.scale[1] * mx, self.offset[1],
|
||||
self.scale[0] * my, self.offset[0],
|
||||
self.scale[1] * mx, self.scale[0] * my)
|
||||
|
||||
def normalize_coordinates(self, stmt):
|
||||
if stmt.function == 'G01' or stmt.function == 'G1':
|
||||
self.interpolation = self.IP_LINEAR
|
||||
|
||||
elif stmt.function == 'G02' or stmt.function == 'G2':
|
||||
self.interpolation = self.IP_ARC
|
||||
self.direction = self.DIR_CLOCKWISE
|
||||
if self.mirror[0] != self.mirror[1]:
|
||||
stmt.function = 'G03'
|
||||
|
||||
elif stmt.function == 'G03' or stmt.function == 'G3':
|
||||
self.interpolation = self.IP_ARC
|
||||
self.direction = self.DIR_COUNTERCLOCKWISE
|
||||
if self.mirror[0] != self.mirror[1]:
|
||||
stmt.function = 'G02'
|
||||
|
||||
if stmt.only_function:
|
||||
return
|
||||
|
||||
last_x, last_y = self.x, self.y
|
||||
if self.notation == 'absolute':
|
||||
x = stmt.x if stmt.x is not None else self.x
|
||||
y = stmt.y if stmt.y is not None else self.y
|
||||
|
||||
else:
|
||||
x = self.x + stmt.x if stmt.x is not None else 0
|
||||
y = self.y + stmt.y if stmt.y is not None else 0
|
||||
|
||||
self.x, self.y = x, y
|
||||
self.op = stmt.op if stmt.op is not None else self.op
|
||||
|
||||
stmt.op = self.op
|
||||
stmt.x = self.matrix[0] * x + self.matrix[1]
|
||||
stmt.y = self.matrix[2] * y + self.matrix[3]
|
||||
|
||||
if stmt.op == 'D01' and self.interpolation == self.IP_ARC:
|
||||
qx, qy = 1, 1
|
||||
if self.in_single_quadrant_mode:
|
||||
if self.direction == self.DIR_CLOCKWISE:
|
||||
qx = 1 if y > last_y else -1
|
||||
qy = 1 if x < last_x else -1
|
||||
else:
|
||||
qx = 1 if y < last_y else -1
|
||||
qy = 1 if x > last_x else -1
|
||||
if last_x == x and last_y == y:
|
||||
qx, qy = 0, 0
|
||||
|
||||
stmt.i = qx * self.matrix[4] * stmt.i if stmt.i is not None else 0
|
||||
stmt.j = qy * self.matrix[5] * stmt.j if stmt.j is not None else 0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue