From dd8944709c5a48ccb52ba41a2310218770eb1669 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 4 Jul 2021 20:56:09 +0200 Subject: Refactor panelize/rx274x, refactor bounding_box return value --- gerbonara/gerber/panelize/rs274x.py | 331 ----------------------------- gerbonara/gerber/panelize/utility.py | 15 +- gerbonara/gerber/pcb.py | 6 +- gerbonara/gerber/primitives.py | 45 ++-- gerbonara/gerber/render/cairo_backend.py | 4 +- gerbonara/gerber/render/rs274x_backend.py | 4 +- gerbonara/gerber/rs274x.py | 335 ++++++++++++++++++++++++------ 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 - -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"(?PSF)(A(?P{decimal}))?(B(?P{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 +# Copyright 2014 Hamilton Kibbe +# Copyright 2019 Hiroshi Murayama +# Copyright 2021 Jan Götte # Modified from parser.py by Paulo Henrique Silva # # 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"(?PIR)(?P{number})".format(number=NUMBER) MI = r"(?PMI)(A(?P0|1))?(B(?P0|1))?" OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) - SF = r"(?PSF)(?P.*)" + SF = r"(?PSF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=cls.DECIMAL) LN = r"(?PLN)(?P.*)" DEPRECATED_UNIT = re.compile(r'(?PG7[01])\*') DEPRECATED_FORMAT = re.compile(r'(?PG9[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 + -- cgit