Remove unnecessary statement class indirection layer

This commit is contained in:
jaseg 2022-01-19 01:10:40 +01:00
parent 9e86bf6b3e
commit 40286fc92f
6 changed files with 92 additions and 272 deletions

View file

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

View file

@ -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}">'

View file

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

View file

@ -4,8 +4,6 @@ import itertools
from dataclasses import dataclass, KW_ONLY, replace
from .gerber_statements import *
@dataclass
class GraphicPrimitive:

View file

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

View file

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