diff --git a/src/gerbonara/cad/kicad/graphical_primitives.py b/src/gerbonara/cad/kicad/graphical_primitives.py index 29ff9b3..f62b0ef 100644 --- a/src/gerbonara/cad/kicad/graphical_primitives.py +++ b/src/gerbonara/cad/kicad/graphical_primitives.py @@ -1,6 +1,8 @@ import string import math +import base64 +import textwrap from .sexp import * from .base_types import * @@ -325,13 +327,28 @@ class DimensionStyle: keep_text_aligned: Flag() = False +@sexp_type('data') +class Base64Blob: + @classmethod + def __map__(kls, obj, parent=None, path=''): + _data, *content = obj + for x in content[:10]: + print(str(x)) + return base64.b64decode(''.join(map(str, content))) + + @classmethod + def __sexp__(kls, value): + encoded = base64.b64encode(value).decode() + yield [Atom.data, *textwrap.wrap(encoded, 76)] + + @sexp_type('image') class Image: at: AtPos = field(default_factory=AtPos) scale: Named(float) = None layer: Named(str) = None uuid: UUID = field(default_factory=UUID) - data: str = '' + data: Base64Blob = '' def offset(self, x=0, y=0): self.at = self.at.with_offset(x, y) diff --git a/src/gerbonara/cad/kicad/schematic.py b/src/gerbonara/cad/kicad/schematic.py index 0727c45..48eadf4 100644 --- a/src/gerbonara/cad/kicad/schematic.py +++ b/src/gerbonara/cad/kicad/schematic.py @@ -85,6 +85,12 @@ class NoConnect: fill='none', stroke_width='0.254', stroke=colorscheme.no_connect) +@sexp_type('bus_alias') +class BusAlias: + name: str = '' + members: Named(Array(str)) = field(default_factory=list) + + @sexp_type('bus_entry') class BusEntry: at: AtPos = field(default_factory=AtPos) @@ -171,6 +177,24 @@ class Polyline: yield _polyline_svg(self, colorscheme.lines) +@sexp_type('circle') +class Circle: + center: Rename(XYCoord) = field(default_factory=XYCoord) + radius: Named(float) = 0.0 + stroke: Stroke = field(default_factory=Stroke) + fill: OmitDefault(Fill) = None + uuid: UUID = field(default_factory=UUID) + + +@sexp_type('rectangle') +class Rectangle: + start: Rename(XYCoord) = field(default_factory=XYCoord) + end: Rename(XYCoord) = field(default_factory=XYCoord) + stroke: Stroke = field(default_factory=Stroke) + fill: OmitDefault(Fill) = None + uuid: UUID = field(default_factory=UUID) + + def label_shape_path_d(shape, w, h): l, r = { Atom.input: '<]', @@ -578,12 +602,28 @@ class TextBox(TextMixin): yield from gr.TextBox.render(self, variables=variables) +@sexp_type('comment') +class TitleComment: + @classmethod + def __map__(kls, obj, parent=None, path=''): + return '\n'.join(obj[2::2]) + + @classmethod + def __sexp__(kls, value): + l = [Atom.comment] + for i, line in enumerate(value.splitlines(), start=1): + l.append(i) + l.append(line.rstrip('\n')) + return l + + @sexp_type('title_block') class TitleBlock: title: Named(str) = '' date: Named(str) = '' rev: Named(str) = '' company: Named(str) = '' + comment: TitleComment = None @sexp_type('lib_symbols') @@ -597,8 +637,11 @@ class Schematic: _version: Named(int, name='version') = 20230620 generator: Named(str) = 'gerbonara' generator_version: Named(str) = __version__ + legacy_generator: Named(Array(str), name='host') = None uuid: UUID = field(default_factory=UUID) page_settings: PageSettings = field(default_factory=PageSettings) + legacy_page: Named(Array(int), name='page') = None + legacy_paper: Named(str, name='paper') = None title_block: TitleBlock = None # The doc says this is expected, but eeschema barfs when it's there. # path: SheetPath = field(default_factory=SheetPath) @@ -607,12 +650,14 @@ class Schematic: no_connects: List(NoConnect) = field(default_factory=list) rule_areas: List(RuleArea) = field(default_factory=list) netclass_flags: List(NetclassFlag) = field(default_factory=list) + bus_aliases: List(BusAlias) = field(default_factory=list) bus_entries: List(BusEntry) = field(default_factory=list) wires: List(Wire) = field(default_factory=list) buses: List(Bus) = field(default_factory=list) images: List(gr.Image) = field(default_factory=list) polylines: List(Polyline) = field(default_factory=list) - circles: List(gr.Circle) = field(default_factory=list) + circles: List(Circle) = field(default_factory=list) + rectangles: List(Rectangle) = field(default_factory=list) texts: List(Text) = field(default_factory=list) text_boxes: List(TextBox) = field(default_factory=list) local_labels: List(LocalLabel) = field(default_factory=list) @@ -620,7 +665,7 @@ class Schematic: hierarchical_labels: List(HierarchicalLabel) = field(default_factory=list) symbols: List(SymbolInstance) = field(default_factory=list) subsheets: List(Subsheet) = field(default_factory=list) - sheet_instances: Named(List(SubsheetCrosslinkSheet)) = field(default_factory=list) + sheet_instances: Named(Array(SubsheetCrosslinkSheet)) = field(default_factory=list) symbol_instances: Named(Array(SymbolCrosslinkProject)) = field(default_factory=list) embedded_fonts: Named(YesNoAtom()) = False _ : SEXP_END = None diff --git a/src/gerbonara/cad/kicad/sexp.py b/src/gerbonara/cad/kicad/sexp.py index 9312489..edc31c2 100644 --- a/src/gerbonara/cad/kicad/sexp.py +++ b/src/gerbonara/cad/kicad/sexp.py @@ -64,7 +64,7 @@ term_regex = r"""(?mx) (\))| ([+-]?\d+\.\d+(?=[\s\)]))| (\-?\d+(?=[\s\)]))| - ([^0-9"\s()][^"\s)]*) + ([^"\s()][^"\s)]*) )""" diff --git a/src/gerbonara/cad/kicad/symbols.py b/src/gerbonara/cad/kicad/symbols.py index ede2a7c..c742e08 100644 --- a/src/gerbonara/cad/kicad/symbols.py +++ b/src/gerbonara/cad/kicad/symbols.py @@ -26,7 +26,7 @@ from .primitives import kicad_mid_to_center_arc, Margins PIN_ETYPE = AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive, Atom.free, Atom.unspecified, Atom.power_in, Atom.power_out, Atom.open_collector, Atom.open_emitter, - Atom.no_connect) + Atom.no_connect, Atom.unconnected) PIN_STYLE = AtomChoice(Atom.line, Atom.inverted, Atom.clock, Atom.inverted_clock, Atom.input_low, Atom.clock_low, @@ -251,11 +251,19 @@ class Circle: **self.stroke.svg_attrs(colorscheme.lines)) +@sexp_type('radius') +class ArcRadius: + at: AtPos = field(default_factory=AtPos) + length: Named(float) = 0.0 + angles: Rename(XYCoord) = field(default_factory=XYCoord) + + @sexp_type('arc') class Arc: start: Rename(XYCoord) = field(default_factory=XYCoord) mid: Rename(XYCoord) = field(default_factory=XYCoord) end: Rename(XYCoord) = field(default_factory=XYCoord) + radius: ArcRadius = None stroke: Stroke = field(default_factory=Stroke) fill: Fill = field(default_factory=Fill) @@ -419,13 +427,13 @@ class Property(TextMixin): @sexp_type('pin_numbers') class PinNumberSpec: - hide: Flag() = False + hide: Named(YesNoAtom()) = False @sexp_type('pin_names') class PinNameSpec: offset: OmitDefault(Named(float)) = 0.508 - hide: Flag() = False + hide: OmitDefault(Named(YesNoAtom())) = False @sexp_type('text_box') class TextBox: diff --git a/tests/test_kicad_symbols.py b/tests/test_kicad_symbols.py index 2df9784..8553a37 100644 --- a/tests/test_kicad_symbols.py +++ b/tests/test_kicad_symbols.py @@ -98,6 +98,14 @@ def test_round_trip(kicad_library_file, tmpfile): j += 1 continue + if original != stage1: + # Make pytest output more useful error messages + context_original = original_lines[max(0, i-10):min(i+10, len(original_lines)-1)] + context_original = '\n'.join(context_original) + context_stage1 = stage1_lines[max(0, i-10):min(i+10, len(stage1_lines)-1)] + context_stage1 = '\n'.join(context_stage1) + assert context_original == context_stage1 + assert original == stage1 i, j = i+1, j+1