summaryrefslogtreecommitdiff
path: root/gerbonara/cad
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/cad')
-rw-r--r--gerbonara/cad/kicad/base_types.py24
-rw-r--r--gerbonara/cad/kicad/footprints.py47
-rw-r--r--gerbonara/cad/kicad/graphical_primitives.py6
-rw-r--r--gerbonara/cad/kicad/pcb.py40
-rw-r--r--gerbonara/cad/kicad/sexp_mapper.py79
5 files changed, 159 insertions, 37 deletions
diff --git a/gerbonara/cad/kicad/base_types.py b/gerbonara/cad/kicad/base_types.py
index 6bb5912..1606fa4 100644
--- a/gerbonara/cad/kicad/base_types.py
+++ b/gerbonara/cad/kicad/base_types.py
@@ -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
diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py
index 2f8abc4..29c947e 100644
--- a/gerbonara/cad/kicad/footprints.py
+++ b/gerbonara/cad/kicad/footprints.py
@@ -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(
diff --git a/gerbonara/cad/kicad/graphical_primitives.py b/gerbonara/cad/kicad/graphical_primitives.py
index 8e2d325..752c9bc 100644
--- a/gerbonara/cad/kicad/graphical_primitives.py
+++ b/gerbonara/cad/kicad/graphical_primitives.py
@@ -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))
diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py
index 8a90a7e..22ad23e 100644
--- a/gerbonara/cad/kicad/pcb.py
+++ b/gerbonara/cad/kicad/pcb.py
@@ -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
diff --git a/gerbonara/cad/kicad/sexp_mapper.py b/gerbonara/cad/kicad/sexp_mapper.py
index 0f52340..ca000c3 100644
--- a/gerbonara/cad/kicad/sexp_mapper.py
+++ b/gerbonara/cad/kicad/sexp_mapper.py
@@ -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)
-
- 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
+ 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 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')
- elif isinstance(t, list):
- t, = t
- return [map_sexp(t, elem, parent=parent) for elem in v]
+ except MappingError as e:
+ raise e
- else:
- raise TypeError(f'Python type {t} has no defined s-expression deserialization')
+ 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