kicad: Extend query API
This commit is contained in:
parent
467e482bf4
commit
56d55fda5d
5 changed files with 158 additions and 36 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue