diff options
-rw-r--r-- | gerbonara/aperture_macros/parse.py | 19 | ||||
-rw-r--r-- | gerbonara/aperture_macros/primitive.py | 55 | ||||
-rw-r--r-- | gerbonara/rs274x.py | 2 | ||||
-rw-r--r-- | gerbonara/tests/test_excellon.py | 9 | ||||
-rw-r--r-- | gerbonara/tests/test_rs274x.py | 68 |
5 files changed, 139 insertions, 14 deletions
diff --git a/gerbonara/aperture_macros/parse.py b/gerbonara/aperture_macros/parse.py index de84a9a..33c205e 100644 --- a/gerbonara/aperture_macros/parse.py +++ b/gerbonara/aperture_macros/parse.py @@ -8,6 +8,7 @@ import operator import re import ast import copy +import warnings import math from . import primitive as ap @@ -88,16 +89,22 @@ class ApertureMacro: block = re.sub(r'\s', '', block) if block[0] == '$': # variable definition - name, expr = block.partition('=') - number = int(name[1:]) - if number in variables: - raise SyntaxError(f'Re-definition of aperture macro variable ${number} inside macro. Previous definition of ${number} was ${variables[number]}.') - variables[number] = _parse_expression(expr, variables, parameters) + try: + name, _, expr = block.partition('=') + number = int(name[1:]) + if number in variables: + warnings.warn(f'Re-definition of aperture macro variable ${number} inside macro. Previous definition of ${number} was ${variables[number]}.') + variables[number] = _parse_expression(expr, variables, parameters) + except Exception as e: + raise SyntaxError(f'Error parsing variable definition {block!r}') from e else: # primitive primitive, *args = block.split(',') args = [ _parse_expression(arg, variables, parameters) for arg in args ] - primitives.append(ap.PRIMITIVE_CLASSES[int(primitive)].from_arglist(unit, args)) + try: + primitives.append(ap.PRIMITIVE_CLASSES[int(primitive)].from_arglist(unit, args)) + except KeyError as e: + raise SyntaxError(f'Unknown aperture macro primitive code {int(primitive)}') return kls(macro_name, max(parameters, default=0), tuple(primitives), tuple(comments)) diff --git a/gerbonara/aperture_macros/primitive.py b/gerbonara/aperture_macros/primitive.py index dce5677..31c5cb9 100644 --- a/gerbonara/aperture_macros/primitive.py +++ b/gerbonara/aperture_macros/primitive.py @@ -34,7 +34,6 @@ def rad_to_deg(a): @dataclass(frozen=True, slots=True) class Primitive: unit: LengthUnit - exposure : Expression def __post_init__(self): for field in fields(self): @@ -95,6 +94,7 @@ class Primitive: @dataclass(frozen=True, slots=True) class Circle(Primitive): code = 1 + exposure : Expression diameter : UnitExpression # center x/y x : UnitExpression @@ -124,6 +124,7 @@ class Circle(Primitive): @dataclass(frozen=True, slots=True) class VectorLine(Primitive): code = 20 + exposure : Expression width : UnitExpression start_x : UnitExpression start_y : UnitExpression @@ -166,6 +167,7 @@ class VectorLine(Primitive): @dataclass(frozen=True, slots=True) class CenterLine(Primitive): code = 21 + exposure : Expression width : UnitExpression height : UnitExpression # center x/y @@ -202,6 +204,7 @@ class CenterLine(Primitive): @dataclass(frozen=True, slots=True) class Polygon(Primitive): code = 5 + exposure : Expression n_vertices : Expression # center x/y x : UnitExpression @@ -228,8 +231,56 @@ class Polygon(Primitive): @dataclass(frozen=True, slots=True) +class Moire(Primitive): + """ Deprecated, but still found in some really old gerber files. """ + code = 6 + # center x/y + x : UnitExpression + y : UnitExpression + d_outer : UnitExpression + line_thickness : UnitExpression + gap_w : UnitExpression + num_circles : Expression + crosshair_thickness : UnitExpression = 0 + crosshair_length : UnitExpression =0 + rotation : Expression = 0 + + def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True): + with self.Calculator(self, variable_binding, unit) as calc: + rotation += deg_to_rad(calc.rotation) + x, y = rotate_point(calc.x, calc.y, -rotation, 0, 0) + x, y = x+offset[0], y+offset[1] + + pitch = calc.line_thickness + calc.gap_w + for i in range(int(round(calc.num_circles))): + yield gp.Circle(x, y, calc.d_outer/2 - i*pitch, polarity_dark=True) + yield gp.Circle(x, y, calc.d_inner/2 - i*pitch - calc.line_thickness, polarity_dark=False) + + if math.isclose(calc.crosshair_thickness, 0, abs_tol=1e-6) or\ + math.isclose(calc.crosshair_length, 0, abs_tol=1e-6): + return + + yield gp.Rectangle(x, y, crosshair_length, crosshair_thickness, rotation=rotation, polarity_dark=True) + yield gp.Rectangle(x, y, crosshair_thickness, crosshair_length, rotation=rotation, polarity_dark=True) + + def dilate(self, offset, unit): + # I'd rather print a warning and produce graphically slightly incorrect output in these few cases here than + # producing macros that may evaluate to primitives with negative values. + warnings.warn('Attempted dilation of macro aperture thermal primitive. This is not supported.') + + def scale(self, scale): + return replace(self, + d_outer=self.d_outer * UnitExpression(scale), + d_inner=self.d_inner * UnitExpression(scale), + gap_w=self.gap_w * UnitExpression(scale), + x=self.x * UnitExpression(scale), + y=self.y * UnitExpression(scale)) + + +@dataclass(frozen=True, slots=True) class Thermal(Primitive): code = 7 + exposure : Expression # center x/y x : UnitExpression y : UnitExpression @@ -270,6 +321,7 @@ class Thermal(Primitive): @dataclass(frozen=True, slots=True) class Outline(Primitive): code = 4 + exposure : Expression length: Expression coords: tuple rotation: Expression = 0 @@ -364,6 +416,7 @@ PRIMITIVE_CLASSES = { CenterLine, Outline, Polygon, + Moire, Thermal, ]}, # alternative codes diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py index d5cbb34..cfada76 100644 --- a/gerbonara/rs274x.py +++ b/gerbonara/rs274x.py @@ -602,6 +602,7 @@ class GerberParser: fr"(?:D0?([123]))?$", 'region_start': r'G36$', 'region_end': r'G37$', + 'eof': r"(D02)?M0?[02]", # P-CAD 2006 files have a spurious D02 before M02 as in "D02M02" 'aperture': r"(G54|G55)?\s*D(?P<number>\d+)", # Allegro combines format spec and unit into one long illegal extended command. 'allegro_format_spec': r"FS(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*\*MO(?P<unit>IN|MM)", @@ -624,7 +625,6 @@ class GerberParser: 'siemens_garbage': r'^ICAS$', 'old_unit':r'(?P<mode>G7[01])', 'old_notation': r'(?P<mode>G9[01])', - 'eof': r"M0?[02]", 'ignored': r"(?P<stmt>M01)", # NOTE: The official spec says names can be empty or contain commas. I think that doesn't make sense. 'attribute': r"(?P<eagle_garbage>G04 #@! %)?(?P<type>TF|TA|TO|TD)(?P<name>[._$a-zA-Z][._$a-zA-Z0-9]*)?(,(?P<value>.*))?", diff --git a/gerbonara/tests/test_excellon.py b/gerbonara/tests/test_excellon.py index cd0960f..4dcedd6 100644 --- a/gerbonara/tests/test_excellon.py +++ b/gerbonara/tests/test_excellon.py @@ -50,6 +50,15 @@ REFERENCE_FILES = { 'diptrace/keyboard.drl': (None, 'diptrace/keyboard_Bottom.gbr'), 'zuken-emulated/Drill/8seg_Driver__routed_Drill_thru_plt.fdr/8seg_Driver__routed_Drill_thru_plt.fdr': (('inch', 'trailing', 4), 'zuken-emulated/Gerber/Conductive-1.fph'), 'zuken-emulated/Drill/8seg_Driver__routed_Drill_thru_nplt.fdr': (('inch', 'trailing', 4), None), + 'p-cad/ZXINET.DRL': (None, None), + 'kicad-x2-tests/nox2ap/Flashpads-NPTH.drl': (None, None), + 'kicad-x2-tests/nox2ap/Flashpads-PTH.drl': (None, None), + 'kicad-x2-tests/nox2noap/Flashpads-NPTH.drl': (None, None), + 'kicad-x2-tests/nox2noap/Flashpads-PTH.drl': (None, None), + 'kicad-x2-tests/x2ap/Flashpads-NPTH.drl': (None, None), + 'kicad-x2-tests/x2ap/Flashpads-PTH.drl': (None, None), + 'kicad-x2-tests/x2noap/Flashpads-NPTH.drl': (None, None), + 'kicad-x2-tests/x2noap/Flashpads-PTH.drl': (None, None), } @filter_syntax_warnings diff --git a/gerbonara/tests/test_rs274x.py b/gerbonara/tests/test_rs274x.py index f85b09f..8f0da70 100644 --- a/gerbonara/tests/test_rs274x.py +++ b/gerbonara/tests/test_rs274x.py @@ -248,6 +248,58 @@ REFERENCE_FILES = [ l.strip() for l in ''' zuken-emulated/Gerber/Resist-B.fph zuken-emulated/Gerber/Conductive-1.fph zuken-emulated/Gerber/Conductive-2.fph + p-cad/ZXINET.GBL + p-cad/ZXINET.GBO + p-cad/ZXINET.GBS + p-cad/ZXINET.GKO + p-cad/ZXINET.GTL + p-cad/ZXINET.GTO + p-cad/ZXINET.GTS + fab-3000/bl + fab-3000/bo + fab-3000/bs + fab-3000/ko + fab-3000/tl + fab-3000/to + fab-3000/ts + fab-3000/drl + kicad-x2-tests/nox2ap/Flashpads-B_Cu.gbr + kicad-x2-tests/nox2ap/Flashpads-B_Mask.gbr + kicad-x2-tests/nox2ap/Flashpads-B_Paste.gbr + kicad-x2-tests/nox2ap/Flashpads-B_Silkscreen.gbr + kicad-x2-tests/nox2ap/Flashpads-Edge_Cuts.gbr + kicad-x2-tests/nox2ap/Flashpads-F_Cu.gbr + kicad-x2-tests/nox2ap/Flashpads-F_Mask.gbr + kicad-x2-tests/nox2ap/Flashpads-F_Paste.gbr + kicad-x2-tests/nox2ap/Flashpads-F_Silkscreen.gbr + kicad-x2-tests/nox2noap/Flashpads-B_Cu.gbr + kicad-x2-tests/nox2noap/Flashpads-B_Mask.gbr + kicad-x2-tests/nox2noap/Flashpads-B_Paste.gbr + kicad-x2-tests/nox2noap/Flashpads-B_Silkscreen.gbr + kicad-x2-tests/nox2noap/Flashpads-Edge_Cuts.gbr + kicad-x2-tests/nox2noap/Flashpads-F_Cu.gbr + kicad-x2-tests/nox2noap/Flashpads-F_Mask.gbr + kicad-x2-tests/nox2noap/Flashpads-F_Paste.gbr + kicad-x2-tests/nox2noap/Flashpads-F_Silkscreen.gbr + kicad-x2-tests/x2ap/Flashpads-B_Cu.gbr + kicad-x2-tests/x2ap/Flashpads-B_Mask.gbr + kicad-x2-tests/x2ap/Flashpads-B_Paste.gbr + kicad-x2-tests/x2ap/Flashpads-B_Silkscreen.gbr + kicad-x2-tests/x2ap/Flashpads-Edge_Cuts.gbr + kicad-x2-tests/x2ap/Flashpads-F_Cu.gbr + kicad-x2-tests/x2ap/Flashpads-F_Mask.gbr + kicad-x2-tests/x2ap/Flashpads-F_Paste.gbr + kicad-x2-tests/x2ap/Flashpads-F_Silkscreen.gbr + kicad-x2-tests/x2noap/Flashpads-B_Cu.gbr + kicad-x2-tests/x2noap/Flashpads-B_Mask.gbr + kicad-x2-tests/x2noap/Flashpads-B_Paste.gbr + kicad-x2-tests/x2noap/Flashpads-B_Silkscreen.gbr + kicad-x2-tests/x2noap/Flashpads-Edge_Cuts.gbr + kicad-x2-tests/x2noap/Flashpads-F_Cu.gbr + kicad-x2-tests/x2noap/Flashpads-F_Mask.gbr + kicad-x2-tests/x2noap/Flashpads-F_Paste.gbr + kicad-x2-tests/x2noap/Flashpads-F_Silkscreen.gbr + gerbv.gbr '''.splitlines() if l ] MIN_REFERENCE_FILES = [ @@ -501,8 +553,6 @@ def test_svg_export_gerber(reference, tmpfile): assert mean < 1.2e-3 assert hist[3:].sum() < 1e-3*hist.size -# FIXME test svg margin, bounding box computation - @filter_syntax_warnings @pytest.mark.parametrize('reference', REFERENCE_FILES, indirect=True) def test_bounding_box(reference, tmpfile): @@ -519,6 +569,12 @@ def test_bounding_box(reference, tmpfile): grb = GerberFile.open(reference) + if reference.match(f'fab-3000/*'): + # These files have the board outline plotted in clear polarity. Change them to dark to not confuse our matching + # code below. + for prim in grb.objects: + prim.polarity_dark = True + if grb.is_empty: pytest.skip() @@ -542,10 +598,10 @@ def test_bounding_box(reference, tmpfile): # Check that all margins are completely black and that the content touches the margins. Allow for some tolerance to # allow for antialiasing artifacts and for things like very thin features. - assert margin_px-2 <= col_prefix <= margin_px+2 - assert margin_px-2 <= col_suffix <= margin_px+2 - assert margin_px-2 <= row_prefix <= margin_px+2 - assert margin_px-2 <= row_suffix <= margin_px+2 + assert margin_px-3 <= col_prefix <= margin_px+3 + assert margin_px-3 <= col_suffix <= margin_px+3 + assert margin_px-3 <= row_prefix <= margin_px+3 + assert margin_px-3 <= row_suffix <= margin_px+3 @filter_syntax_warnings def test_syntax_error(): |