From 2ce0ff81ae50053022cc091b9db2d44f5b3ddc63 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 9 Jan 2022 23:37:27 +0100 Subject: Fix remaining svg rendering/gerber compositing bugs --- gerbonara/gerber/graphic_primitives.py | 18 +++++++++++++----- gerbonara/gerber/rs274x.py | 22 ++-------------------- gerbonara/gerber/tests/image_support.py | 3 ++- gerbonara/gerber/tests/test_rs274x.py | 13 ++++++++----- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/gerbonara/gerber/graphic_primitives.py b/gerbonara/gerber/graphic_primitives.py index 4e176b2..1b9f09b 100644 --- a/gerbonara/gerber/graphic_primitives.py +++ b/gerbonara/gerber/graphic_primitives.py @@ -177,14 +177,22 @@ def point_line_distance(l1, l2, p): return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / length def svg_arc(old, new, center, clockwise): - print(f'{old=} {new=} {center=}') - r = point_distance(old, new) + r = point_distance(old, center) d = point_line_distance(old, new, center) # invert sweep flag since the svg y axis is mirrored sweep_flag = int(not clockwise) - large_arc = int((d > 0) == clockwise) # FIXME check signs - print(f'{r=:.3} {d=:.3} {sweep_flag=} {large_arc=} {clockwise=}') - return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {new[0]:.6} {new[1]:.6}' + # In the degenerate case where old == new, we always take the long way around. To represent this "full-circle arc" + # in SVG, we have to split it into two. + if math.isclose(point_distance(old, new), 0): + intermediate = center[0] + (center[0] - old[0]), center[1] + (center[1] - old[1]) + # Note that we have to preserve the sweep flag to avoid causing self-intersections by flipping the direction of + # a circular cutin + return f'A {r:.6} {r:.6} 0 1 {sweep_flag} {intermediate[0]:.6} {intermediate[1]:.6} ' +\ + f'A {r:.6} {r:.6} 0 1 {sweep_flag} {new[0]:.6} {new[1]:.6}' + + else: # normal case + large_arc = int((d > 0) == clockwise) + return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {new[0]:.6} {new[1]:.6}' @dataclass class ArcPoly(GraphicPrimitive): diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py index 607b8bc..e203780 100644 --- a/gerbonara/gerber/rs274x.py +++ b/gerbonara/gerber/rs274x.py @@ -91,7 +91,6 @@ class GerberFile(CamFile): if force_bounds is None: (min_x, min_y), (max_x, max_y) = self.bounding_box(svg_unit, default=((0, 0), (0, 0))) - print('bounding box:', (min_x, min_y), (max_x, max_y)) else: (min_x, min_y), (max_x, max_y) = force_bounds min_x = convert(min_x, arg_unit, svg_unit) @@ -129,8 +128,9 @@ class GerberFile(CamFile): # dedup apertures new_apertures = {} replace_apertures = {} + mock_settings = self.import_settings for ap in self.apertures + other.apertures: - gbr = ap.to_gerber() + gbr = ap.to_gerber(mock_settings) if gbr not in new_apertures: new_apertures[gbr] = ap else: @@ -281,13 +281,7 @@ class GerberFile(CamFile): def offset(self, dx=0, dy=0, unit='mm'): # TODO round offset to file resolution - #print(f'offset {dx},{dy} file unit') - #for obj in self.objects: - # print(' ', obj) self.objects = [ obj.with_offset(dx, dy, unit) for obj in self.objects ] - #print('after:') - #for obj in self.objects: - # print(' ', obj) def rotate(self, angle:'radian', center=(0,0), unit='mm'): """ Rotate file contents around given point. @@ -307,17 +301,9 @@ class GerberFile(CamFile): for ap in self.apertures: ap.rotation += angle - #print(f'rotate {angle} @ {center}') - #for obj in self.objects: - # print(' ', obj) - for obj in self.objects: obj.rotate(angle, *center, unit) - #print('after') - #for obj in self.objects: - # print(' ', obj) - def invert_polarity(self): for obj in self.objects: obj.polarity_dark = not p.polarity_dark @@ -459,10 +445,6 @@ class GraphicsState: def _create_arc(self, old_point, new_point, control_point, aperture=True): clockwise = self.interpolation_mode == CircularCWModeStmt - print('creating arc') - print(' old point', old_point) - print(' new point', new_point) - print(' control point', self.map_coord(*control_point, relative=True)) 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) diff --git a/gerbonara/gerber/tests/image_support.py b/gerbonara/gerber/tests/image_support.py index ee84203..ba28561 100644 --- a/gerbonara/gerber/tests/image_support.py +++ b/gerbonara/gerber/tests/image_support.py @@ -63,13 +63,14 @@ def run_cargo_cmd(cmd, args, **kwargs): def svg_to_png(in_svg, out_png, dpi=100, bg='black'): run_cargo_cmd('resvg', ['--background', bg, '--dpi', str(dpi), in_svg, out_png], check=True, stdout=subprocess.DEVNULL) -def gerbv_export(in_gbr, out_svg, format='svg', origin=(0, 0), size=(6, 6), fg='#ffffff'): +def gerbv_export(in_gbr, out_svg, format='svg', origin=(0, 0), size=(6, 6), fg='#ffffff', bg='#000000'): x, y = origin w, h = size cmd = ['gerbv', '-x', format, '--border=0', f'--origin={x:.6f}x{y:.6f}', f'--window_inch={w:.6f}x{h:.6f}', f'--foreground={fg}', + f'--background={bg}', '-o', str(out_svg), str(in_gbr)] subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py index 5be0af5..7382997 100644 --- a/gerbonara/gerber/tests/test_rs274x.py +++ b/gerbonara/gerber/tests/test_rs274x.py @@ -46,7 +46,7 @@ def print_on_error(request): if request.node.rep_call.failed: for msg in messages: - print(msg) + print(msg, end='') @pytest.fixture def tmpfile(request): @@ -322,8 +322,14 @@ def test_svg_export(reference, tmpfile): with open(out_svg, 'w') as f: f.write(str(grb.to_svg(force_bounds=bounds, arg_unit='inch', color='white'))) + # NOTE: Instead of having gerbv directly export a PNG, we ask gerbv to output SVG which we then rasterize using + # resvg. We have to do this since gerbv's built-in cairo-based PNG export has severe aliasing issues. In contrast, + # using resvg for both allows an apples-to-apples comparison of both results. + ref_svg = tmpfile('Reference export', '.svg') ref_png = tmpfile('Reference render', '.png') - gerbv_export(reference, ref_png, origin=bounds[0], size=bounds[1], format='png', fg='#000000') + gerbv_export(reference, ref_svg, origin=bounds[0], size=bounds[1]) + svg_to_png(ref_svg, ref_png, dpi=72) # make dpi match Cairo's default + out_png = tmpfile('Output render', '.png') svg_to_png(out_svg, out_png, dpi=72) # make dpi match Cairo's default @@ -363,11 +369,8 @@ def test_bounding_box(reference, tmpfile): assert (img > 0).any() # there must be some content, none of the test gerbers are completely empty. cols = img.sum(axis=1) rows = img.sum(axis=0) - print('shape:', img.shape) col_prefix, col_suffix = np.argmax(cols > 0), np.argmax(cols[::-1] > 0) row_prefix, row_suffix = np.argmax(rows > 0), np.argmax(rows[::-1] > 0) - print('cols', 'prefix:', row_prefix, 'suffix:', row_suffix) - print('rows', 'prefix:', row_prefix, 'suffix:', row_suffix) # Check that all margins are completely black and that the content touches the margins. Allow for some tolerance to # allow for antialiasing artifacts. -- cgit