Fix all failing tests that don't involve kicad-cli

autoroute
jaseg 2023-10-26 00:36:24 +02:00
rodzic 31af2b260c
commit a35125b123
10 zmienionych plików z 78 dodań i 154 usunięć

Wyświetl plik

@ -196,8 +196,10 @@ class XYCoord:
x: float = 0
y: float = 0
def __init__(self, x=0, y=0):
if isinstance(x, XYCoord):
def __init__(self, x=None, y=None):
if x is None:
self.x, self.y = None, None
elif isinstance(x, XYCoord):
self.x, self.y = x.x, x.y
elif isinstance(x, (tuple, list)):
self.x, self.y = x
@ -227,6 +229,25 @@ class PointList:
xy : List(XYCoord) = field(default_factory=list)
@sexp_type('arc')
class Arc:
start: Rename(XYCoord) = None
mid: Rename(XYCoord) = None
end: Rename(XYCoord) = None
@sexp_type('pts')
class ArcPointList:
@classmethod
def __map__(kls, obj, parent=None):
_tag, *values = obj
return [map_sexp((XYCoord if elem[0] == 'xy' else Arc), elem, parent=parent) for elem in values]
@classmethod
def __sexp__(kls, value):
yield [kls.name_atom, *(e for elem in value for e in elem.__sexp__(elem))]
@sexp_type('xyz')
class XYZCoord:
x: float = 0
@ -322,7 +343,7 @@ class TextMixin:
x2 = max(max(l.x1, l.x2) for l in lines)
y2 = max(max(l.y1, l.y2) for l in lines)
r = self.effects.font.thickness/2
return (x1-r, y1-r), (x2+r, y2+r)
return (x1-r, -(y1-r)), (x2+r, -(y2+r))
def svg_path_data(self):
for line in self.render():
@ -409,7 +430,7 @@ class TextMixin:
x, y = x+offx, y+offy
x, y = rotate_point(x, y, math.radians(-rot or 0))
x, y = x+self.at.x, y+self.at.y
points.append((x, y))
points.append((x, -y))
for p1, p2 in zip(points[:-1], points[1:]):
yield go.Line(*p1, *p2, aperture=aperture, unit=MM)

Wyświetl plik

@ -105,7 +105,7 @@ class Line:
dasher.line(self.end.x, self.end.y)
for x1, y1, x2, y2 in dasher:
yield go.Line(x1, y1, x2, y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
yield go.Line(x1, -y1, x2, -y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
@sexp_type('fp_rect')
@ -127,7 +127,7 @@ class Rectangle:
w, h = x2-x1, y2-y1
if self.fill == Atom.solid:
yield go.Region.from_rectangle(x1, y1, w, h, unit=MM)
yield go.Region.from_rectangle(x1, -y1, w, h, unit=MM)
dasher = Dasher(self)
dasher.move(x1, y1)
@ -138,7 +138,7 @@ class Rectangle:
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)
yield go.Line(x1, -y1, x2, -y2, aperture=aperture, unit=MM)
@sexp_type('fp_circle')
@ -159,7 +159,7 @@ class Circle:
dasher = Dasher(self)
aperture = ap.CircleAperture(dasher.width or 0, unit=MM)
circle = go.Arc.from_circle(x, y, r, aperture=aperture, unit=MM)
circle = go.Arc.from_circle(x, -y, r, aperture=aperture, unit=MM)
if self.fill == Atom.solid:
yield circle.to_region()
@ -173,7 +173,7 @@ class Circle:
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)
yield go.Line(x1, -y1, x2, -y2, aperture=aperture, unit=MM)
@sexp_type('fp_arc')
@ -201,7 +201,7 @@ class Arc:
if math.isclose(x1, x2, abs_tol=1e-6) and math.isclose(y1, y2, abs_tol=1e-6):
cx = (x1 + mx) / 2
cy = (y1 + my) / 2
arc = go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, clockwise=True, 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
@ -211,7 +211,7 @@ class Arc:
dasher.segments.append((line.x1, line.y1, line.x2, line.y2))
for line in dasher:
yield go.Line(x1, y1, x2, y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
yield go.Line(x1, -y1, x2, -y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
else:
# https://stackoverflow.com/questions/56224824/how-do-i-find-the-circumcenter-of-the-triangle-using-python-without-external-lib
@ -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=False, aperture=aperture, unit=MM)
if dasher.solid:
yield arc
@ -230,7 +230,7 @@ class Arc:
dasher.segments.append((line.x1, line.y1, line.x2, line.y2))
for line in dasher:
yield go.Line(x1, y1, x2, y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
yield go.Line(x1, -y1, x2, -y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
@sexp_type('fp_poly')
@ -249,16 +249,16 @@ 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)
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)
yield go.Region([(pt.x, -pt.y) for pt in self.pts.xy], unit=MM)
@sexp_type('fp_curve')
@ -449,7 +449,7 @@ class Pad:
else:
aperture = self.aperture(margin)
yield go.Flash(self.at.x+ox, self.at.y+oy, aperture, unit=MM)
yield go.Flash(self.at.x+ox, -(self.at.y+oy), aperture, unit=MM)
def aperture(self, margin=None):
rotation = math.radians(self.at.rotation)
@ -581,14 +581,14 @@ class Pad:
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 = 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)
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)
yield go.Flash(self.at.x, -self.at.y, aperture=aperture, unit=MM)
@sexp_type('model')
@ -907,7 +907,7 @@ class Footprint:
for fe in obj.render(variables=variables):
fe.rotate(rotation)
fe.offset(x, y, MM)
fe.offset(x, -y, MM)
layer_stack[layer].objects.append(fe)
for obj in self.pads:
@ -939,7 +939,7 @@ class Footprint:
for fe in obj.render(margin=margin, cache=cache):
fe.rotate(rotation)
fe.offset(x, y, MM)
fe.offset(x, -y, MM)
if isinstance(fe, go.Flash) and fe.aperture:
fe.aperture = fe.aperture.rotated(rotation)
layer_stack[layer_map[layer]].objects.append(fe)
@ -947,7 +947,7 @@ class Footprint:
for obj in self.pads:
for fe in obj.render_drill():
fe.rotate(rotation)
fe.offset(x, y, MM)
fe.offset(x, -y, MM)
if obj.type == Atom.np_thru_hole:
layer_stack.drill_npth.append(fe)

Wyświetl plik

@ -53,7 +53,7 @@ class TextBox:
raise ValueError('Vector font text with empty render cache')
for poly in render_cache.polygons:
reg = go.Region([(p.x, p.y) for p in poly.pts.xy], unit=MM)
reg = go.Region([(p.x, -p.y) for p in poly.pts.xy], unit=MM)
if self.stroke:
if self.stroke.type not in (None, Atom.default, Atom.solid):
@ -91,7 +91,7 @@ class Line:
dasher.line(self.end.x, self.end.y)
for x1, y1, x2, y2 in dasher:
yield go.Line(x1, y1, x2, y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
yield go.Line(x1, -y1, x2, -y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
# FIXME render all primitives using dasher, maybe share code w/ fp_ prefix primitives
def offset(self, x=0, y=0):
@ -105,11 +105,11 @@ class FillMode:
fill: AtomChoice(Atom.solid, Atom.yes, Atom.no, Atom.none) = False
@classmethod
def __map__(self, obj, parent=None):
def __map__(kls, obj, parent=None):
return obj[1] in (Atom.solid, Atom.yes)
@classmethod
def __sexp__(self, value):
def __sexp__(kls, value):
yield [Atom.fill, Atom.solid if value else Atom.none]
@sexp_type('gr_rect')
@ -123,8 +123,8 @@ class Rectangle:
tstamp: Timestamp = None
def render(self, variables=None):
rect = go.Region.from_rectangle(self.start.x, self.start.y,
self.end.x-self.start.x, self.end.y-self.start.y,
rect = go.Region.from_rectangle(self.start.x, -self.start.y,
self.end.x-self.start.x, -(self.end.y-self.start.y),
unit=MM)
if self.fill:
@ -155,9 +155,9 @@ class Circle:
tstamp: Timestamp = None
def render(self, variables=None):
r = math.dist((self.center.x, self.center.y), (self.end.x, self.end.y))
r = math.dist((self.center.x, -self.center.y), (self.end.x, -self.end.y))
aperture = ap.CircleAperture(self.width or 0, unit=MM)
arc = go.Arc.from_circle(self.center.x, self.center.y, r, aperture=aperture, unit=MM)
arc = go.Arc.from_circle(self.center.x, -self.center.y, r, aperture=aperture, unit=MM)
if self.width:
# FIXME stroke support
@ -186,8 +186,11 @@ class Arc:
def __post_init__(self):
self.start = XYCoord(self.start)
self.end = XYCoord(self.end)
self.mid = XYCoord(self.mid) if self.mid else center_arc_to_kicad_mid(XYCoord(self.center), self.start, self.end)
self.center = None
if self.mid or self.center is None:
self.mid = XYCoord(self.mid)
elif self.center:
self.mid = center_arc_to_kicad_mid(XYCoord(self.center), self.start, self.end)
self.center = None
def rotate(self, angle, cx=None, cy=None):
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
@ -203,7 +206,7 @@ class Arc:
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)
yield go.Arc(x1, -y1, x2, -y2, cx-x1, -(cy-y1), aperture=aperture, clockwise=True, unit=MM)
def offset(self, x=0, y=0):
self.start = self.start.with_offset(x, y)
@ -213,7 +216,7 @@ class Arc:
@sexp_type('gr_poly')
class Polygon:
pts: PointList = field(default_factory=PointList)
pts: ArcPointList = field(default_factory=list)
layer: Named(str) = None
width: Named(float) = None
stroke: Stroke = field(default_factory=Stroke)
@ -221,7 +224,7 @@ 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)
reg = go.Region([(pt.x, -pt.y) for pt in self.pts.xy], unit=MM)
# FIXME stroke support
if self.width and self.width >= 0.005 or self.stroke.width and self.stroke.width > 0.005:

Wyświetl plik

@ -180,7 +180,7 @@ class TrackSegment:
return
aperture = ap.CircleAperture(self.width, unit=MM)
yield go.Line(self.start.x, self.start.y, self.end.x, self.end.y, aperture=aperture, unit=MM)
yield go.Line(self.start.x, -self.start.y, self.end.x, -self.end.y, aperture=aperture, unit=MM)
def rotate(self, angle, cx=None, cy=None):
if cx is None or cy is None:
@ -225,7 +225,7 @@ class TrackArc:
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)
yield go.Arc(x1, -y1, x2, -y2, cx-x1, -(cy-y1), aperture=aperture, clockwise=True, unit=MM)
def rotate(self, angle, cx=None, cy=None):
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
@ -287,11 +287,11 @@ class Via:
def render_drill(self):
aperture = ap.ExcellonTool(self.drill, plated=True, unit=MM)
yield go.Flash(self.at.x, self.at.y, aperture=aperture, unit=MM)
yield go.Flash(self.at.x, -self.at.y, aperture=aperture, unit=MM)
def render(self, variables=None, cache=None):
aperture = ap.CircleAperture(self.size, unit=MM)
yield go.Flash(self.at.x, self.at.y, aperture, unit=MM)
yield go.Flash(self.at.x, -self.at.y, aperture, unit=MM)
def rotate(self, angle, cx=None, cy=None):
if cx is None or cy is None:
@ -763,7 +763,7 @@ class Board:
for fe in obj.render(variables=variables):
fe.rotate(rotation)
fe.offset(x, y, MM)
fe.offset(x, -y, MM)
layer_stack[layer].objects.append(fe)
for obj in self.vias:
@ -771,13 +771,13 @@ class Board:
for layer in fnmatch.filter(layer_map, glob):
for fe in obj.render(cache=cache):
fe.rotate(rotation)
fe.offset(x, y, MM)
fe.offset(x, -y, MM)
fe.aperture = fe.aperture.rotated(rotation)
layer_stack[layer_map[layer]].objects.append(fe)
for fe in obj.render_drill():
fe.rotate(rotation)
fe.offset(x, y, MM)
fe.offset(x, -y, MM)
layer_stack.drill_pth.append(fe)
def bounding_box(self, unit=MM):

Wyświetl plik

@ -159,6 +159,8 @@ class Rename(WrapperType):
def __bind_field__(self, field):
if self.name_atom is None:
self.name_atom = Atom(field.name)
if hasattr(self.next_type, '__bind_field__'):
self.next_type.__bind_field__(field)
def __map__(self, obj, parent=None):
return map_sexp(self.next_type, obj, parent=parent)
@ -229,106 +231,6 @@ class Untagged(WrapperType):
_tag, *rest = inner
yield rest
class List(WrapperType):
def __bind_field__(self, field):
self.attr = field.name
def __map__(self, value, parent):
l = getattr(parent, self.attr, [])
mapped = map_sexp(self.next_type, value, parent=parent)
l.append(mapped)
setattr(parent, self.attr, l)
def __sexp__(self, value):
for elem in value:
yield from sexp(self.next_type, elem)
class _SexpTemplate:
@staticmethod
def __atoms__(kls):
return [kls.name_atom]
@staticmethod
def __map__(kls, value, *args, parent=None, **kwargs):
positional = iter(kls.positional)
inst = kls(*args, **kwargs)
for v in value[1:]: # skip key
if isinstance(v, Atom) and v in kls.keys:
name, etype = kls.keys[v]
mapped = map_sexp(etype, [v], parent=inst)
if mapped is not None:
setattr(inst, name, mapped)
elif isinstance(v, list):
name, etype = kls.keys[v[0]]
mapped = map_sexp(etype, v, parent=inst)
if mapped is not None:
setattr(inst, name, mapped)
else:
try:
pos_key = next(positional)
setattr(inst, pos_key.name, v)
except StopIteration:
raise TypeError(f'Unhandled positional argument {v!r} while parsing {kls}')
getattr(inst, '__after_parse__', lambda x: None)(parent)
return inst
@staticmethod
def __sexp__(kls, value):
getattr(value, '__before_sexp__', lambda: None)()
out = [kls.name_atom]
for f in fields(kls):
if f.type is SEXP_END:
break
out += sexp(f.type, getattr(value, f.name))
yield out
@staticmethod
def parse(kls, data, *args, **kwargs):
return kls.__map__(parse_sexp(data), *args, **kwargs)
@staticmethod
def sexp(self):
return next(self.__sexp__(self))
def sexp_type(name=None):
def register(cls):
cls = dataclass(cls)
cls.name_atom = Atom(name) if name is not None else None
for key in '__sexp__', '__map__', '__atoms__', 'parse':
if not hasattr(cls, key):
setattr(cls, key, classmethod(getattr(_SexpTemplate, key)))
if not hasattr(cls, 'sexp'):
setattr(cls, 'sexp', getattr(_SexpTemplate, 'sexp'))
cls.positional = []
cls.keys = {}
for f in fields(cls):
f_type = f.type
if f_type is SEXP_END:
break
if hasattr(f_type, '__bind_field__'):
f_type.__bind_field__(f)
atoms = getattr(f_type, '__atoms__', lambda: [])
atoms = list(atoms())
for atom in atoms:
cls.keys[atom] = (f.name, f_type)
if not atoms:
cls.positional.append(f)
return cls
return register
class List(WrapperType):
def __bind_field__(self, field):
self.attr = field.name
@ -406,7 +308,6 @@ class _SexpTemplate:
# those from being called more than once on the same object.
return replace(self, **{f.name: copy.copy(getattr(self, f.name)) for f in fields(self) if not f.kw_only and hasattr(f.type, '__before_sexp__')})
def sexp_type(name=None):
def register(cls):
cls = dataclass(cls)

Wyświetl plik

@ -501,7 +501,7 @@ class Symbol:
power: Wrap(Flag()) = False
pin_numbers: OmitDefault(PinNumberSpec) = field(default_factory=PinNumberSpec)
pin_names: OmitDefault(PinNameSpec) = field(default_factory=PinNameSpec)
exclude_from_sim: Named(YesNoAtom()) = False
exclude_from_sim: OmitDefault(Named(YesNoAtom())) = False
in_bom: Named(YesNoAtom()) = True
on_board: Named(YesNoAtom()) = True
properties: List(Property) = field(default_factory=list)

Wyświetl plik

@ -165,11 +165,10 @@ class ArcPoly(GraphicPrimitive):
yield f'M {float(self.outline[0][0]):.6} {float(self.outline[0][1]):.6}'
for old, new, arc in self.segments:
if not arc:
for old, new, (clockwise, center) in self.segments:
if clockwise is None:
yield f'L {float(new[0]):.6} {float(new[1]):.6}'
else:
clockwise, center = arc
yield svg_arc(old, new, center, clockwise)
def to_svg(self, fg='black', bg='white', tag=Tag):

Wyświetl plik

@ -736,7 +736,7 @@ class LayerStack:
tags = []
layer_transform = f'translate(0 {bounds[0][1] + bounds[1][1]}) scale(1 -1)'
for (side, use), layer in reversed(self.graphic_layers.items()):
if re.match(side_re, side) and (fg := colors.get(f'{side} {use}')):
if re.fullmatch(side_re, side) and (fg := colors.get(f'{side} {use}')):
tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
**stroke_attrs, id=f'l-{side}-{use}', transform=layer_transform))

Wyświetl plik

@ -194,9 +194,9 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
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 = {f'{side} {use}': KICAD_LAYER_COLORS[kicad_id] for (side, use), kicad_id in LAYER_MAP_G2K.items()}
color_map['drill pth'] = (255, 255, 255, 1)
color_map['drill npth'] = (255, 255, 255, 1)
# 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()}
@ -223,7 +223,7 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
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)))
out_svg.write_text(str(stack.to_svg(colors=color_map, force_bounds=bounds, margin=margin)))
print_on_error('Input footprint:', kicad_mod_file)
ref_svg = tmpfile('Reference render', '.svg')

Wyświetl plik

@ -530,7 +530,7 @@ def svg_arc(old, new, center, clockwise):
f'A {r:.6} {r:.6} 0 1 {sweep_flag} {float(new[0]):.6} {float(new[1]):.6}'
else: # normal case
d = point_line_distance(old, new, center[0], center[1])
d = point_line_distance(old, new, (center[0], center[1]))
large_arc = int((d < 0) == clockwise)
return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {float(new[0]):.6} {float(new[1]):.6}'