kiacd 9.0 compatibility WIP
This commit is contained in:
parent
75905f7d0c
commit
9d2d635eee
7 changed files with 161 additions and 51 deletions
|
|
@ -65,6 +65,7 @@ testpaths = ["tests"]
|
|||
norecursedirs = ["*"]
|
||||
kicad_symbols_tag = "9.0.6"
|
||||
kicad_footprints_tag = "9.0.6"
|
||||
kicad_source_tag = "9.0.6"
|
||||
# Tag to use for container for footprint svg export
|
||||
# For a list of available tags, see https://hub.docker.com/r/kicad/kicad/tags
|
||||
kicad_container_tag = "9.0.6-full"
|
||||
|
|
|
|||
|
|
@ -123,6 +123,12 @@ class Stroke:
|
|||
return attrs
|
||||
|
||||
|
||||
@sexp_type('fill')
|
||||
class Fill:
|
||||
type: Named(AtomChoice(Atom.none, Atom.outline, Atom.background, Atom.color)) = Atom.none
|
||||
color: Color = None
|
||||
|
||||
|
||||
class WidthMixin:
|
||||
def __post_init__(self):
|
||||
if self.width is not None:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from .symbols import Symbol
|
|||
from . import graphical_primitives as gr
|
||||
|
||||
from .. import primitives as cad_pr
|
||||
from ... import __version__
|
||||
|
||||
from ... import graphic_primitives as gp
|
||||
from ... import graphic_objects as go
|
||||
|
|
@ -160,6 +161,7 @@ class Bus:
|
|||
class Polyline:
|
||||
points: PointList = field(default_factory=list)
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
fill: OmitDefault(Fill) = None
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
def bounding_box(self, default=None):
|
||||
|
|
@ -169,34 +171,6 @@ class Polyline:
|
|||
yield _polyline_svg(self, colorscheme.lines)
|
||||
|
||||
|
||||
@sexp_type('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(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)
|
||||
|
||||
@property
|
||||
def _text_offset(self):
|
||||
return (0, -2*self.line_width)
|
||||
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
yield from TextMixin.to_svg(self, colorscheme.labels)
|
||||
|
||||
|
||||
def label_shape_path_d(shape, w, h):
|
||||
l, r = {
|
||||
Atom.input: '<]',
|
||||
|
|
@ -220,16 +194,36 @@ def label_shape_path_d(shape, w, h):
|
|||
return d + f'L {e+r:.3f} {0:.3f} L {e:.3f} {r:.3f} Z'
|
||||
|
||||
|
||||
@sexp_type('global_label')
|
||||
class GlobalLabel(TextMixin):
|
||||
@dataclass
|
||||
class TextLabel(TextMixin):
|
||||
text: str = ''
|
||||
shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive)) = Atom.input
|
||||
shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive, Atom.dot, Atom.round, Atom.diamond, Atom.rectangle)) = Atom.passive
|
||||
exclude_from_sim: Named(YesNoAtom()) = False
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
fields_autoplaced: Wrap(Flag()) = False
|
||||
fields_autoplaced: Named(YesNoAtom()) = False
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
properties: List(Property) = field(default_factory=list)
|
||||
properties: List(DrawnProperty) = field(default_factory=list)
|
||||
|
||||
|
||||
@sexp_type('text')
|
||||
class Text(TextLabel):
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
yield from TextMixin.to_svg(self, colorscheme.text)
|
||||
|
||||
|
||||
@sexp_type('label')
|
||||
class LocalLabel(TextLabel):
|
||||
@property
|
||||
def _text_offset(self):
|
||||
return (0, -2*self.line_width)
|
||||
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
yield from TextMixin.to_svg(self, colorscheme.labels)
|
||||
|
||||
|
||||
@sexp_type('global_label')
|
||||
class GlobalLabel(TextLabel):
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
text = super(TextMixin, self).to_svg(colorscheme.labels),
|
||||
text.attrs['transform'] = f'translate({self.size*0.6:.3f} 0)'
|
||||
|
|
@ -240,14 +234,7 @@ class GlobalLabel(TextMixin):
|
|||
|
||||
|
||||
@sexp_type('hierarchical_label')
|
||||
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)
|
||||
fields_autoplaced: Wrap(Flag()) = False
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
class HierarchicalLabel(TextLabel):
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
text, = TextMixin.to_svg(self, colorscheme.labels),
|
||||
text.attrs['transform'] = f'translate({self.size*1.2:.3f} 0)'
|
||||
|
|
@ -256,6 +243,15 @@ class HierarchicalLabel(TextMixin):
|
|||
yield Tag('g', children=[frame, text])
|
||||
|
||||
|
||||
@sexp_type('netclass_flag')
|
||||
class NetclassFlag(TextLabel):
|
||||
length: Named(float) = 2.54
|
||||
|
||||
def to_svg(self, colorscheme=Colorscheme.KiCad):
|
||||
# FIXME
|
||||
yield from TextMixin.to_svg(self, colorscheme.text)
|
||||
|
||||
|
||||
@sexp_type('pin')
|
||||
class Pin:
|
||||
name: str = '1'
|
||||
|
|
@ -269,6 +265,8 @@ class SymbolCrosslinkSheet:
|
|||
path: str = ''
|
||||
reference: Named(str) = ''
|
||||
unit: Named(int) = 1
|
||||
value: OmitDefault(Named(str)) = None
|
||||
footprint: OmitDefault(Named(str)) = None
|
||||
|
||||
|
||||
@sexp_type('project')
|
||||
|
|
@ -287,6 +285,7 @@ class MirrorFlags:
|
|||
class DrawnProperty(TextMixin):
|
||||
key: str = None
|
||||
value: str = None
|
||||
id: Named(int) = None
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
hide: Flag() = False
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
|
|
@ -336,6 +335,14 @@ class DrawnProperty(TextMixin):
|
|||
yield from TextMixin.to_svg(self, colorscheme.values)
|
||||
|
||||
|
||||
@sexp_type('default_instance')
|
||||
class DefaultSymbolInstance:
|
||||
reference: Named(str) = ''
|
||||
unit: Named(int) = 1
|
||||
value: Named(str) = ''
|
||||
footprint: Named(str) = ''
|
||||
|
||||
|
||||
@sexp_type('symbol')
|
||||
class SymbolInstance:
|
||||
name: str = None
|
||||
|
|
@ -348,8 +355,9 @@ class SymbolInstance:
|
|||
in_bom: Named(YesNoAtom()) = True
|
||||
on_board: Named(YesNoAtom()) = True
|
||||
dnp: Named(YesNoAtom()) = True
|
||||
fields_autoplaced: Wrap(Flag()) = True
|
||||
fields_autoplaced: Named(YesNoAtom()) = True
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
default_instance: DefaultSymbolInstance = None
|
||||
properties: List(DrawnProperty) = field(default_factory=list)
|
||||
# AFAICT this property is completely redundant.
|
||||
pins: List(Pin) = field(default_factory=list)
|
||||
|
|
@ -485,7 +493,11 @@ class SubsheetFill:
|
|||
class Subsheet:
|
||||
at: Rename(XYCoord) = field(default_factory=XYCoord)
|
||||
size: Rename(XYCoord) = field(default_factory=lambda: XYCoord(2.54, 2.54))
|
||||
fields_autoplaced: Wrap(Flag()) = True
|
||||
exclude_from_sim: Named(YesNoAtom()) = False
|
||||
in_bom: Named(YesNoAtom()) = False
|
||||
on_board: Named(YesNoAtom()) = False
|
||||
dnp: Named(YesNoAtom()) = False
|
||||
fields_autoplaced: Named(YesNoAtom()) = True
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
fill: SubsheetFill = field(default_factory=SubsheetFill)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
|
@ -499,7 +511,7 @@ class Subsheet:
|
|||
schematic: object = field(repr=False, default=None)
|
||||
|
||||
def __after_parse__(self, parent):
|
||||
self.sheet_name, self.file_name = self._properties
|
||||
self.sheet_name, self.file_name, *_extra_params = self._properties
|
||||
self.schematic = parent
|
||||
|
||||
def __before_sexp__(self):
|
||||
|
|
@ -544,6 +556,36 @@ class Subsheet:
|
|||
**self.stroke.svg_attrs(colorscheme.lines))
|
||||
|
||||
|
||||
@sexp_type('rule_area')
|
||||
class RuleArea:
|
||||
polyline: Polyline = None
|
||||
|
||||
|
||||
@sexp_type('text_box')
|
||||
class TextBox(TextMixin):
|
||||
text: str = None
|
||||
exclude_from_sim: Named(YesNoAtom()) = False
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
size: Rename(XYCoord) = None
|
||||
margins: Rename(gr.Margins) = None
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
fill: OmitDefault(Fill) = None
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
def render(self, variables={}, cache=None):
|
||||
yield from gr.TextBox.render(self, variables=variables)
|
||||
|
||||
|
||||
@sexp_type('title_block')
|
||||
class TitleBlock:
|
||||
title: Named(str) = ''
|
||||
date: Named(str) = ''
|
||||
rev: Named(str) = ''
|
||||
company: Named(str) = ''
|
||||
|
||||
|
||||
@sexp_type('lib_symbols')
|
||||
class LocalLibrary:
|
||||
symbols: List(Symbol) = field(default_factory=list)
|
||||
|
|
@ -553,26 +595,34 @@ SUPPORTED_FILE_FORMAT_VERSIONS = [20230620]
|
|||
@sexp_type('kicad_sch')
|
||||
class Schematic:
|
||||
_version: Named(int, name='version') = 20230620
|
||||
generator: Named(Atom) = Atom.gerbonara
|
||||
generator: Named(str) = 'gerbonara'
|
||||
generator_version: Named(str) = __version__
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
page_settings: PageSettings = field(default_factory=PageSettings)
|
||||
title_block: TitleBlock = None
|
||||
# The doc says this is expected, but eeschema barfs when it's there.
|
||||
# path: SheetPath = field(default_factory=SheetPath)
|
||||
lib_symbols: LocalLibrary = field(default_factory=list)
|
||||
junctions: List(Junction) = field(default_factory=list)
|
||||
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_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)
|
||||
texts: List(Text) = field(default_factory=list)
|
||||
text_boxes: List(TextBox) = field(default_factory=list)
|
||||
local_labels: List(LocalLabel) = field(default_factory=list)
|
||||
global_labels: List(GlobalLabel) = field(default_factory=list)
|
||||
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)
|
||||
symbol_instances: Named(Array(SymbolCrosslinkProject)) = field(default_factory=list)
|
||||
embedded_fonts: Named(YesNoAtom()) = False
|
||||
_ : SEXP_END = None
|
||||
original_filename: str = None
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,8 @@ class YesNoAtom:
|
|||
self.yes, self.no = yes, no
|
||||
|
||||
def __map__(self, value, parent=None):
|
||||
if not value: # compatibility with legacy flag style
|
||||
return False
|
||||
value, = value
|
||||
return value == self.yes
|
||||
|
||||
|
|
|
|||
|
|
@ -419,14 +419,13 @@ class Property(TextMixin):
|
|||
|
||||
@sexp_type('pin_numbers')
|
||||
class PinNumberSpec:
|
||||
hide: OmitDefault(Named(YesNoAtom())) = False
|
||||
hide: Flag() = False
|
||||
|
||||
|
||||
@sexp_type('pin_names')
|
||||
class PinNameSpec:
|
||||
offset: OmitDefault(Named(float)) = 0.508
|
||||
hide: OmitDefault(Named(YesNoAtom())) = False
|
||||
|
||||
hide: Flag() = False
|
||||
|
||||
@sexp_type('text_box')
|
||||
class TextBox:
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ def pytest_addoption(parser):
|
|||
parser.addini('kicad_footprints_tag', 'git tag or branch for KiCad footprint library repo used as testdata', default='main')
|
||||
parser.addini('kicad_symbols_tag', 'git tag or branch for KiCad symbol library repo used as testdata', default='main')
|
||||
parser.addini('kicad_container_tag', 'docker hub tag for the KiCad container to use for exporting footprint images', default='main')
|
||||
parser.addini('kicad_source_tag', 'git tag for the KiCad source repo whose demos directory is used as testdata', default='main')
|
||||
parser.addoption("--use-cached-data", action="store_true", help="Do not re-check git repo caches and podman image")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
|
|
@ -95,8 +97,13 @@ def pytest_configure(config):
|
|||
else:
|
||||
config.kicad_symbols_libdir = config.cache.mkdir('kicad-symbols') / 'repo'
|
||||
|
||||
if (lib_dir := os.environ.get('KICAD_SOURCE')):
|
||||
config.kicad_source_dir = Path(lib_dir).expanduser()
|
||||
else:
|
||||
config.kicad_source_dir = config.cache.mkdir('kicad-source') / 'repo'
|
||||
|
||||
is_pytest_controller = 'PYTEST_XDIST_WORKER' not in os.environ
|
||||
if is_pytest_controller:
|
||||
if is_pytest_controller and not config.getoption("--use-cached-data"):
|
||||
# Update cached library repos unless they are overridden from outside.
|
||||
if not os.environ.get('KICAD_FOOTPRINTS'):
|
||||
tag = config.getini('kicad_footprints_tag')
|
||||
|
|
@ -106,16 +113,20 @@ def pytest_configure(config):
|
|||
tag = config.getini('kicad_symbols_tag')
|
||||
_update_repo_cache(config.kicad_symbols_libdir, 'https://gitlab.com/kicad/libraries/kicad-symbols', tag)
|
||||
|
||||
if not os.environ.get('KICAD_SOURCE'):
|
||||
tag = config.getini('kicad_source_tag')
|
||||
_update_repo_cache(config.kicad_source_dir, 'https://gitlab.com/kicad/code/kicad', tag)
|
||||
|
||||
tag = config.getini("kicad_container_tag")
|
||||
config.kicad_container = os.environ.get('KICAD_CONTAINER', f'registry.hub.docker.com/kicad/kicad:{tag}')
|
||||
|
||||
if is_pytest_controller:
|
||||
if is_pytest_controller and not config.getoption("--use-cached-data"):
|
||||
print('Updating podman image')
|
||||
subprocess.run(['podman', 'pull', config.kicad_container], check=True)
|
||||
|
||||
config.image_support = ImageSupport(config.cache.mkdir('image_cache'), config.kicad_container)
|
||||
|
||||
if is_pytest_controller:
|
||||
if is_pytest_controller and not config.getoption("--use-cached-data"):
|
||||
print('Checking KiCad footprint library render cache')
|
||||
with multiprocessing.pool.ThreadPool() as pool: # use thread pool here since we're only monitoring podman processes
|
||||
lib_dirs = list(config.kicad_footprints_libdir.glob('*.pretty'))
|
||||
|
|
@ -130,3 +141,14 @@ def pytest_generate_tests(metafunc):
|
|||
if 'kicad_mod_file' in metafunc.fixturenames:
|
||||
mod_files = list(metafunc.config.kicad_footprints_libdir.glob('*.pretty/*.kicad_mod'))
|
||||
metafunc.parametrize('kicad_mod_file', mod_files, ids=list(map(str, mod_files)))
|
||||
|
||||
if 'kicad_sch_file' in metafunc.fixturenames:
|
||||
files = list(metafunc.config.kicad_source_dir.glob('demos/*.kicad_sch'))
|
||||
files += list(metafunc.config.kicad_source_dir.glob('qa/data/**/*.kicad_sch'))
|
||||
metafunc.parametrize('kicad_sch_file', files, ids=list(map(str, files)))
|
||||
|
||||
if 'kicad_pcb_file' in metafunc.fixturenames:
|
||||
files = list(metafunc.config.kicad_source_dir.glob('demos/*.kicad_pcb'))
|
||||
files += list(metafunc.config.kicad_source_dir.glob('qa/data/**/*.kicad_pcb'))
|
||||
metafunc.parametrize('kicad_pcb_file', files, ids=list(map(str, files)))
|
||||
|
||||
|
|
|
|||
30
tests/test_kicad_schematic.py
Normal file
30
tests/test_kicad_schematic.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import math
|
||||
from itertools import zip_longest
|
||||
import pytest
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
import bs4
|
||||
|
||||
from .utils import tmpfile, print_on_error
|
||||
from .image_support import run_cargo_cmd, svg_soup
|
||||
|
||||
from gerbonara import graphic_objects as go
|
||||
from gerbonara.utils import MM, arc_bounds, sum_bounds
|
||||
from gerbonara.layers import LayerStack
|
||||
from gerbonara.cad.kicad.sexp import build_sexp, Atom
|
||||
from gerbonara.cad.kicad.sexp_mapper import sexp
|
||||
from gerbonara.cad.kicad.tmtheme import *
|
||||
from gerbonara.cad.kicad.schematic import Schematic
|
||||
|
||||
|
||||
def test_load_kicad_schematic(kicad_sch_file):
|
||||
if kicad_sch_file.name in [
|
||||
# contains legacy syntax
|
||||
]:
|
||||
pytest.skip()
|
||||
sch = Schematic.open(kicad_sch_file)
|
||||
print('Loaded schematic with', len(sch.wires), 'wires and', len(sch.symbols), 'symbols.')
|
||||
for subsh in sch.subsheets:
|
||||
subsh = subsh.open()
|
||||
print('Loaded sub-sheet with', len(subsh.wires), 'wires and', len(subsh.symbols), 'symbols.')
|
||||
Loading…
Add table
Add a link
Reference in a new issue