summaryrefslogtreecommitdiff
path: root/gerbonara/gerber
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber')
-rwxr-xr-xgerbonara/gerber/excellon.py7
-rw-r--r--gerbonara/gerber/gerber_statements.py218
-rw-r--r--gerbonara/gerber/graphic_objects.py65
-rw-r--r--gerbonara/gerber/graphic_primitives.py2
-rw-r--r--gerbonara/gerber/rs274x.py64
-rw-r--r--gerbonara/gerber/utils.py8
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
+