Fix a bunch of failing tests
This commit is contained in:
parent
aaaf96e8d9
commit
bda404c18b
5 changed files with 180 additions and 34 deletions
|
|
@ -172,10 +172,10 @@ class GenericMacros:
|
|||
rounded_rect = ApertureMacro('GRR', [
|
||||
ap.CenterLine('mm', [1, var(1)-2*var(3), var(2), 0, 0, var(6) * -deg_per_rad]),
|
||||
ap.CenterLine('mm', [1, var(1), var(2)-2*var(3), 0, 0, var(6) * -deg_per_rad]),
|
||||
ap.Circle('mm', [1, var(3)*2, +(var(1)/2-var(3)), +(var(2)/2-var(3)), 0]),
|
||||
ap.Circle('mm', [1, var(3)*2, +(var(1)/2-var(3)), -(var(2)/2-var(3)), 0]),
|
||||
ap.Circle('mm', [1, var(3)*2, -(var(1)/2-var(3)), +(var(2)/2-var(3)), 0]),
|
||||
ap.Circle('mm', [1, var(3)*2, -(var(1)/2-var(3)), -(var(2)/2-var(3)), 0]),
|
||||
ap.Circle('mm', [1, var(3)*2, +(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad]),
|
||||
ap.Circle('mm', [1, var(3)*2, +(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad]),
|
||||
ap.Circle('mm', [1, var(3)*2, -(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad]),
|
||||
ap.Circle('mm', [1, var(3)*2, -(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad]),
|
||||
*_generic_hole(4)])
|
||||
|
||||
# params: width, height, length difference between narrow side (top) and wide side (bottom), *hole, rotation
|
||||
|
|
|
|||
|
|
@ -177,13 +177,18 @@ class Arc:
|
|||
|
||||
|
||||
def render(self, variables=None):
|
||||
cx, cy = self.mid.x, self.mid.y
|
||||
mx, my = self.mid.x, self.mid.y
|
||||
x1, y1 = self.start.x, self.start.y
|
||||
x2, y2 = self.end.x, self.end.y
|
||||
dasher = Dasher(self)
|
||||
|
||||
# https://stackoverflow.com/questions/56224824/how-do-i-find-the-circumcenter-of-the-triangle-using-python-without-external-lib
|
||||
d = 2 * (x1 * (y2 - my) + x2 * (my - y1) + mx * (y1 - y2))
|
||||
cx = ((x1 * x1 + y1 * y1) * (y2 - my) + (x2 * x2 + y2 * y2) * (my - y1) + (mx * mx + my * my) * (y1 - y2)) / d
|
||||
cy = ((x1 * x1 + y1 * y1) * (mx - x2) + (x2 * x2 + y2 * y2) * (x1 - mx) + (mx * mx + my * my) * (x2 - x1)) / d
|
||||
|
||||
# KiCad only has clockwise arcs.
|
||||
arc = go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, clockwise=True, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
|
||||
arc = go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, clockwise=False, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
|
||||
if dasher.solid:
|
||||
yield arc
|
||||
|
||||
|
|
@ -360,20 +365,20 @@ class Pad:
|
|||
primitives: OmitDefault(CustomPadPrimitives) = None
|
||||
|
||||
def render(self, variables=None):
|
||||
if self.type in (Atom.connect, Atom.np_thru_hole):
|
||||
return
|
||||
#if self.type in (Atom.connect, Atom.np_thru_hole):
|
||||
# return
|
||||
|
||||
yield go.Flash(self.at.x, self.at.y, self.aperture().rotated(math.radians(self.at.rotation)), unit=MM)
|
||||
yield go.Flash(self.at.x, self.at.y, self.aperture(), unit=MM)
|
||||
|
||||
def aperture(self):
|
||||
if self.shape == Atom.circle:
|
||||
return ap.CircleAperture(self.size.x, unit=MM)
|
||||
|
||||
elif self.shape == Atom.rect:
|
||||
return ap.RectangleAperture(self.size.x, self.size.y, unit=MM)
|
||||
return ap.RectangleAperture(self.size.x, self.size.y, unit=MM).rotated(self.at.rotation)
|
||||
|
||||
elif self.shape == Atom.oval:
|
||||
return ap.ObroundAperture(self.size.x, self.size.y, unit=MM)
|
||||
return ap.ObroundAperture(self.size.x, self.size.y, unit=MM).rotated(self.at.rotation)
|
||||
|
||||
elif self.shape == Atom.trapezoid:
|
||||
# KiCad's trapezoid aperture "rect_delta" param is just weird to the point that I think it's probably
|
||||
|
|
@ -405,7 +410,7 @@ class Pad:
|
|||
for obj in self.primitives.all():
|
||||
for gn_obj in obj.render():
|
||||
primitives += gn_obj._aperture_macro_primitives() # todo: precision params
|
||||
macro = ApertureMacro(primitives=primitives)
|
||||
macro = ApertureMacro(primitives=primitives).rotated(self.at.rotation)
|
||||
return ap.ApertureMacroInstance(macro, unit=MM)
|
||||
|
||||
def render_drill(self):
|
||||
|
|
@ -413,14 +418,30 @@ class Pad:
|
|||
return
|
||||
|
||||
plated = self.type != Atom.np_thru_hole
|
||||
aperture = ap.ExcellonTool(self.drill.diameter, plated=plated, unit=MM)
|
||||
if self.drill.oval:
|
||||
w = self.drill.width / 2
|
||||
l = go.Line(-w, 0, w, 0, aperture=aperture, unit=MM)
|
||||
dia = self.drill.diameter
|
||||
w = self.drill.width
|
||||
|
||||
if self.drill.offset:
|
||||
ox, oy = self.drill.offset.x, self.drill.offset.y
|
||||
else:
|
||||
ox, oy = 0, 0
|
||||
|
||||
if w > dia:
|
||||
dx = 0
|
||||
dy = (w-dia)/2
|
||||
else:
|
||||
dx = (dia-w)/2
|
||||
dy = 0
|
||||
|
||||
aperture = ap.ExcellonTool(min(dia, w), plated=plated, unit=MM)
|
||||
l = go.Line(ox-dx, oy-dy, ox+dx, oy+dy, aperture=aperture, unit=MM)
|
||||
l.rotate(math.radians(self.at.rotation))
|
||||
l.offset(self.at.x, self.at.y)
|
||||
yield l
|
||||
|
||||
else:
|
||||
aperture = ap.ExcellonTool(self.drill.diameter, plated=plated, unit=MM)
|
||||
yield go.Flash(self.at.x, self.at.y, aperture=aperture, unit=MM)
|
||||
|
||||
|
||||
|
|
@ -583,6 +604,8 @@ LAYER_MAP_K2G = {
|
|||
'F.CrtYd': ('top', 'courtyard'),
|
||||
'B.Fab': ('bottom', 'fabrication'),
|
||||
'F.Fab': ('top', 'fabrication'),
|
||||
'Dwgs.User': ('mechanical', 'drawings'),
|
||||
'Cmts.User': ('mechanical', 'comments'),
|
||||
'Edge.Cuts': ('mechanical', 'outline'),
|
||||
}
|
||||
|
||||
|
|
@ -609,7 +632,9 @@ class FootprintInstance(Positioned):
|
|||
if self.value is not None:
|
||||
variables['VALUE'] = str(self.value)
|
||||
|
||||
self.sexp.render(layer_stack, LAYER_MAP_K2G,
|
||||
layer_map = {kc_id: gn_id for kc_id, gn_id in LAYER_MAP_K2G.items() if gn_id in layer_stack}
|
||||
|
||||
self.sexp.render(layer_stack, layer_map,
|
||||
x=x, y=y, rotation=rotation,
|
||||
side=self.side,
|
||||
text=(not self.hide_text),
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class FillMode:
|
|||
|
||||
@classmethod
|
||||
def __map__(self, obj, parent=None):
|
||||
return obj[0] in (Atom.solid, Atom.yes)
|
||||
return obj[1] in (Atom.solid, Atom.yes)
|
||||
|
||||
@classmethod
|
||||
def __sexp__(self, value):
|
||||
|
|
|
|||
|
|
@ -716,7 +716,7 @@ class Arc(GraphicObject):
|
|||
|
||||
def as_primitive(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
|
||||
w = self.aperture.equivalent_width(unit) if self.aperture else 0
|
||||
return gp.Arc(x1=conv.x1, y1=conv.y1,
|
||||
x2=conv.x2, y2=conv.y2,
|
||||
cx=conv.cx, cy=conv.cy,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
|
||||
import math
|
||||
from itertools import zip_longest
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
import bs4
|
||||
|
||||
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 ..utils import MM, arc_bounds, sum_bounds
|
||||
from ..layers import LayerStack
|
||||
from ..cad.kicad.sexp import build_sexp
|
||||
from ..cad.kicad.sexp_mapper import sexp
|
||||
|
|
@ -65,15 +68,104 @@ def test_round_trip(kicad_mod_file):
|
|||
assert original == stage1
|
||||
|
||||
|
||||
def _compute_style(elem):
|
||||
current_style = {}
|
||||
for elem in [*reversed(list(elem.parents)), elem]:
|
||||
attrs = dict(elem.attrs)
|
||||
for match in re.finditer(r'([^:;]+):([^:;]+)', attrs.pop('style', '')):
|
||||
k, v = match.groups()
|
||||
current_style[k.strip().lower()] = v.strip()
|
||||
|
||||
for k, v in elem.attrs.items():
|
||||
current_style[k.lower()] = v
|
||||
return current_style
|
||||
|
||||
|
||||
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
|
||||
style = _compute_style(path)
|
||||
if style.get('stroke', 'none') != 'none':
|
||||
sr = float(style.get('stroke-width', 0)) / 2
|
||||
else:
|
||||
sr = 0
|
||||
|
||||
if 'C' in path_d:
|
||||
raise ValueError('Path contains cubic beziers')
|
||||
|
||||
last_x, last_y = None, None
|
||||
for match in re.finditer(r'([ML]) ?([0-9.]+) *,? *([0-9.]+)|(A) ?([0-9.]+) *,? *([0-9.]+) *,? *([0-9.]+) *,? * ([01]) *,? *([01]) *,? *([0-9.]+) *,? *([0-9.]+)', path_d):
|
||||
ml, x, y, a, rx, ry, angle, large_arc, sweep, ax, ay = match.groups()
|
||||
|
||||
if ml:
|
||||
x, y = float(x), float(y)
|
||||
last_x, last_y = x, y
|
||||
yield x-sr, y-sr
|
||||
yield x-sr, y+sr
|
||||
yield x+sr, y-sr
|
||||
yield x+sr, y+sr
|
||||
|
||||
elif a:
|
||||
rx, ry = float(rx), float(ry)
|
||||
ax, ay = float(ax), float(ay)
|
||||
angle = float(angle)
|
||||
large_arc = bool(int(large_arc))
|
||||
sweep = bool(int(sweep))
|
||||
|
||||
if not math.isclose(rx, ry, abs_tol=1e-6):
|
||||
raise ValueError("Elliptical arcs not supported. How did that end up here? KiCad can't do those either!")
|
||||
|
||||
mx = (last_x + ax)/2
|
||||
my = (last_y + ay)/2
|
||||
dx = ax - last_x
|
||||
dy = ay - last_y
|
||||
l = math.hypot(dx, dy)
|
||||
# clockwise normal
|
||||
nx = -dy/l
|
||||
ny = dx/l
|
||||
nl = math.sqrt(rx**2 - (l/2)**2)
|
||||
if sweep != large_arc:
|
||||
cx = mx + nx*nl
|
||||
cy = my + ny*nl
|
||||
else:
|
||||
cx = mx - nx*nl
|
||||
cy = my - ny*nl
|
||||
|
||||
(min_x, min_y), (max_x, max_y) = arc_bounds(last_x, last_y, ax, ay, cx-last_x, cy-last_y, clockwise=(not sweep))
|
||||
min_x -= sr
|
||||
min_y -= sr
|
||||
max_x += sr
|
||||
max_y += sr
|
||||
# dbg_i += 1
|
||||
# with open(f'/tmp/dbg-arc-{dbg_i}.svg', 'w') as f:
|
||||
# vbx, vby = min(last_x, ax), min(last_y, ay)
|
||||
# vbw, vbh = max(last_x, ax), max(last_y, ay)
|
||||
# vbw -= vbx
|
||||
# vbh -= vby
|
||||
# k = 2
|
||||
# vbx -= vbw*k
|
||||
# vby -= vbh*k
|
||||
# vbw *= 2*k+1
|
||||
# vbh *= 2*k+1
|
||||
# sw = min(vbw, vbh)*1e-3
|
||||
# mr = 3*sw
|
||||
# f.write('<?xml version="1.0" standalone="no"?>\n')
|
||||
# f.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||
# f.write(f'<svg version="1.1" width="200mm" height="200mm" viewBox="{vbx} {vby} {vbw} {vbh}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">>\n')
|
||||
# f.write(f'<path fill="none" stroke="#ff00ff" stroke-width="{sw}" d="{path_d}"/>\n')
|
||||
# f.write(f'<rect fill="none" stroke="#404040" stroke-width="{sw}" x="{min_x}" y="{min_y}" width="{max_x-min_x}" height="{max_y-min_y}"/>\n')
|
||||
# f.write(f'<circle fill="none" r="{mr}" stroke="blue" stroke-width="{sw}" cx="{last_x}" cy="{last_y}"/>\n')
|
||||
# f.write(f'<circle fill="none" r="{mr}" stroke="blue" stroke-width="{sw}" cx="{ax}" cy="{ay}"/>\n')
|
||||
# f.write(f'<circle fill="none" r="{mr}" stroke="red" stroke-width="{sw}" cx="{mx}" cy="{my}"/>\n')
|
||||
# f.write(f'<circle fill="none" r="{mr}" stroke="red" stroke-width="{sw}" cx="{cx}" cy="{cy}"/>\n')
|
||||
# f.write('</svg>\n')
|
||||
yield min_x, min_y
|
||||
yield min_x, max_y
|
||||
yield max_x, min_y
|
||||
yield max_x, max_y
|
||||
last_x, last_y = ax, ay
|
||||
|
||||
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
|
||||
|
|
@ -82,31 +174,35 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
|
|||
# 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)
|
||||
stack.add_layer('mechanical drawings')
|
||||
stack.add_layer('mechanical comments')
|
||||
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()}
|
||||
# Remove alpha since overlaid shapes won't work correctly with non-1 alpha without complicated svg filter hacks
|
||||
color_map = {key: (f'#{r:02x}{g:02x}{b:02x}', '1') for key, (r, g, b, _a) in color_map.items()}
|
||||
|
||||
margin = 10 # mm
|
||||
|
||||
layer = stack[('top', 'courtyard')]
|
||||
points = []
|
||||
bounds = []
|
||||
print('===== BOUNDS =====')
|
||||
for obj in layer.objects:
|
||||
if isinstance(obj, (go.Line, go.Arc)):
|
||||
points.append((obj.x1, obj.y1))
|
||||
points.append((obj.x2, obj.y2))
|
||||
bbox = (min_x, min_y), (max_x, max_y) = obj.bounding_box(unit=MM)
|
||||
import textwrap
|
||||
print(f'{min_x: 3.6f} {min_y: 3.6f} {max_x: 3.6f} {max_y: 3.6f}', '\n'.join(textwrap.wrap(str(obj), width=80, subsequent_indent=' '*(3+4*(3+1+6)))))
|
||||
bounds.append(bbox)
|
||||
print('===== END =====')
|
||||
|
||||
if not points:
|
||||
if not bounds:
|
||||
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)
|
||||
bounds = sum_bounds(bounds)
|
||||
(min_x, min_y), (max_x, max_y) = bounds
|
||||
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')
|
||||
|
|
@ -158,12 +254,37 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
|
|||
# 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:
|
||||
# fix up usvg width/height
|
||||
root = soup.find('svg')
|
||||
root['width'] = root_w
|
||||
root['height'] = root_h
|
||||
|
||||
# remove alpha to avoid complicated filter hacks
|
||||
for elem in root.descendants:
|
||||
if not isinstance(elem, bs4.Tag):
|
||||
continue
|
||||
|
||||
if elem.has_attr('opacity'):
|
||||
elem['opacity'] = '1'
|
||||
|
||||
if elem.has_attr('fill-opacity'):
|
||||
elem['fill-opacity'] = '1'
|
||||
|
||||
if elem.has_attr('stroke-opacity'):
|
||||
elem['stroke-opacity'] = '1'
|
||||
|
||||
# kicad-cli incorrectly fills arcs
|
||||
for elem in root.find_all('path'):
|
||||
if ' C ' in elem.get('d', '') and elem.get('stroke', 'none') != 'none':
|
||||
elem['fill'] = 'none'
|
||||
|
||||
# Move fabrication layers above drills because kicad-cli's svg rendering is wonky.
|
||||
with svg_soup(out_svg) as soup:
|
||||
root = soup.find('svg')
|
||||
root.append(soup.find('g', id='l-bottom-fabrication').extract())
|
||||
root.append(soup.find('g', id='l-top-fabrication').extract())
|
||||
|
||||
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'))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue