summaryrefslogtreecommitdiff
path: root/gerbonara/tests
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-04-22 17:16:20 +0200
committerjaseg <git@jaseg.de>2023-04-22 17:16:20 +0200
commita93d118773818fbd47af4965d7b37e1b10bdf9b6 (patch)
tree528a06ac8154dfa70caa8714e7aa5ea62a51dd9f /gerbonara/tests
parent5ce88e4d1b06dcc846c94ec614fb00f64e85c125 (diff)
downloadgerbonara-a93d118773818fbd47af4965d7b37e1b10bdf9b6.tar.gz
gerbonara-a93d118773818fbd47af4965d7b37e1b10bdf9b6.tar.bz2
gerbonara-a93d118773818fbd47af4965d7b37e1b10bdf9b6.zip
kicad unit tests WIP
Diffstat (limited to 'gerbonara/tests')
-rw-r--r--gerbonara/tests/image_support.py29
-rw-r--r--gerbonara/tests/test_kicad_footprints.py122
2 files changed, 145 insertions, 6 deletions
diff --git a/gerbonara/tests/image_support.py b/gerbonara/tests/image_support.py
index b5e06cf..9902863 100644
--- a/gerbonara/tests/image_support.py
+++ b/gerbonara/tests/image_support.py
@@ -143,6 +143,29 @@ def gerbv_export(in_gbr, out_svg, export_format='svg', origin=(0, 0), size=(6, 6
print(f'Re-using cache for {Path(in_gbr).name}')
shutil.copy(cachefile, out_svg)
+def kicad_fp_export(mod_file, out_svg):
+ mod_file = Path(mod_file)
+ if mod_file.suffix.lower() != '.kicad_mod':
+ raise ValueError("KiCad footprint file must have .kicad_mod extension for kicad-cli to do it's thing")
+
+ params = f'(noparams)'.encode()
+ digest = hashlib.blake2b(mod_file.read_bytes() + params).hexdigest()
+ cachefile = cachedir / f'{digest}.svg'
+
+ if not cachefile.is_file():
+ print(f'Building cache for {mod_file.name}')
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ pretty_dir = mod_file.parent
+ fp_name = mod_file.name[:-len('.kicad_mod')]
+ cmd = ['kicad-cli', 'fp', 'export', 'svg', '--output', tmpdir, '--footprint', fp_name, pretty_dir]
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ out_file = Path(tmpdir) / f'{fp_name}.svg'
+ shutil.copy(out_file, cachefile)
+ else:
+ print(f'Re-using cache for {mod_file.name}')
+ shutil.copy(cachefile, out_svg)
+
@contextmanager
def svg_soup(filename):
with open(filename, 'r') as f:
@@ -258,12 +281,12 @@ def gerber_difference_merge(ref1, ref2, actual, diff_out=None, composite_out=Non
return svg_difference(ref1_svg.name, act_svg.name, diff_out=diff_out)
-def svg_difference(reference, actual, diff_out=None, background=None):
+def svg_difference(reference, actual, diff_out=None, background=None, dpi=100):
with tempfile.NamedTemporaryFile(suffix='-ref.png') as ref_png,\
tempfile.NamedTemporaryFile(suffix='-act.png') as act_png:
- svg_to_png(reference, ref_png.name, bg=background)
- svg_to_png(actual, act_png.name, bg=background)
+ svg_to_png(reference, ref_png.name, bg=background, dpi=dpi)
+ svg_to_png(actual, act_png.name, bg=background, dpi=dpi)
return image_difference(ref_png.name, act_png.name, diff_out=diff_out)
diff --git a/gerbonara/tests/test_kicad_footprints.py b/gerbonara/tests/test_kicad_footprints.py
index a238e1c..d5e7085 100644
--- a/gerbonara/tests/test_kicad_footprints.py
+++ b/gerbonara/tests/test_kicad_footprints.py
@@ -1,17 +1,27 @@
from itertools import zip_longest
+import subprocess
import re
+from .utils import tmpfile, print_on_error
+from .image_support import kicad_fp_export, svg_difference, svg_soup, svg_to_png, run_cargo_cmd
+
+from .. import graphic_objects as go
+from ..utils import MM
+from ..layers import LayerStack
from ..cad.kicad.sexp import build_sexp
from ..cad.kicad.sexp_mapper import sexp
-from ..cad.kicad.footprints import Footprint
+from ..cad.kicad.footprints import Footprint, FootprintInstance, LAYER_MAP_G2K
+from ..cad.kicad.layer_colors import KICAD_LAYER_COLORS, KICAD_DRILL_COLORS
+
def test_parse(kicad_mod_file):
- Footprint.open(kicad_mod_file)
+ Footprint.open_mod(kicad_mod_file)
+
def test_round_trip(kicad_mod_file):
print('========== Stage 1 load ==========')
- orig_fp = Footprint.open(kicad_mod_file)
+ orig_fp = Footprint.open_mod(kicad_mod_file)
print('========== Stage 1 save ==========')
stage1_sexp = build_sexp(orig_fp.sexp())
with open('/tmp/foo.sexp', 'w') as f:
@@ -55,3 +65,109 @@ def test_round_trip(kicad_mod_file):
assert original == stage1
+def _parse_path_d(path):
+ path_d = path.get('d')
+ if not path_d:
+ return
+
+ for match in re.finditer(r'[ML] ?([0-9.]+) *,? *([0-9.]+)', path_d):
+ x, y = match.groups()
+ x, y = float(x), float(y)
+ yield x, y
+
+def test_render(kicad_mod_file, tmpfile, print_on_error):
+ # Hide text and remove text from KiCad's renders. Our text rendering is alright, but KiCad has some weird issue
+ # where it seems to mis-calculate the bounding box of stroke font text, leading to a wonky viewport not matching the
+ # actual content, and text that is slightly off from where it should be. The difference is only a few hundred
+ # micrometers, but it's enough to really throw off our error calculation, so we just ignore text.
+ fp = FootprintInstance(0, 0, sexp=Footprint.open_mod(kicad_mod_file), hide_text=True)
+ stack = LayerStack(courtyard=True, fabrication=True)
+ fp.render(stack)
+ color_map = {gn_id: KICAD_LAYER_COLORS[kicad_id] for gn_id, kicad_id in LAYER_MAP_G2K.items()}
+ color_map[('drill', 'pth')] = (255, 255, 255, 1)
+ color_map[('drill', 'npth')] = (255, 255, 255, 1)
+ color_map = {key: (f'#{r:02x}{g:02x}{b:02x}', str(a)) for key, (r, g, b, a) in color_map.items()}
+
+ margin = 10 # mm
+
+ layer = stack[('top', 'courtyard')]
+ points = []
+ for obj in layer.objects:
+ if isinstance(obj, (go.Line, go.Arc)):
+ points.append((obj.x1, obj.y1))
+ points.append((obj.x2, obj.y2))
+
+ if not points:
+ print('Footprint has no paths on courtyard layer')
+ return
+
+ min_x = min(x for x, y in points)
+ min_y = min(y for x, y in points)
+ max_x = max(x for x, y in points)
+ max_y = max(y for x, y in points)
+ w, h = max_x-min_x, max_y-min_y
+ bounds = ((min_x, min_y), (max_x, max_y))
+ print_on_error('Gerbonara bounds:', bounds, f'w={w:.6f}', f'h={h:.6f}')
+
+ out_svg = tmpfile('Output', '.svg')
+ out_svg.write_text(str(stack.to_svg(color_map=color_map, force_bounds=bounds, margin=margin)))
+
+ print_on_error('Input footprint:', kicad_mod_file)
+ ref_svg = tmpfile('Reference render', '.svg')
+ kicad_fp_export(kicad_mod_file, ref_svg)
+
+ # KiCad's bounding box calculation for SVG output looks broken, and the resulting files have viewports that are too
+ # large. We align our output and KiCad's output using the footprint's courtyard layer.
+ points = []
+ with svg_soup(ref_svg) as soup:
+ for group in soup.find_all('g'):
+ style = group.get('style', '').lower().replace(' ', '')
+ if 'fill:#ff26e2' not in style or 'stroke:#ff26e2' not in style:
+ continue
+
+ # This group contains courtyard layer items.
+ for path in group.find_all('path'):
+ points += _parse_path_d(path)
+
+ if not points:
+ print('Footprint has no paths on courtyard layer')
+ return
+
+ min_x = min(x for x, y in points)
+ min_y = min(y for x, y in points)
+ max_x = max(x for x, y in points)
+ max_y = max(y for x, y in points)
+ print_on_error('KiCad bounds:', ((min_x, min_y), (max_x, max_y)), f'w={max_x-min_x:.6f}', f'h={max_y-min_y:.6f}')
+ min_x -= margin
+ min_y -= margin
+ max_x += margin
+ max_y += margin
+ w, h = max_x-min_x, max_y-min_y
+
+ root = soup.find('svg')
+ root_w = root['width'] = f'{w:.6f}mm'
+ root_h = root['height'] = f'{h:.6f}mm'
+ root['viewBox'] = f'{min_x:.6f} {min_y:.6f} {w:.6f} {h:.6f}'
+
+ for group in soup.find_all('g', attrs={'class': 'stroked-text'}):
+ group.decompose()
+
+ # Currently, there is a bug in resvg leading to mis-rendering. On the file below from the KiCad standard lib, resvg
+ # renders all round pads in a wrong color (?). Interestingly, passing the file through usvg before rendering fixes
+ # this.
+ # Sample footprint: Connector_PinSocket_2.00mm.pretty/PinSocket_2x11_P2.00mm_Vertical.kicad_mod
+ run_cargo_cmd('usvg', [str(ref_svg), str(ref_svg)])
+
+ # fix up usvg width/height
+ with svg_soup(ref_svg) as soup:
+ root = soup.find('svg')
+ root['width'] = root_w
+ root['height'] = root_h
+
+ svg_to_png(ref_svg, tmpfile('Reference render', '.png'), bg=None, dpi=600)
+ svg_to_png(out_svg, tmpfile('Output render', '.png'), bg=None, dpi=600)
+ mean, _max, hist = svg_difference(ref_svg, out_svg, dpi=600, diff_out=tmpfile('Difference', '.png'))
+ assert mean < 1e-3
+ assert hist[9] < 100
+ assert hist[3:].sum() < 1e-3*hist.size
+