diff options
-rwxr-xr-x | gerbonara/gerber/excellon.py | 7 | ||||
-rw-r--r-- | gerbonara/gerber/gerber_statements.py | 218 | ||||
-rw-r--r-- | gerbonara/gerber/graphic_objects.py | 65 | ||||
-rw-r--r-- | gerbonara/gerber/graphic_primitives.py | 2 | ||||
-rw-r--r-- | gerbonara/gerber/rs274x.py | 64 | ||||
-rw-r--r-- | gerbonara/gerber/utils.py | 8 |
6 files changed, 92 insertions, 272 deletions
diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py index 0129867..e827c3f 100755 --- a/gerbonara/gerber/excellon.py +++ b/gerbonara/gerber/excellon.py @@ -26,7 +26,7 @@ from collections import Counter from .cam import CamFile, FileSettings from .graphic_objects import Flash, Line, Arc from .apertures import ExcellonTool -from .utils import Inch, MM +from .utils import Inch, MM, InterpMode def parse(data, settings=None): return ExcellonFile.parse(data, settings=settings) @@ -208,11 +208,6 @@ class ProgramState(Enum): ROUTING = 2 FINISHED = 2 -class InterpMode(Enum): - LINEAR = 0 - CIRCULAR_CW = 1 - CIRCULAR_CCW = 2 - class ExcellonParser(object): def __init__(self, settings=None): diff --git a/gerbonara/gerber/gerber_statements.py b/gerbonara/gerber/gerber_statements.py deleted file mode 100644 index 62aab3b..0000000 --- a/gerbonara/gerber/gerber_statements.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Gerber (RS-274X) Statements -=========================== -**Gerber RS-274X file statement classes** - -""" - -# FIXME make this entire file obsolete and just return strings from graphical objects directly instead - -class Statement: - pass - -class ParamStmt(Statement): - pass - -class FormatSpecStmt(ParamStmt): - """ FS - Gerber Format Specification Statement """ - - def to_gerber(self, settings): - zeros = 'T' if settings.zeros == 'trailing' else 'L' # default to leading if "None" is specified - notation = 'I' if settings.notation == 'incremental' else 'A' # default to absolute - number_format = str(settings.number_format[0]) + str(settings.number_format[1]) - - return f'%FS{zeros}{notation}X{number_format}Y{number_format}*%' - - def __str__(self): - return '<FS Format Specification>' - - -class UnitStmt(ParamStmt): - """ MO - Coordinate unit mode statement """ - - def to_gerber(self, settings): - return '%MOMM*%' if settings.unit == 'mm' else '%MOIN*%' - - def __str__(self): - return ('<MO Coordinate unit mode statement>' % mode_str) - - -class LoadPolarityStmt(ParamStmt): - """ LP - Gerber Load Polarity statement """ - - def __init__(self, dark): - self.dark = dark - - def to_gerber(self, settings): - lp = 'D' if self.dark else 'C' - return f'%LP{lp}*%' - - def __str__(self): - lp = 'dark' if self.dark else 'clear' - return f'<LP Level Polarity: {lp}>' - - -class ApertureDefStmt(ParamStmt): - """ AD - Aperture Definition Statement """ - - def __init__(self, number, aperture): - self.number = number - self.aperture = aperture - - def to_gerber(self, settings): - return f'%ADD{self.number}{self.aperture.to_gerber(settings)}*%' - - def __str__(self): - return f'<AD aperture def for {str(self.aperture).strip("<>")}>' - - def __repr__(self): - return f'ApertureDefStmt({self.number}, {repr(self.aperture)})' - - -class ApertureMacroStmt(ParamStmt): - """ AM - Aperture Macro Statement """ - - def __init__(self, macro): - self.macro = macro - - def to_gerber(self, settings): - return f'%AM{self.macro.name}*\n{self.macro.to_gerber(unit=settings.unit)}*\n%' - - def __str__(self): - return f'<AM Aperture Macro {self.macro.name}: {self.macro}>' - - -class ImagePolarityStmt(ParamStmt): - """ IP - Image Polarity Statement. (Deprecated) """ - - def to_gerber(self, settings): - #ip = 'POS' if settings.image_polarity == 'positive' else 'NEG' - return f'%IPPOS*%' - - def __str__(self): - return '<IP Image Polarity>' - - -class CoordStmt(Statement): - """ D01 - D03 operation statements """ - - def __init__(self, x, y, i=None, j=None, unit=None): - self.x, self.y, self.i, self.j = x, y, i, j - self.unit = unit - - def to_gerber(self, settings): - ret = '' - for var in 'xyij': - val = self.unit.convert_to(settings.unit, getattr(self, var)) - if val is not None: - ret += var.upper() + settings.write_gerber_value(val) - return ret + self.code + '*' - - def __str__(self): - if self.i is None: - return f'<{self.__name__.strip()} x={self.x} y={self.y}>' - else: - return f'<{self.__name__.strip()} x={self.x} y={self.y} i={self.i} j={self.j}>' - -class InterpolateStmt(CoordStmt): - """ D01 Interpolation """ - code = 'D01' - -class MoveStmt(CoordStmt): - """ D02 Move """ - code = 'D02' - -class FlashStmt(CoordStmt): - """ D03 Flash """ - code = 'D03' - -class InterpolationModeStmt(Statement): - """ G01 / G02 / G03 interpolation mode statement """ - def to_gerber(self, settings): - return self.code + '*' - - def __str__(self): - return f'<{self.__doc__.strip()}>' - -class LinearModeStmt(InterpolationModeStmt): - """ G01 linear interpolation mode statement """ - code = 'G01' - -class CircularCWModeStmt(InterpolationModeStmt): - """ G02 circular interpolation mode statement """ - code = 'G02' - -class CircularCCWModeStmt(InterpolationModeStmt): - """ G03 circular interpolation mode statement """ - code = 'G03' - -class SingleQuadrantModeStmt(InterpolationModeStmt): - """ G75 single-quadrant arc interpolation mode statement """ - code = 'G75' - -class RegionStartStmt(InterpolationModeStmt): - """ G36 Region Mode Start Statement. """ - code = 'G36' - -class RegionEndStmt(InterpolationModeStmt): - """ G37 Region Mode End Statement. """ - code = 'G37' - -class ApertureStmt(Statement): - def __init__(self, d): - self.d = int(d) - - def to_gerber(self, settings): - return 'D{0}*'.format(self.d) - - def __str__(self): - return '<Aperture: %d>' % self.d - - -class CommentStmt(Statement): - """ G04 Comment Statment """ - - def __init__(self, comment): - self.comment = comment if comment is not None else "" - - def to_gerber(self, settings): - return f'G04{self.comment}*' - - def __str__(self): - return f'<G04 Comment: {self.comment}>' - - -class EofStmt(Statement): - """ M02 EOF Statement """ - - def to_gerber(self, settings): - return 'M02*' - - def __str__(self): - return '<M02 EOF Statement>' - -class UnknownStmt(Statement): - def __init__(self, line): - self.line = line - - def to_gerber(self, settings): - return self.line - - def __str__(self): - return f'<Unknown Statement: "{self.line}">' diff --git a/gerbonara/gerber/graphic_objects.py b/gerbonara/gerber/graphic_objects.py index f97aff4..032b562 100644 --- a/gerbonara/gerber/graphic_objects.py +++ b/gerbonara/gerber/graphic_objects.py @@ -2,9 +2,8 @@ import math from dataclasses import dataclass, KW_ONLY, astuple, replace, fields -from .utils import MM +from .utils import MM, InterpMode from . import graphic_primitives as gp -from .gerber_statements import * def convert(value, src, dst): @@ -76,15 +75,21 @@ class Flash(GerberObject): def to_statements(self, gs): yield from gs.set_polarity(self.polarity_dark) yield from gs.set_aperture(self.aperture) - yield FlashStmt(self.x, self.y, unit=self.unit) + + x = gs.file_settings.write_gerber_value(self.x, self.unit) + y = gs.file_settings.write_gerber_value(self.y, self.unit) + yield f'D03X{x}Y{y}*' + gs.update_point(self.x, self.y, unit=self.unit) def to_xnc(self, ctx): yield from ctx.select_tool(self.tool) yield from ctx.drill_mode() + x = ctx.settings.write_gerber_value(self.x, self.unit) y = ctx.settings.write_gerber_value(self.y, self.unit) yield f'X{x}Y{y}' + ctx.set_current_point(self.unit, self.x, self.y) def curve_length(self, unit=MM): @@ -143,24 +148,35 @@ class Region(GerberObject): def to_statements(self, gs): yield from gs.set_polarity(self.polarity_dark) - yield RegionStartStmt() + yield 'G36*' yield from gs.set_current_point(self.poly.outline[0], unit=self.unit) for point, arc_center in zip(self.poly.outline[1:], self.poly.arc_centers): if arc_center is None: - yield from gs.set_interpolation_mode(LinearModeStmt) - yield InterpolateStmt(*point, unit=self.unit) + yield from gs.set_interpolation_mode(InterpMode.LINEAR) + + x = gs.file_settings.write_gerber_value(point[0], self.unit) + y = gs.file_settings.write_gerber_value(point[1], self.unit) + yield f'D01X{x}Y{y}*' + gs.update_point(*point, unit=self.unit) else: clockwise, (cx, cy) = arc_center x2, y2 = point - yield from gs.set_interpolation_mode(CircularCWModeStmt if clockwise else CircularCCWModeStmt) - yield InterpolateStmt(x2, y2, cx-x2, cy-y2, unit=self.unit) + yield from gs.set_interpolation_mode(InterpMode.CIRCULAR_CW if clockwise else InterpMode.CIRCULAR_CCW) + + x = gs.file_settings.write_gerber_value(x2, self.unit) + y = gs.file_settings.write_gerber_value(y2, self.unit) + # TODO are these coordinates absolute or relative now?! + i = gs.file_settings.write_gerber_value(cx-x2, self.unit) + j = gs.file_settings.write_gerber_value(cy-y2, self.unit) + yield f'D01X{x}Y{y}I{i}J{j}*' + gs.update_point(x2, y2, unit=self.unit) - yield RegionEndStmt() + yield 'G37*' @dataclass @@ -207,15 +223,23 @@ class Line(GerberObject): def to_statements(self, gs): yield from gs.set_polarity(self.polarity_dark) yield from gs.set_aperture(self.aperture) - yield from gs.set_interpolation_mode(LinearModeStmt) + yield from gs.set_interpolation_mode(InterpMode.LINEAR) yield from gs.set_current_point(self.p1, unit=self.unit) - yield InterpolateStmt(*self.p2, unit=self.unit) + + x = gs.file_settings.write_gerber_value(self.x2, self.unit) + y = gs.file_settings.write_gerber_value(self.y2, self.unit) + yield f'D01X{x}Y{y}*' + gs.update_point(*self.p2, unit=self.unit) def to_xnc(self, ctx): yield from ctx.select_tool(self.tool) yield from ctx.route_mode(self.unit, *self.p1) - yield 'G01' + 'X' + ctx.settings.write_gerber_value(self.p2[0], self.unit) + 'Y' + ctx.settings.write_gerber_value(self.p2[1], self.unit) + + x = ctx.settings.write_gerber_value(self.x2, self.unit) + y = ctx.settings.write_gerber_value(self.y2, self.unit) + yield f'G01X{x}Y{y}' + ctx.set_current_point(self.unit, *self.p2) def curve_length(self, unit=MM): @@ -280,20 +304,29 @@ class Arc(GerberObject): def to_statements(self, gs): yield from gs.set_polarity(self.polarity_dark) yield from gs.set_aperture(self.aperture) - yield from gs.set_interpolation_mode(CircularCCWModeStmt) + # TODO is the following line correct? + yield from gs.set_interpolation_mode(InterpMode.CIRCULAR_CW if self.clockwise else InterpMode.CIRCULAR_CCW) yield from gs.set_current_point(self.p1, unit=self.unit) - yield InterpolateStmt(self.x2, self.y2, self.cx, self.cy, unit=self.unit) + + x = gs.file_settings.write_gerber_value(self.x2, self.unit) + y = gs.file_settings.write_gerber_value(self.y2, self.unit) + i = gs.file_settings.write_gerber_value(self.cx, self.unit) + j = gs.file_settings.write_gerber_value(self.cy, self.unit) + yield f'D01X{x}Y{y}I{i}J{j}*' + gs.update_point(*self.p2, unit=self.unit) def to_xnc(self, ctx): yield from ctx.select_tool(self.tool) yield from ctx.route_mode(self.unit, self.x1, self.y1) code = 'G02' if self.clockwise else 'G03' + x = ctx.settings.write_gerber_value(self.x2, self.unit) y = ctx.settings.write_gerber_value(self.y2, self.unit) - i = ctx.settings.write_gerber_value(self.cx - self.x1, self.unit) - j = ctx.settings.write_gerber_value(self.cy - self.y1, self.unit) + i = ctx.settings.write_gerber_value(self.cx, self.unit) + j = ctx.settings.write_gerber_value(self.cy, self.unit) yield f'{code}X{x}Y{y}I{i}J{j}' + ctx.set_current_point(self.unit, self.x2, self.y2) def curve_length(self, unit=MM): diff --git a/gerbonara/gerber/graphic_primitives.py b/gerbonara/gerber/graphic_primitives.py index 83b216e..644071c 100644 --- a/gerbonara/gerber/graphic_primitives.py +++ b/gerbonara/gerber/graphic_primitives.py @@ -4,8 +4,6 @@ import itertools from dataclasses import dataclass, KW_ONLY, replace -from .gerber_statements import * - @dataclass class GraphicPrimitive: diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py index 42d7f81..75178f7 100644 --- a/gerbonara/gerber/rs274x.py +++ b/gerbonara/gerber/rs274x.py @@ -33,9 +33,8 @@ from itertools import count, chain from io import StringIO import textwrap -from .gerber_statements import * from .cam import CamFile, FileSettings -from .utils import sq_distance, rotate_point, MM, Inch, units +from .utils import sq_distance, rotate_point, MM, Inch, units, InterpMode from .aperture_macros.parse import ApertureMacro, GenericMacros from . import graphic_primitives as gp from . import graphic_objects as go @@ -215,25 +214,28 @@ class GerberFile(CamFile): return ((min_x, min_y), (max_x, max_y)) - def generate_statements(self, drop_comments=True): - yield UnitStmt() - yield FormatSpecStmt() - yield ImagePolarityStmt() - yield SingleQuadrantModeStmt() - yield LoadPolarityStmt(True) + def generate_statements(self, settings, drop_comments=True): + yield '%MOMM*%' if (settings.unit == 'mm') else '%MOIN*%' + + zeros = 'T' if settings.zeros == 'trailing' else 'L' # default to leading if "None" is specified + notation = 'I' if settings.notation == 'incremental' else 'A' # default to absolute + number_format = str(settings.number_format[0]) + str(settings.number_format[1]) + yield f'%FS{zeros}{notation}X{number_format}Y{number_format}*%' + yield '%IPPOS*%' + yield 'G75' + yield '%LPD*%' if not drop_comments: - yield CommentStmt('File processed by Gerbonara. Original comments:') + yield 'G04 File processed by Gerbonara. Original comments:' for cmt in self.comments: - yield CommentStmt(cmt) + yield f'G04{cmt}' # Always emit gerbonara's generic, rotation-capable aperture macro replacements for the standard C/R/O/P shapes. # Unconditionally emitting these here is easier than first trying to figure out if we need them later, # and they are only a few bytes anyway. - yield ApertureMacroStmt(GenericMacros.circle) - yield ApertureMacroStmt(GenericMacros.rect) - yield ApertureMacroStmt(GenericMacros.obround) - yield ApertureMacroStmt(GenericMacros.polygon) + am_stmt = lambda macro: f'%AM{macro.name}*\n{macro.to_gerber(unit=settings.unit)}*\n%' + for macro in [ GenericMacros.circle, GenericMacros.rect, GenericMacros.obround, GenericMacros.polygon ]: + yield am_stmt(macro) processed_macros = set() aperture_map = {} @@ -243,17 +245,17 @@ class GerberFile(CamFile): macro_grb = aperture._rotated().macro.to_gerber() # use native unit to compare macros if macro_grb not in processed_macros: processed_macros.add(macro_grb) - yield ApertureMacroStmt(aperture._rotated().macro) + yield am_stmt(aperture._rotated().macro) - yield ApertureDefStmt(number, aperture) + yield f'%ADD{number}{aperture.to_gerber(settings)}*%' aperture_map[id(aperture)] = number - gs = GraphicsState(aperture_map=aperture_map) + gs = GraphicsState(aperture_map=aperture_map, file_settings=settings) for primitive in self.objects: yield from primitive.to_statements(gs) - yield EofStmt() + yield 'M02*' def __str__(self): return f'<GerberFile with {len(self.apertures)} apertures, {len(self.objects)} objects>' @@ -269,7 +271,7 @@ class GerberFile(CamFile): settings = self.import_settings.copy() or FileSettings() settings.zeros = None settings.number_format = (5,6) - return '\n'.join(stmt.to_gerber(settings) for stmt in self.generate_statements()) + return '\n'.join(self.generate_statements(settings)) def offset(self, dx=0, dy=0, unit=MM): # TODO round offset to file resolution @@ -308,7 +310,7 @@ class GraphicsState: point : tuple = None aperture : apertures.Aperture = None file_settings : FileSettings = None - interpolation_mode : InterpolationModeStmt = LinearModeStmt + interpolation_mode : InterpMode = InterpMode.LINEAR multi_quadrant_mode : bool = None # used only for syntax checking aperture_mirroring = (False, False) # LM mirroring (x, y) aperture_rotation = 0 # LR rotation in degree, ccw @@ -411,7 +413,7 @@ class GraphicsState: 'pass through the created objects here. Note that these will not show up in e.g. SVG output since ' 'their line width is zero.', SyntaxWarning) - if self.interpolation_mode == LinearModeStmt: + if self.interpolation_mode == InterpMode.LINEAR: if i is not None or j is not None: raise SyntaxError("i/j coordinates given for linear D01 operation (which doesn't take i/j)") @@ -437,7 +439,7 @@ class GraphicsState: polarity_dark=self.polarity_dark, unit=self.file_settings.unit) def _create_arc(self, old_point, new_point, control_point, aperture=True): - clockwise = self.interpolation_mode == CircularCWModeStmt + clockwise = self.interpolation_mode == InterpMode.CIRCULAR_CW return go.Arc(*old_point, *new_point, *self.map_coord(*control_point, relative=True), clockwise=clockwise, aperture=(self.aperture if aperture else None), polarity_dark=self.polarity_dark, unit=self.file_settings.unit) @@ -458,12 +460,12 @@ class GraphicsState: def set_polarity(self, polarity_dark): if self.polarity_dark != polarity_dark: self.polarity_dark = polarity_dark - yield LoadPolarityStmt(polarity_dark) + yield '%LPD*%' if polarity_dark else '%LPC*%' def set_aperture(self, aperture): if self.aperture != aperture: self.aperture = aperture - yield ApertureStmt(self.aperture_map[id(aperture)]) + yield f'D{self.aperture_map[id(aperture)]}*' def set_current_point(self, point, unit=None): point_mm = MM(point[0], unit), MM(point[1], unit) @@ -471,12 +473,14 @@ class GraphicsState: if not points_close(self.point, point_mm): self.point = point_mm - yield MoveStmt(*point, unit=unit) + x = self.file_settings.write_gerber_value(point[0], unit=unit) + y = self.file_settings.write_gerber_value(point[1], unit=unit) + yield f'D02X{x}Y{y}*' def set_interpolation_mode(self, mode): if self.interpolation_mode != mode: self.interpolation_mode = mode - yield mode() + yield {InterpMode.LINEAR: 'G01', InterpMode.CIRCULAR_CW: 'G02', InterpMode.CIRCULAR_CCW: 'G03'}[mode] class GerberParser: @@ -591,11 +595,11 @@ class GerberParser: def _parse_interpolation_mode(self, match): if match['code'] == 'G01': - self.graphics_state.interpolation_mode = LinearModeStmt + self.graphics_state.interpolation_mode = InterpMode.LINEAR elif match['code'] == 'G02': - self.graphics_state.interpolation_mode = CircularCWModeStmt + self.graphics_state.interpolation_mode = InterpMode.CIRCULAR_CW elif match['code'] == 'G03': - self.graphics_state.interpolation_mode = CircularCCWModeStmt + self.graphics_state.interpolation_mode = InterpMode.CIRCULAR_CCW elif match['code'] == 'G74': self.multi_quadrant_mode = True # used only for syntax checking elif match['code'] == 'G75': @@ -620,7 +624,7 @@ class GerberParser: self.last_operation = op if op in ('D1', 'D01'): - if self.graphics_state.interpolation_mode != LinearModeStmt: + if self.graphics_state.interpolation_mode != InterpMode.LINEAR: if self.multi_quadrant_mode is None: warnings.warn('Circular arc interpolation without explicit G75 Single-Quadrant mode statement. '\ 'This can cause problems with older gerber interpreters.', SyntaxWarning) diff --git a/gerbonara/gerber/utils.py b/gerbonara/gerber/utils.py index f7df4ed..060aa0b 100644 --- a/gerbonara/gerber/utils.py +++ b/gerbonara/gerber/utils.py @@ -24,6 +24,7 @@ files. """ import os +from enum import Enum from math import radians, sin, cos, sqrt, atan2, pi @@ -75,6 +76,12 @@ units = {'inch': Inch, 'mm': MM, None: None} to_unit = lambda name: units[name] +class InterpMode(Enum): + LINEAR = 0 + CIRCULAR_CW = 1 + CIRCULAR_CCW = 2 + + def decimal_string(value, precision=6, padding=False): """ Convert float to string with limited precision @@ -161,3 +168,4 @@ def sq_distance(point1, point2): diff2 = point1[1] - point2[1] return diff1 * diff1 + diff2 * diff2 + |