summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2021-07-04 20:56:09 +0200
committerjaseg <git@jaseg.de>2021-07-04 20:56:09 +0200
commitdd8944709c5a48ccb52ba41a2310218770eb1669 (patch)
tree6a98a25f223cd1297462e0bc3a4f6f64c12fc3e5
parente38a983c398d5577008926f2cc9c0803b99e8fa9 (diff)
downloadgerbonara-dd8944709c5a48ccb52ba41a2310218770eb1669.tar.gz
gerbonara-dd8944709c5a48ccb52ba41a2310218770eb1669.tar.bz2
gerbonara-dd8944709c5a48ccb52ba41a2310218770eb1669.zip
Refactor panelize/rx274x, refactor bounding_box return value
-rw-r--r--gerbonara/gerber/panelize/rs274x.py331
-rw-r--r--gerbonara/gerber/panelize/utility.py15
-rw-r--r--gerbonara/gerber/pcb.py6
-rw-r--r--gerbonara/gerber/primitives.py45
-rw-r--r--gerbonara/gerber/render/cairo_backend.py4
-rw-r--r--gerbonara/gerber/render/rs274x_backend.py4
-rw-r--r--gerbonara/gerber/rs274x.py335
7 files changed, 314 insertions, 426 deletions
diff --git a/gerbonara/gerber/panelize/rs274x.py b/gerbonara/gerber/panelize/rs274x.py
index 2f44cd4..e69de29 100644
--- a/gerbonara/gerber/panelize/rs274x.py
+++ b/gerbonara/gerber/panelize/rs274x.py
@@ -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
diff --git a/gerbonara/gerber/panelize/utility.py b/gerbonara/gerber/panelize/utility.py
index 37de5e8..fc44e79 100644
--- a/gerbonara/gerber/panelize/utility.py
+++ b/gerbonara/gerber/panelize/utility.py
@@ -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] \ No newline at end of file
+ return vec1[0] * vec2[0] + vec1[1] * vec2[1]
diff --git a/gerbonara/gerber/pcb.py b/gerbonara/gerber/pcb.py
index 1d22e74..f00c4b7 100644
--- a/gerbonara/gerber/pcb.py
+++ b/gerbonara/gerber/pcb.py
@@ -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
+
diff --git a/gerbonara/gerber/primitives.py b/gerbonara/gerber/primitives.py
index 7d15e8a..fa08fef 100644
--- a/gerbonara/gerber/primitives.py
+++ b/gerbonara/gerber/primitives.py
@@ -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):
diff --git a/gerbonara/gerber/render/cairo_backend.py b/gerbonara/gerber/render/cairo_backend.py
index a7baf84..431ab94 100644
--- a/gerbonara/gerber/render/cairo_backend.py
+++ b/gerbonara/gerber/render/cairo_backend.py
@@ -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)
diff --git a/gerbonara/gerber/render/rs274x_backend.py b/gerbonara/gerber/render/rs274x_backend.py
index c7af2ea..bf9f164 100644
--- a/gerbonara/gerber/render/rs274x_backend.py
+++ b/gerbonara/gerber/render/rs274x_backend.py
@@ -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))
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py
index 4852ed2..b659f20 100644
--- a/gerbonara/gerber/rs274x.py
+++ b/gerbonara/gerber/rs274x.py
@@ -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])
+ (x0, y0), (x1, y1)= self.bounding_box
+ return (x1 - x0, y1 - y0)
@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)
+ def bounding_box(self):
+ bounds = [ p.bounding_box for p in self.primitives ]
- if stmt.y is not None:
- min_y = min(stmt.y, min_y)
- max_y = max(stmt.y, 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))
- @property
- def bounding_box(self):
- min_x = min_y = 1000000
- max_x = max_y = -1000000
-
- 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)
+ # 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
- min_y = min(bounds[1][0], min_y)
- max_y = max(bounds[1][1], max_y)
+ 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)
- return ((min_x, max_x), (min_y, max_y))
+ for thing in chain(self.aperture_macros.values(), self.aperture_defs, self.main_statements):
+ print(thing.to_gerber(self.context), file=f)
- 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")
+ 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
+