Refactor panelize/rx274x, refactor bounding_box return value

This commit is contained in:
jaseg 2021-07-04 20:56:09 +02:00
parent e38a983c39
commit dd8944709c
7 changed files with 316 additions and 428 deletions

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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))

View file

@ -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