summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2022-01-09 23:37:27 +0100
committerjaseg <git@jaseg.de>2022-01-09 23:37:27 +0100
commit2ce0ff81ae50053022cc091b9db2d44f5b3ddc63 (patch)
treec1b4a444564c9799905aebb3556038a2ffe005c0
parent4d77937f01e4a7561f412a07e1e2c5a5dba0fc49 (diff)
downloadgerbonara-2ce0ff81ae50053022cc091b9db2d44f5b3ddc63.tar.gz
gerbonara-2ce0ff81ae50053022cc091b9db2d44f5b3ddc63.tar.bz2
gerbonara-2ce0ff81ae50053022cc091b9db2d44f5b3ddc63.zip
Fix remaining svg rendering/gerber compositing bugs
-rw-r--r--gerbonara/gerber/graphic_primitives.py18
-rw-r--r--gerbonara/gerber/rs274x.py22
-rw-r--r--gerbonara/gerber/tests/image_support.py3
-rw-r--r--gerbonara/gerber/tests/test_rs274x.py13
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.