From 0c15111463e9b48bc9cddb7ae86081e056a2a6b5 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 9 Mar 2026 16:31:08 +0100 Subject: [PATCH] Add pretty_svg test, fix some bugs. --- src/gerbonara/cam.py | 20 +++++++++++++++++++- src/gerbonara/layers.py | 2 +- src/gerbonara/rs274x.py | 20 +------------------- tests/test_layers.py | 23 +++++++++++++++++++++++ tests/test_rs274x.py | 9 +++++++++ 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/gerbonara/cam.py b/src/gerbonara/cam.py index 14c1e8b..06340ba 100644 --- a/src/gerbonara/cam.py +++ b/src/gerbonara/cam.py @@ -26,7 +26,7 @@ import shutil from pathlib import Path from functools import cached_property -from .utils import LengthUnit, MM, Inch, Tag, sum_bounds, setup_svg +from .utils import LengthUnit, MM, Inch, Tag, sum_bounds, setup_svg, convex_hull from . import graphic_primitives as gp from . import graphic_objects as go @@ -351,6 +351,24 @@ class CamFile: return sum_bounds(( p.bounding_box(unit) for p in self.objects ), default=default) + def convex_hull(self, tol=0.01, unit=None): + unit = unit or self.unit + points = [] + + for obj in self.objects: + if isinstance(obj, go.Line): + line = obj.as_primitive(unit) + points.append((line.x1, line.y1)) + points.append((line.x2, line.y2)) + + elif isinstance(obj, go.Arc): + for obj in obj.approximate(tol, unit): + line = obj.as_primitive(unit) + points.append((line.x1, line.y1)) + points.append((line.x2, line.y2)) + + return convex_hull(points) + def to_excellon(self): """ Convert to a :py:class:`.ExcellonFile`. Returns ``self`` if it already is one. """ raise NotImplementedError() diff --git a/src/gerbonara/layers.py b/src/gerbonara/layers.py index 7fc08bd..e810375 100644 --- a/src/gerbonara/layers.py +++ b/src/gerbonara/layers.py @@ -1315,7 +1315,7 @@ class LayerStack: return if (cur, i) in joins and joins[(cur, i)] != (nearest, j): - warnings.warn(f'Three-way intersection on outline layer at: {(nearest, j)}; {(cur, i)}; and {joins[(nearest, j)]}. Falling back to returning the convex hull of the outline layer.{maybe_allegro_hint}') + warnings.warn(f'Three-way intersection on outline layer at: {(nearest, j)}; {(cur, i)}; and {joins[(cur, i)]}. Falling back to returning the convex hull of the outline layer.{maybe_allegro_hint}') yield list(convex_hull_to_lines(self.outline.instance.convex_hull(tol, unit), unit)) return diff --git a/src/gerbonara/rs274x.py b/src/gerbonara/rs274x.py index 08eb8c2..c0c954e 100644 --- a/src/gerbonara/rs274x.py +++ b/src/gerbonara/rs274x.py @@ -28,7 +28,7 @@ import dataclasses import functools from .cam import CamFile, FileSettings -from .utils import MM, Inch, units, InterpMode, UnknownStatementWarning, convex_hull +from .utils import MM, Inch, units, InterpMode, UnknownStatementWarning from .aperture_macros.parse import ApertureMacro, GenericMacros from . import graphic_primitives as gp from . import graphic_objects as go @@ -377,24 +377,6 @@ class GerberFile(CamFile): for obj in self.objects: obj.polarity_dark = not obj.polarity_dark - def convex_hull(self, tol=0.01, unit=None): - unit = unit or self.unit - points = [] - - for obj in self.objects: - if isinstance(obj, go.Line): - line = obj.as_primitive(unit) - points.append((line.x1, line.y1)) - points.append((line.x2, line.y2)) - - elif isinstance(obj, go.Arc): - for obj in obj.approximate(tol, unit): - line = obj.as_primitive(unit) - points.append((line.x1, line.y1)) - points.append((line.x2, line.y2)) - - return convex_hull(points) - class GraphicsState: """ Internal class used to track Gerber processing state during import and export. diff --git a/tests/test_layers.py b/tests/test_layers.py index b0535ac..a5393b4 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -17,6 +17,7 @@ # from pathlib import Path +import tempfile import pytest @@ -317,6 +318,18 @@ REFERENCE_DIRS = { 'silk_screen_bottom.art': 'bottom silk', 'silk_screen_top.art': 'top silk', }, + 'world_clock_2': { + 'wc2-B_Cu.gbr': 'bottom copper', + 'wc2-B_Mask.gbr': 'bottom mask', + 'wc2-B_Paste.gbr': 'bottom paste', + 'wc2-B_SilkS.gbr': 'bottom silk', + 'wc2-Edge_Cuts.gbr': 'mechanical outline', + 'wc2-F_Cu.gbr': 'top copper', + 'wc2-F_Mask.gbr': 'top mask', + 'wc2-F_Paste.gbr': 'top paste', + 'wc2-F_SilkS.gbr': 'top silk', + 'wc2.kicad_pcb': None, + }, } @filter_syntax_warnings @@ -359,3 +372,13 @@ def test_layer_classifier(ref_dir, file_map): if 'upverter' not in ref_dir: assert isinstance(layer, ExcellonFile) + +@filter_syntax_warnings +@pytest.mark.parametrize('ref_dir', list(REFERENCE_DIRS)) +def test_pretty_svg_export(ref_dir): + """ Tests the basic functionality of to_pretty_svg """ + path = reference_path(ref_dir) + stack = LayerStack.open_dir(path) + with tempfile.NamedTemporaryFile(suffix='.svg') as f: + stack.to_pretty_svg() + diff --git a/tests/test_rs274x.py b/tests/test_rs274x.py index 944b5fe..0ee5ab3 100644 --- a/tests/test_rs274x.py +++ b/tests/test_rs274x.py @@ -301,6 +301,15 @@ REFERENCE_FILES = [ l.strip() for l in ''' kicad-x2-tests/x2noap/Flashpads-F_Mask.gbr kicad-x2-tests/x2noap/Flashpads-F_Paste.gbr kicad-x2-tests/x2noap/Flashpads-F_Silkscreen.gbr + world_clock_2/wc2-B_Cu.gbr + world_clock_2/wc2-B_Mask.gbr + world_clock_2/wc2-B_Paste.gbr + world_clock_2/wc2-B_SilkS.gbr + world_clock_2/wc2-Edge_Cuts.gbr + world_clock_2/wc2-F_Cu.gbr + world_clock_2/wc2-F_Mask.gbr + world_clock_2/wc2-F_Paste.gbr + world_clock_2/wc2-F_SilkS.gbr gerbv.gbr '''.splitlines() if l ]