summaryrefslogtreecommitdiff
path: root/gerbonara/cad/kicad
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/cad/kicad')
-rw-r--r--gerbonara/cad/kicad/base_types.py29
-rw-r--r--gerbonara/cad/kicad/footprints.py38
-rw-r--r--gerbonara/cad/kicad/graphical_primitives.py29
-rw-r--r--gerbonara/cad/kicad/pcb.py14
-rw-r--r--gerbonara/cad/kicad/sexp_mapper.py103
-rw-r--r--gerbonara/cad/kicad/symbols.py2
6 files changed, 70 insertions, 145 deletions
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)