kopia lustrzana https://gitlab.com/gerbolyze/gerbonara
Fix failing test cases
rodzic
9624e46147
commit
36da1fd68b
|
@ -97,7 +97,8 @@ class Stroke:
|
|||
class Dasher:
|
||||
def __init__(self, obj):
|
||||
if obj.stroke:
|
||||
w, t = obj.stroke.width or 0.254, obj.stroke.type
|
||||
w = obj.stroke.width if obj.stroke.width is not None else 0.254
|
||||
t = obj.stroke.type
|
||||
else:
|
||||
w = obj.width or 0
|
||||
t = Atom.solid
|
||||
|
@ -210,6 +211,20 @@ class XYCoord:
|
|||
else:
|
||||
self.x, self.y = x, y
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.x, self.y))
|
||||
|
||||
def __getitem__(self, index):
|
||||
return (self.x, self.y)[index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if index == 0:
|
||||
self.x = value
|
||||
elif index == 1:
|
||||
self.y = value
|
||||
else:
|
||||
raise IndexError(f'Invalid 2D point coordinate index {index}')
|
||||
|
||||
def within_distance(self, x, y, dist):
|
||||
return math.dist((x, y), (self.x, self.y)) < dist
|
||||
|
||||
|
|
|
@ -220,7 +220,7 @@ class Arc:
|
|||
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=False, aperture=aperture, unit=MM)
|
||||
arc = go.Arc(x1, -y1, x2, -y2, cx-x1, -(cy-y1), clockwise=True, aperture=aperture, unit=MM)
|
||||
if dasher.solid:
|
||||
yield arc
|
||||
|
||||
|
@ -249,13 +249,14 @@ class Polygon:
|
|||
|
||||
dasher = Dasher(self)
|
||||
start = self.pts.xy[0]
|
||||
dasher.move(start.x, -start.y)
|
||||
dasher.move(start.x, start.y)
|
||||
for point in self.pts.xy[1:]:
|
||||
dasher.line(point.x, point.y)
|
||||
|
||||
aperture = ap.CircleAperture(dasher.width, unit=MM)
|
||||
for x1, y1, x2, y2 in dasher:
|
||||
yield go.Line(x1, -y1, x2, -y2, aperture=aperture, unit=MM)
|
||||
if dasher.width > 0:
|
||||
aperture = ap.CircleAperture(dasher.width, unit=MM)
|
||||
for x1, y1, x2, y2 in dasher:
|
||||
yield go.Line(x1, -y1, x2, -y2, aperture=aperture, unit=MM)
|
||||
|
||||
if self.fill == Atom.solid:
|
||||
yield go.Region([(pt.x, -pt.y) for pt in self.pts.xy], unit=MM)
|
||||
|
@ -466,10 +467,10 @@ class Pad:
|
|||
0, 0, # no hole
|
||||
rotation), unit=MM)
|
||||
else:
|
||||
return ap.RectangleAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(rotation)
|
||||
return ap.RectangleAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(-rotation)
|
||||
|
||||
elif self.shape == Atom.oval:
|
||||
return ap.ObroundAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(rotation)
|
||||
return ap.ObroundAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(-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
|
||||
|
@ -495,14 +496,14 @@ class Pad:
|
|||
(x+dy+2*margin*math.cos(alpha), y+2*margin,
|
||||
2*dy,
|
||||
0, 0, # no hole
|
||||
rotation), unit=MM)
|
||||
-rotation + math.pi), unit=MM)
|
||||
|
||||
else:
|
||||
return ap.ApertureMacroInstance(GenericMacros.rounded_isosceles_trapezoid,
|
||||
(x+dy, y,
|
||||
2*dy, margin,
|
||||
0, 0, # no hole
|
||||
rotation), unit=MM)
|
||||
-rotation + math.pi), unit=MM)
|
||||
|
||||
elif self.shape == Atom.roundrect:
|
||||
x, y = self.size.x, self.size.y
|
||||
|
@ -514,7 +515,7 @@ class Pad:
|
|||
0, 0, # no hole
|
||||
rotation), unit=MM)
|
||||
else:
|
||||
return ap.RectangleAperture(x+margin, y+margin, unit=MM).rotated(rotation)
|
||||
return ap.RectangleAperture(x+margin, y+margin, unit=MM).rotated(-rotation)
|
||||
|
||||
elif self.shape == Atom.custom:
|
||||
primitives = []
|
||||
|
@ -556,7 +557,7 @@ class Pad:
|
|||
elif self.options.anchor == Atom.circle and self.size.x > 0:
|
||||
primitives.append(amp.Circle(MM, 1, self.size.x+2*margin, 0, 0, 0))
|
||||
|
||||
macro = ApertureMacro(primitives=tuple(primitives)).rotated(rotation)
|
||||
macro = ApertureMacro(primitives=tuple(primitives)).rotated(-rotation)
|
||||
return ap.ApertureMacroInstance(macro, unit=MM)
|
||||
|
||||
def render_drill(self):
|
||||
|
@ -881,7 +882,7 @@ class Footprint:
|
|||
for text in self.texts:
|
||||
text.at.rotation = (text.at.rotation + delta) % 360
|
||||
|
||||
def objects(self, text=False, pads=True, groups=True):
|
||||
def objects(self, text=False, pads=True, groups=True, zones=True):
|
||||
return chain(
|
||||
(self.texts if text else []),
|
||||
(self.text_boxes if text else []),
|
||||
|
@ -893,7 +894,7 @@ class Footprint:
|
|||
self.curves,
|
||||
(self.dimensions if text else []),
|
||||
(self.pads if pads else []),
|
||||
self.zones,
|
||||
(self.zones if zones else []),
|
||||
self.groups if groups else [])
|
||||
|
||||
def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, flip=False, variables={}, cache=None):
|
||||
|
@ -901,7 +902,7 @@ class Footprint:
|
|||
y += self.at.y
|
||||
rotation += math.radians(self.at.rotation)
|
||||
|
||||
for obj in self.objects(pads=False, text=text):
|
||||
for obj in self.objects(pads=False, text=text, zones=False):
|
||||
if not (layer := layer_map.get(obj.layer)):
|
||||
continue
|
||||
|
||||
|
|
|
@ -203,10 +203,10 @@ class Arc:
|
|||
return
|
||||
|
||||
aperture = ap.CircleAperture(self.width, unit=MM)
|
||||
cx, cy = self.mid.x, self.mid.y
|
||||
x1, y1 = self.start.x, self.start.y
|
||||
x2, y2 = self.end.x, self.end.y
|
||||
yield go.Arc(x1, -y1, x2, -y2, cx-x1, -(cy-y1), aperture=aperture, clockwise=True, unit=MM)
|
||||
(cx, cy), _r = kicad_mid_to_center_arc(self.mid, self.start, self.end)
|
||||
yield go.Arc(x1, -y1, x2, -y2, cx-x1, -(cy-y1), aperture=aperture, clockwise=False, unit=MM)
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
|
@ -224,7 +224,23 @@ class Polygon:
|
|||
tstamp: Timestamp = None
|
||||
|
||||
def render(self, variables=None):
|
||||
reg = go.Region([(pt.x, -pt.y) for pt in self.pts.xy], unit=MM)
|
||||
points = []
|
||||
centers = []
|
||||
for point_or_arc in self.pts:
|
||||
if points:
|
||||
centers.append((None, (None, None)))
|
||||
|
||||
if isinstance(point_or_arc, XYCoord):
|
||||
points.append((point_or_arc.x, -point_or_arc.y))
|
||||
|
||||
else: # base_types.Arc
|
||||
points.append((point_or_arc.start.x, -point_or_arc.start.y))
|
||||
points.append((point_or_arc.end.x, -point_or_arc.end.y))
|
||||
(cx, cy), _r = kicad_mid_to_center_arc(point_or_arc.mid, point_or_arc.start, point_or_arc.end)
|
||||
centers.append((False, (cx, -cy)))
|
||||
|
||||
reg = go.Region(points, centers, unit=MM)
|
||||
reg.close()
|
||||
|
||||
# FIXME stroke support
|
||||
if self.width and self.width >= 0.005 or self.stroke.width and self.stroke.width > 0.005:
|
||||
|
|
|
@ -59,6 +59,31 @@ def center_arc_to_kicad_mid(center, start, end):
|
|||
return XYCoord(mx, my)
|
||||
|
||||
|
||||
def kicad_mid_to_center_arc(mid, start, end):
|
||||
""" Convert kicad's slightly insane midpoint notation to standrad center/p1/p2 notation.
|
||||
|
||||
Returns the center and radius of the circle passing the given 3 points.
|
||||
In case the 3 points form a line, raises a ValueError.
|
||||
"""
|
||||
# https://stackoverflow.com/questions/28910718/give-3-points-and-a-plot-circle
|
||||
p1, p2, p3 = start, mid, end
|
||||
|
||||
temp = p2[0] * p2[0] + p2[1] * p2[1]
|
||||
bc = (p1[0] * p1[0] + p1[1] * p1[1] - temp) / 2
|
||||
cd = (temp - p3[0] * p3[0] - p3[1] * p3[1]) / 2
|
||||
det = (p1[0] - p2[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p2[1])
|
||||
|
||||
if abs(det) < 1.0e-6:
|
||||
raise ValueError()
|
||||
|
||||
# Center of circle
|
||||
cx = (bc*(p2[1] - p3[1]) - cd*(p1[1] - p2[1])) / det
|
||||
cy = ((p1[0] - p2[0]) * cd - (p2[0] - p3[0]) * bc) / det
|
||||
|
||||
radius = math.sqrt((cx - p1[0])**2 + (cy - p1[1])**2)
|
||||
return ((cx, cy), radius)
|
||||
|
||||
|
||||
@sexp_type('hatch')
|
||||
class Hatch:
|
||||
style: AtomChoice(Atom.none, Atom.edge, Atom.full) = Atom.edge
|
||||
|
|
|
@ -20,6 +20,7 @@ from .base_types import *
|
|||
from ...utils import rotate_point, Tag, arc_bounds
|
||||
from ...newstroke import Newstroke
|
||||
from .schematic_colors import *
|
||||
from .primitives import center_arc_to_kicad_mid
|
||||
|
||||
|
||||
PIN_ETYPE = AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive, Atom.free,
|
||||
|
@ -249,28 +250,6 @@ class Circle:
|
|||
**self.stroke.svg_attrs(colorscheme.lines))
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/28910718/give-3-points-and-a-plot-circle
|
||||
def define_circle(p1, p2, p3):
|
||||
"""
|
||||
Returns the center and radius of the circle passing the given 3 points.
|
||||
In case the 3 points form a line, raises a ValueError.
|
||||
"""
|
||||
temp = p2[0] * p2[0] + p2[1] * p2[1]
|
||||
bc = (p1[0] * p1[0] + p1[1] * p1[1] - temp) / 2
|
||||
cd = (temp - p3[0] * p3[0] - p3[1] * p3[1]) / 2
|
||||
det = (p1[0] - p2[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p2[1])
|
||||
|
||||
if abs(det) < 1.0e-6:
|
||||
raise ValueError()
|
||||
|
||||
# Center of circle
|
||||
cx = (bc*(p2[1] - p3[1]) - cd*(p1[1] - p2[1])) / det
|
||||
cy = ((p1[0] - p2[0]) * cd - (p2[0] - p3[0]) * bc) / det
|
||||
|
||||
radius = math.sqrt((cx - p1[0])**2 + (cy - p1[1])**2)
|
||||
return ((cx, cy), radius)
|
||||
|
||||
|
||||
@sexp_type('arc')
|
||||
class Arc:
|
||||
start: Rename(XYCoord) = field(default_factory=XYCoord)
|
||||
|
@ -280,7 +259,7 @@ class Arc:
|
|||
fill: Fill = field(default_factory=Fill)
|
||||
|
||||
def bounding_box(self, default=None):
|
||||
(cx, cy), r = define_circle((self.start.x, self.start.y), (self.mid.x, self.mid.y), (self.end.x, self.end.y))
|
||||
(cx, cy), r = center_arc_to_kicad_mid(self.mid, self.start, self.end)
|
||||
x1, y1 = self.start.x, self.start.y
|
||||
x2, y2 = self.mid.x-x1, self.mid.y-x2
|
||||
x3, y3 = (self.end.x - x1)/2, (self.end.y - y1)/2
|
||||
|
@ -289,7 +268,7 @@ class Arc:
|
|||
|
||||
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
(cx, cy), r = define_circle((self.start.x, self.start.y), (self.mid.x, self.mid.y), (self.end.x, self.end.y))
|
||||
(cx, cy), r = center_arc_to_kicad_mid(self.mid, self.start, self.end)
|
||||
|
||||
x1r = self.start.x - cx
|
||||
y1r = self.start.y - cy
|
||||
|
|
|
@ -294,6 +294,7 @@ class Region(GraphicObject):
|
|||
self.polarity_dark = polarity_dark
|
||||
self.outline = [] if outline is None else outline
|
||||
self.arc_centers = [] if arc_centers is None else arc_centers
|
||||
self.close()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.outline)
|
||||
|
@ -319,6 +320,12 @@ class Region(GraphicObject):
|
|||
(arc[0], (arc[1][0]*factor, arc[1][1]*factor)) if arc else None
|
||||
for p, arc in zip_longest(self.outline, self.arc_centers) ]
|
||||
|
||||
def close(self):
|
||||
if self.outline and self.outline[-1] != self.outline[0]:
|
||||
self.outline.append(self.outline[-1])
|
||||
if self.arc_centers:
|
||||
self.arc_centers.append((None, (None, None)))
|
||||
|
||||
@classmethod
|
||||
def from_rectangle(kls, x, y, w, h, unit=MM):
|
||||
return kls([
|
||||
|
@ -364,7 +371,7 @@ class Region(GraphicObject):
|
|||
|
||||
def outline_objects(self, aperture=None):
|
||||
for p1, p2, (clockwise, center) in self.iter_segments():
|
||||
if center:
|
||||
if clockwise is not None:
|
||||
yield Arc(*p1, *p2, *center, clockwise, aperture=aperture, unit=self.unit, polarity_dark=self.polarity_dark)
|
||||
else:
|
||||
yield Line(*p1, *p2, aperture=aperture, unit=self.unit, polarity_dark=self.polarity_dark)
|
||||
|
@ -377,13 +384,17 @@ class Region(GraphicObject):
|
|||
|
||||
points = []
|
||||
for p1, p2, (clockwise, center) in self.iter_segments():
|
||||
if center:
|
||||
if clockwise is not None:
|
||||
for p in approximate_arc(*center, *p1, *p2, clockwise,
|
||||
max_error=max_error, clip_max_error=clip_max_error):
|
||||
points.append(p)
|
||||
points.pop()
|
||||
else:
|
||||
points.append(p1)
|
||||
points.append(p2)
|
||||
|
||||
if points[0] != points[-1]:
|
||||
points.append(points[0])
|
||||
|
||||
yield amp.Outline(self.unit, int(self.polarity_dark), len(points)-1, tuple(coord for p in points for coord in p))
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ from pathlib import Path
|
|||
import tempfile
|
||||
import textwrap
|
||||
import os
|
||||
import stat
|
||||
from functools import total_ordering
|
||||
import shutil
|
||||
import bs4
|
||||
|
@ -156,10 +157,14 @@ def kicad_fp_export(mod_file, out_svg):
|
|||
print(f'Building cache for {mod_file.name}')
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
os.chmod(tmpdir, 0o1777)
|
||||
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)
|
||||
cmd = ['podman', 'run', '--mount', f'type=bind,src={pretty_dir},dst=/{pretty_dir.name}',
|
||||
'--mount', f'type=bind,src={tmpdir},dst=/out',
|
||||
'registry.gitlab.com/kicad/kicad-ci/kicad-cli-docker/kicad:nightly',
|
||||
'kicad-cli', 'fp', 'export', 'svg', '--output', '/out', '--footprint', fp_name, f'/{pretty_dir.name}']
|
||||
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:
|
||||
|
|
|
@ -282,6 +282,10 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
|
|||
root['width'] = root_w
|
||||
root['height'] = root_h
|
||||
|
||||
#for elem in root.find_all('path'):
|
||||
# if elem.attrs.get('fill', '').lower() == '#d864ff' and math.isclose(float(elem.attrs.get('fill-opacity', 0)), 0.4):
|
||||
# elem.decompose()
|
||||
|
||||
# remove alpha to avoid complicated filter hacks
|
||||
for elem in root.descendants:
|
||||
if not isinstance(elem, bs4.Tag):
|
||||
|
@ -312,8 +316,8 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
|
|||
mean, _max, hist = svg_difference(ref_svg, out_svg, dpi=600, diff_out=tmpfile('Difference', '.png'))
|
||||
|
||||
# compensate for circular pads aliasing badly
|
||||
aliasing_artifacts = 1e-4 * len(fp.sexp.pads)/50
|
||||
assert mean < 1e-3 + aliasing_artifacts
|
||||
aliasing_artifacts = 1e-3 * len(fp.sexp.pads)/10
|
||||
assert mean < 3e-3 + aliasing_artifacts
|
||||
assert hist[9] < 100
|
||||
assert hist[3:].sum() < (1e-3 + 10*aliasing_artifacts)*hist.size
|
||||
|
||||
|
|
|
@ -286,9 +286,9 @@ def approximate_arc(cx, cy, x1, y1, x2, y2, clockwise, max_error=1e-2, clip_max_
|
|||
l = math.sqrt(r**2 - (r - max_error)**2)
|
||||
|
||||
angle_max = math.asin(l/r)
|
||||
sweep_angle = sweep_angle(cx, cy, x1, y1, x2, y2, clockwise)
|
||||
num_segments = math.ceil(sweep_angle / angle_max)
|
||||
angle = sweep_angle / num_segments
|
||||
alpha = sweep_angle(cx, cy, x1, y1, x2, y2, clockwise)
|
||||
num_segments = math.ceil(alpha / angle_max)
|
||||
angle = alpha / num_segments
|
||||
|
||||
if not clockwise:
|
||||
angle = -angle
|
||||
|
|
Ładowanie…
Reference in New Issue