kicad: Extend query API

This commit is contained in:
jaseg 2023-06-29 19:47:31 +02:00
parent 467e482bf4
commit 56d55fda5d
5 changed files with 158 additions and 36 deletions

View file

@ -39,12 +39,6 @@ class Group:
members: Named(List(str)) = field(default_factory=list)
@sexp_type('property')
class Property:
key: str = ''
value: str = ''
@sexp_type('color')
class Color:
r: int = None
@ -248,6 +242,24 @@ class EditTime:
def bump(self):
self.value = time.time()
@sexp_type('property')
class Property:
key: str = ''
value: str = ''
@sexp_type('property')
class DrawnProperty:
key: str = None
value: str = None
id: Named(int) = None
at: AtPos = field(default_factory=AtPos)
layer: Named(str) = None
hide: Flag() = False
tstamp: Timestamp = None
effects: TextEffect = field(default_factory=TextEffect)
if __name__ == '__main__':
class Foo:
pass

View file

@ -30,12 +30,16 @@ from ...aperture_macros.parse import GenericMacros, ApertureMacro
from ...aperture_macros import primitive as amp
class _MISSING:
pass
@sexp_type('attr')
class Attribute:
type: AtomChoice(Atom.smd, Atom.through_hole) = None
board_only: Flag() = False
exclude_from_pos_files: Flag() = False
exclude_from_bom: Flag() = False
dnp: Flag() = False
@sexp_type('fp_text')
@ -378,7 +382,13 @@ class Pad:
thermal_gap: Named(float) = None
options: OmitDefault(CustomPadOptions) = None
primitives: OmitDefault(CustomPadPrimitives) = None
_: SEXP_END = None
footprint: object = None
def find_connected(self, **filters):
""" Find footprints connected to the same net as this pad """
return self.footprint.board.find_footprints(net=self.net.name, **filters)
def render(self, variables=None, margin=None, cache=None):
#if self.type in (Atom.connect, Atom.np_thru_hole):
# return
@ -562,8 +572,10 @@ class Footprint:
at: AtPos = field(default_factory=AtPos)
descr: Named(str) = None
tags: Named(str) = None
properties: List(Property) = field(default_factory=list)
properties: List(DrawnProperty) = field(default_factory=list)
path: Named(str) = None
sheetname: Named(str) = None
sheetfile: Named(str) = None
autoplace_cost90: Named(float) = None
autoplace_cost180: Named(float) = None
solder_mask_margin: Named(float) = None
@ -592,6 +604,26 @@ class Footprint:
_ : SEXP_END = None
original_filename: str = None
_bounding_box: tuple = None
board: object = None
def __after_parse__(self, parent):
self.properties = {prop.key: prop for prop in self.properties}
for pad in self.pads:
pad.footprint = self
def __before_sexp__(self):
self.properties = list(self.properties.values())
def property_value(self, key, default=_MISSING):
if default is not _MISSING and key not in self.properties:
return default
return self.properties[key].value
@property
def pads_by_number(self):
return {(int(pad.number) if pad.number.isnumeric() else pad.number): pad for pad in self.pads if pad.number}
@property
def version(self):
@ -633,6 +665,19 @@ class Footprint:
@property
def single_sided(self):
raise NotImplementedError()
def rotate(self, angle, cx=None, cy=None):
""" Rotate this footprint by the given angle in radians, counter-clockwise. When (cx, cy) are given, rotate
around the given coordinates in the global coordinate space. Otherwise rotate around the footprint's origin. """
if (cx, cy) != (None, None):
x, y = self.at.x-cx, self.at.y-cy
self.at.x = math.cos(angle)*x - math.sin(angle)*y + cx
self.at.y = math.sin(angle)*x + math.cos(angle)*y + cy
self.at.rotation -= math.degrees(angle)
for pad in self.pads:
pad.at.rotation -= math.degrees(angle)
def objects(self, text=False, pads=True):
return chain(

View file

@ -143,6 +143,7 @@ class Rectangle:
end: Rename(XYCoord) = None
layer: Named(str) = None
width: Named(float) = None
stroke: Stroke = field(default_factory=Stroke)
fill: FillMode = False
tstamp: Timestamp = None
@ -155,6 +156,7 @@ class Rectangle:
yield rect
if self.width:
# FIXME stroke support
yield from rect.outline_objects(aperture=ap.CircleAperture(self.width, unit=MM))
@ -164,6 +166,7 @@ class Circle:
end: Rename(XYCoord) = None
layer: Named(str) = None
width: Named(float) = None
stroke: Stroke = field(default_factory=Stroke)
fill: FillMode = False
tstamp: Timestamp = None
@ -173,6 +176,7 @@ class Circle:
arc = go.Arc.from_circle(self.center.x, self.center.y, r, aperture=aperture, unit=MM)
if self.width:
# FIXME stroke support
yield arc
if self.fill:
@ -190,6 +194,7 @@ class Arc:
tstamp: Timestamp = None
def render(self, variables=None):
# FIXME stroke support
if not self.width:
return
@ -212,6 +217,7 @@ class Polygon:
def render(self, variables=None):
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:
yield from reg.outline_objects(aperture=ap.CircleAperture(self.width, unit=MM))

View file

@ -5,6 +5,7 @@ Library for handling KiCad's PCB files (`*.kicad_mod`).
from pathlib import Path
from dataclasses import field
from itertools import chain
import re
import fnmatch
from .sexp import *
@ -23,6 +24,12 @@ from ...newstroke import Newstroke
from ...utils import MM
def match_filter(f, value):
if isinstance(f, str) and re.fullmatch(f, value):
return True
return value in f
@sexp_type('general')
class GeneralSection:
thickness: Named(float) = 1.60
@ -234,6 +241,39 @@ class Board:
original_filename: str = None
_bounding_box: tuple = None
def __after_parse__(self, parent):
self.properties = {prop.key: prop.value for prop in self.properties}
for fp in self.footprints:
fp.board = self
def __before_sexp__(self):
self.properties = [Property(key, value) for key, value in self.properties.items()]
def find_pads(self, net=None):
for fp in self.footprints:
for pad in fp.pads:
if net and not match_filter(net, pad.net.name):
continue
yield pad
def find_footprints(self, value=None, reference=None, name=None, net=None, sheetname=None, sheetfile=None):
for fp in self.footprints:
if name and not match_filter(name, fp.name):
continue
if value and not match_filter(value, fp.properties.get('value', '')):
continue
if reference and not match_filter(reference, fp.properties.get('reference', '')):
continue
if net and not any(match_filter(net, pad.net.name) for pad in fp.pads):
continue
if sheetname and not match_filter(sheetname, fp.sheetname):
continue
if sheetfile and not match_filter(sheetfile, fp.sheetfile):
continue
yield fp
@property
def version(self):
return self._version

View file

@ -1,4 +1,6 @@
import textwrap
from dataclasses import MISSING
from .sexp import *
@ -48,39 +50,58 @@ class Flag:
def sexp(t, v):
if v is None:
return []
elif t in (int, float, str, Atom):
return [t(v)]
elif hasattr(t, '__sexp__'):
return list(t.__sexp__(v))
elif isinstance(t, list):
t, = t
return [sexp(t, elem) for elem in v]
else:
raise TypeError(f'Python type {t} has no defined s-expression serialization')
try:
if v is None:
return []
elif t in (int, float, str, Atom):
return [t(v)]
elif hasattr(t, '__sexp__'):
return list(t.__sexp__(v))
elif isinstance(t, list):
t, = t
return [sexp(t, elem) for elem in v]
else:
raise TypeError(f'Python type {t} has no defined s-expression serialization')
except MappingError as e:
raise e
except Exception as e:
raise MappingError(f'Error trying to serialize {textwrap.shorten(str(v), width=120)} into type {t}', t, v) from e
class MappingError(TypeError):
def __init__(self, msg, t, sexp):
super().__init__(msg)
self.t, self.sexp = t, sexp
def map_sexp(t, v, parent=None):
if t is not Atom and hasattr(t, '__map__'):
return t.__map__(v, parent=parent)
try:
if t is not Atom and hasattr(t, '__map__'):
return t.__map__(v, parent=parent)
elif t in (int, float, str, Atom):
v, = v
if not isinstance(v, t):
types = set({type(v), t})
if types == {int, float} or types == {str, Atom}:
v = t(v)
else:
raise TypeError(f'Cannot map s-expression value {v} of type {type(v)} to Python type {t}')
return v
elif t in (int, float, str, Atom):
v, = v
if not isinstance(v, t):
types = set({type(v), t})
if types == {int, float} or types == {str, Atom}:
v = t(v)
else:
raise TypeError(f'Cannot map s-expression value {v} of type {type(v)} to Python type {t}')
return v
elif isinstance(t, list):
t, = t
return [map_sexp(t, elem, parent=parent) for elem in v]
elif isinstance(t, list):
t, = t
return [map_sexp(t, elem, parent=parent) for elem in v]
else:
raise TypeError(f'Python type {t} has no defined s-expression deserialization')
else:
raise TypeError(f'Python type {t} has no defined s-expression deserialization')
except MappingError as e:
raise e
except Exception as e:
raise MappingError(f'Error trying to map {textwrap.shorten(str(v), width=120)} into type {t}', t, v) from e
class WrapperType:
@ -301,8 +322,6 @@ def sexp_type(name=None):
return register
class List(WrapperType):
def __bind_field__(self, field):
self.attr = field.name