From a35125b123bb0c645f6e06c97287e2fb6ef2d6cb Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 26 Oct 2023 00:36:24 +0200 Subject: Fix all failing tests that don't involve kicad-cli --- gerbonara/cad/kicad/base_types.py | 29 ++++++-- gerbonara/cad/kicad/footprints.py | 38 +++++----- gerbonara/cad/kicad/graphical_primitives.py | 29 ++++---- gerbonara/cad/kicad/pcb.py | 14 ++-- gerbonara/cad/kicad/sexp_mapper.py | 103 +--------------------------- gerbonara/cad/kicad/symbols.py | 2 +- 6 files changed, 70 insertions(+), 145 deletions(-) (limited to 'gerbonara/cad') diff --git a/gerbonara/cad/kicad/base_types.py b/gerbonara/cad/kicad/base_types.py index 81eb0c4..32717fb 100644 --- a/gerbonara/cad/kicad/base_types.py +++ b/gerbonara/cad/kicad/base_types.py @@ -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) diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py index a95f36e..b24e004 100644 --- a/gerbonara/cad/kicad/footprints.py +++ b/gerbonara/cad/kicad/footprints.py @@ -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) diff --git a/gerbonara/cad/kicad/graphical_primitives.py b/gerbonara/cad/kicad/graphical_primitives.py index 568d1d2..a2393d2 100644 --- a/gerbonara/cad/kicad/graphical_primitives.py +++ b/gerbonara/cad/kicad/graphical_primitives.py @@ -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: diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py index f0e6f87..c5b2bd3 100644 --- a/gerbonara/cad/kicad/pcb.py +++ b/gerbonara/cad/kicad/pcb.py @@ -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): diff --git a/gerbonara/cad/kicad/sexp_mapper.py b/gerbonara/cad/kicad/sexp_mapper.py index 96727e4..d2d9d30 100644 --- a/gerbonara/cad/kicad/sexp_mapper.py +++ b/gerbonara/cad/kicad/sexp_mapper.py @@ -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) diff --git a/gerbonara/cad/kicad/symbols.py b/gerbonara/cad/kicad/symbols.py index a98fbeb..baa77bb 100644 --- a/gerbonara/cad/kicad/symbols.py +++ b/gerbonara/cad/kicad/symbols.py @@ -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) -- cgit