summaryrefslogtreecommitdiff
path: root/gerbonara/cad/kicad/schematic.py
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-07-20 16:42:05 +0200
committerjaseg <git@jaseg.de>2023-07-20 16:42:19 +0200
commitbdbdf7f58607bb98999e17ace8a743267a06cd9d (patch)
tree05a0b3d491d52782c8b96cfc4acbd13e44fa28e0 /gerbonara/cad/kicad/schematic.py
parenta1b8cbf86160eb2fbe73ffda61c953589ffa3512 (diff)
downloadgerbonara-bdbdf7f58607bb98999e17ace8a743267a06cd9d.tar.gz
gerbonara-bdbdf7f58607bb98999e17ace8a743267a06cd9d.tar.bz2
gerbonara-bdbdf7f58607bb98999e17ace8a743267a06cd9d.zip
Schematic rendering WIP
Diffstat (limited to 'gerbonara/cad/kicad/schematic.py')
-rw-r--r--gerbonara/cad/kicad/schematic.py240
1 files changed, 233 insertions, 7 deletions
diff --git a/gerbonara/cad/kicad/schematic.py b/gerbonara/cad/kicad/schematic.py
index 1bdbf38..846df2a 100644
--- a/gerbonara/cad/kicad/schematic.py
+++ b/gerbonara/cad/kicad/schematic.py
@@ -9,6 +9,7 @@ from itertools import chain
import re
import fnmatch
import os.path
+import warnings
from .sexp import *
from .base_types import *
@@ -23,8 +24,26 @@ from ... import graphic_objects as go
from ... import apertures as ap
from ...layers import LayerStack
from ...newstroke import Newstroke
-from ...utils import MM, rotate_point
-
+from ...utils import MM, rotate_point, Tag, setup_svg
+from .schematic_colors import *
+
+
+KICAD_PAPER_SIZES = {
+ 'A5': (210, 148),
+ 'A4': (297, 210),
+ 'A3': (420, 297),
+ 'A2': (594, 420),
+ 'A1': (841, 594),
+ 'A0': (1189, 841),
+ 'A': (11*25.4, 8.5*25.4),
+ 'B': (17*25.4, 11*15.4),
+ 'C': (22*25.4, 17*25.4),
+ 'D': (34*25.4, 22*25.4),
+ 'E': (44*25.4, 34*25.4),
+ 'USLetter': (11*25.4, 8.5*25.4),
+ 'USLegal': (14*25.4, 8.5*25.4),
+ 'USLedger': (17*25.4, 11*25.4),
+ }
@sexp_type('path')
class SheetPath:
@@ -39,12 +58,29 @@ class Junction:
color: Color = field(default_factory=lambda: Color(0, 0, 0, 0))
uuid: UUID = field(default_factory=UUID)
+ def bounding_box(self, default=None):
+ r = (self.diameter/2 or 0.635)
+ return (self.at.x - r, self.at.y - r), (self.at.x + r, self.at.y + r)
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ yield Tag('circle', cx=f'{self.at.x:.3f}', cy=f'{self.at.y:.3f}', r=(self.diameter/2 or 0.635),
+ fill=self.color.svg(colorscheme.wire))
+
@sexp_type('no_connect')
class NoConnect:
at: Rename(XYCoord) = field(default_factory=XYCoord)
uuid: UUID = field(default_factory=UUID)
+ def bounding_box(self, default=None):
+ r = 0.635
+ return (self.at.x - r, self.at.y - r), (self.at.x + r, self.at.y + r)
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ r = 0.635
+ yield Tag('path', d=f'M {-r:.3f} {-r:.3f} L {r:.3f} {r:.3f} M {-r:.3f} {r:.3f} L {r:.3f} {-r:.3f}',
+ fill='none', stroke_width='0.1', stroke=colorscheme.no_connect)
+
@sexp_type('bus_entry')
class BusEntry:
@@ -53,6 +89,44 @@ class BusEntry:
stroke: Stroke = field(default_factory=Stroke)
uuid: UUID = field(default_factory=UUID)
+ def bounding_box(self, default=None):
+ r = math.hypot(self.size.x, self.size.y)
+ x1, y1 = self.at.x, self.at.y
+ x2, y2 = rotate_point(x1+r, y1+r, self.at.rotation or 0)
+ x1, x2 = min(x1, x2), max(x1, x2)
+ y1, y2 = min(y1, y2), max(y1, y2)
+
+ r = (self.stroke.width or 0.254) / 2
+ return (x1-r, y1-r), (x2+r, y2+r)
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ yield Tag('path', d='M {self.at.x} {self.at.y} l {self.size.x} {self.size.y}',
+ transform=f'rotate({self.at.rotation or 0})',
+ fill='none', stroke=self.stroke.svg_color(colorscheme.bus), width=self.stroke.width or '0.254')
+
+
+def _polyline_svg(self, default_color):
+ da = Dasher(self)
+ if len(self.points.xy) < 2:
+ warnings.warn(f'Schematic {type(self)} with less than two points')
+
+ x0, y0, *rest = self.points.xy
+ da.move(x0, y0)
+ for xn, yn in rest:
+ da.line(xn, yn)
+
+ return da.svg(stroke=self.stroke.svg_color(default_color))
+
+
+def _polyline_bounds(self):
+ x1 = min(pt.x for pt in self.points)
+ y1 = min(pt.y for pt in self.points)
+ x2 = max(pt.x for pt in self.points)
+ y2 = max(pt.y for pt in self.points)
+
+ r = (self.stroke.width or 0.254) / 2
+ return (x1-r, y1-r), (x2+r, y2+r)
+
@sexp_type('wire')
class Wire:
@@ -60,6 +134,12 @@ class Wire:
stroke: Stroke = field(default_factory=Stroke)
uuid: UUID = field(default_factory=UUID)
+ def bounding_box(self, default=None):
+ return _polyline_bounds(self)
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ yield _polyline_svg(self, colorscheme.wire)
+
@sexp_type('bus')
class Bus:
@@ -67,6 +147,12 @@ class Bus:
stroke: Stroke = field(default_factory=Stroke)
uuid: UUID = field(default_factory=UUID)
+ def bounding_box(self, default=None):
+ return _polyline_bounds(self)
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ yield _polyline_svg(self, colorscheme.bus)
+
@sexp_type('polyline')
class Polyline:
@@ -74,27 +160,62 @@ class Polyline:
stroke: Stroke = field(default_factory=Stroke)
uuid: UUID = field(default_factory=UUID)
+ def bounding_box(self, default=None):
+ return _polyline_bounds(self)
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ yield _polyline_svg(self, colorscheme.lines)
+
@sexp_type('text')
-class Text:
+class Text(TextMixin):
text: str = ''
exclude_from_sim: Named(YesNoAtom()) = True
at: AtPos = field(default_factory=AtPos)
effects: TextEffect = field(default_factory=TextEffect)
uuid: UUID = field(default_factory=UUID)
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ yield from TextMixin.to_svg(self, colorscheme.text)
+
@sexp_type('label')
-class LocalLabel:
+class LocalLabel(TextMixin):
text: str = ''
at: AtPos = field(default_factory=AtPos)
fields_autoplaced: Wrap(Flag()) = False
effects: TextEffect = field(default_factory=TextEffect)
uuid: UUID = field(default_factory=UUID)
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ yield from TextMixin.to_svg(self, colorscheme.text)
+
+
+def label_shape_path_d(shape, w, h):
+ l, r = {
+ Atom.input: '<]',
+ Atom.output: '[>',
+ Atom.bidirectional: '<>',
+ Atom.tri_state: '<>',
+ Atom.passive: '[]'}.get(shape, '<]')
+ r = h/2
+
+ if l == '[':
+ d = 'M {r:.3f} {r:.3f} L 0 {r:.3f} L 0 {-r:.3f} L {r:.3f} {-r:.3f}'
+ else:
+ d = 'M {r:.3f} {r:.3f} L 0 0 L {r:.3f} {-r:.3f}'
+
+ e = w+r
+ d += ' L {e:.3f} {-r:.3f}'
+
+ if l == '[':
+ return d + 'L {e+r:.3f} {-r:.3f} L {e+r:.3f} {r:.3f} L {e:.3f} {r:.3f} Z'
+ else:
+ return d + 'L {e+r:.3f} {0:.3f} L {e:.3f} {r:.3f} Z'
+
@sexp_type('global_label')
-class GlobalLabel:
+class GlobalLabel(TextMixin):
text: str = ''
shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive)) = Atom.input
at: AtPos = field(default_factory=AtPos)
@@ -103,9 +224,17 @@ class GlobalLabel:
uuid: UUID = field(default_factory=UUID)
properties: List(Property) = field(default_factory=list)
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ text = super(TextMixin, self).to_svg(colorscheme.text),
+ text.attrs['transform'] = f'translate({self.size*0.6:.3f} 0)'
+ (x1, y1), (x2, y2) = self.bounding_box()
+ frame = Tag('path', fill='none', stroke_width=0.254, stroke=colorscheme.lines,
+ d=label_shape_path_d(self.shape, self.size*0.2 + y2-y1, self.size*1.2 + 0.254))
+ yield Tag('g', children=[frame, text])
+
@sexp_type('hierarchical_label')
-class HierarchicalLabel:
+class HierarchicalLabel(TextMixin):
text: str = ''
shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive)) = Atom.input
at: AtPos = field(default_factory=AtPos)
@@ -113,6 +242,13 @@ class HierarchicalLabel:
effects: TextEffect = field(default_factory=TextEffect)
uuid: UUID = field(default_factory=UUID)
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ text, = TextMixin.to_svg(self, colorscheme.text),
+ text.attrs['transform'] = f'translate({self.size*1.2:.3f} 0)'
+ frame = Tag('path', fill='none', stroke_width=0.254, stroke=colorscheme.lines,
+ d=label_shape_path_d(self.shape, self.size, self.size))
+ yield Tag('g', children=[frame, text])
+
@sexp_type('pin')
class Pin:
@@ -141,13 +277,26 @@ class MirrorFlags:
@sexp_type('property')
-class DrawnProperty:
+class DrawnProperty(TextMixin):
key: str = None
value: str = None
at: AtPos = field(default_factory=AtPos)
hide: Flag() = False
effects: TextEffect = field(default_factory=TextEffect)
+ # Alias value for text mixin
+ @property
+ def text(self):
+ return self.value
+
+ @text.setter
+ def text(self, value):
+ self.value = value
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ if not self.hide:
+ yield from TextMixin.to_svg(self, colorscheme.text)
+
@sexp_type('symbol')
class SymbolInstance:
@@ -168,6 +317,31 @@ class SymbolInstance:
# AFAICT this property, too, is completely redundant. It ultimately just lists paths and references of at most
# three other uses of the same symbol in this schematic.
instances: Named(List(SymbolCrosslinkProject)) = field(default_factory=list)
+ _ : SEXP_END = None
+ schematic: object = None
+
+ def __after_parse__(self, parent):
+ self.schematic = parent
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ children = []
+
+ for prop in self.properties:
+ children += prop.to_svg()
+
+ sym = self.schematic.lookup_symbol(self.lib_name, self.lib_id).raw_units[self.unit - 1]
+ for elem in sym.graphical_elements:
+ children += elem.to_svg(colorscheme)
+
+ xform = f'translate({self.at.x:.3f} {self.at.y:.3f})'
+ if self.at.rotation:
+ xform = f'rotate({self.at.rotation}) {xform}'
+ if self.mirror.x:
+ xform = f'scale(-1 1) {xform}'
+ if self.mirror.y:
+ xform = f'scale(1 -1) {xform}'
+
+ yield Tag('g', children=children, transform=xform, fill=colorscheme.fill, stroke=colorscheme.lines)
@sexp_type('path')
@@ -189,6 +363,11 @@ class SubsheetPin:
at: AtPos = field(default_factory=AtPos)
effects: TextEffect = field(default_factory=TextEffect)
uuid: UUID = field(default_factory=UUID)
+ _ : SEXP_END = None
+ subsheet: object = None
+
+ def __after_parse__(self, parent):
+ self.subsheet = parent
@sexp_type('fill')
@@ -235,6 +414,21 @@ class Subsheet:
return Schematic.open(resolved)
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ children = []
+
+ for prop in self._properties:
+ children += prop.to_svg(colorscheme)
+
+ # FIXME
+ #for elem in self.pins:
+ # children += pin.to_svg(colorscheme)
+
+ xform = f'translate({self.at.x:.3f} {self.at.y:.3f})'
+ yield Tag('g', children=children, transform=xform,
+ fill=self.fill.color.svg(colorscheme.fill),
+ **self.stroke.svg_attrs(colorscheme.lines))
+
@sexp_type('lib_symbols')
class LocalLibrary:
@@ -277,6 +471,14 @@ class Schematic:
if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
raise FormatError(f'File format version {value} is not supported. Supported versions are {", ".join(map(str, SUPPORTED_FILE_FORMAT_VERSIONS))}.')
+
+ def lookup_symbol(self, lib_name, lib_id):
+ key = lib_name or lib_id
+ for sym in self.lib_symbols.symbols:
+ if sym.name == key or sym.raw_name == key:
+ return sym
+ raise KeyError(f'Symbol with {lib_name=} {lib_id=} not found')
+
def write(self, filename=None):
with open(filename or self.original_filename, 'w') as f:
f.write(self.serialize())
@@ -292,6 +494,29 @@ class Schematic:
def load(kls, data, *args, **kwargs):
return kls.parse(data, *args, **kwargs)
+ @property
+ def elements(self):
+ yield from self.junctions
+ yield from self.no_connects
+ yield from self.bus_entries
+ yield from self.wires
+ yield from self.buses
+ yield from self.images
+ yield from self.polylines
+ yield from self.texts
+ yield from self.local_labels
+ yield from self.global_labels
+ yield from self.hierarchical_labels
+ yield from self.symbols
+ yield from self.subsheets
+
+ def to_svg(self, colorscheme=Colorscheme.KiCad):
+ children = []
+ for elem in self.elements:
+ children += elem.to_svg(colorscheme)
+ w, h = KICAD_PAPER_SIZES[self.page_settings.page_format]
+ return setup_svg(children, ((0, 0), (w, h)))
+
if __name__ == '__main__':
import sys
@@ -303,4 +528,5 @@ if __name__ == '__main__':
print('Loaded sub-sheet with', len(subsh.wires), 'wires and', len(subsh.symbols), 'symbols.')
sch.write('/tmp/test.kicad_sch')
+ Path('/tmp/test.svg').write_text(str(sch.to_svg()))