"""Write a complete KiCad project directory. Emits the schematic, a `.kicad_pro`, and project-local library tables pointing at the Visaton library so the project opens with every symbol and footprint resolved. No board is generated: KiCad's own *Update PCB from Schematic* does that, and every Visaton symbol already carries a `Footprint` property. """ from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path from . import symbols as symbols_mod from .catalog import Catalog from .model import Project from .schematic import DEFAULT_PITCH_MM, SchematicBuilder, Sheet from .symbols import Resolution, Resolver, SymbolLibrary LIB_NICKNAME = "Visaton" @dataclass class BuildResult: directory: Path schematic: Path project_file: Path sheet: Sheet resolutions: list[Resolution] @property def notes(self) -> list[str]: out = [] for r in self.resolutions: for note in r.notes: out.append(f"Part{r.part.slot}: {note}") return out def _pro_contents(name: str) -> dict: """Minimal but complete project file. KiCad fills in the rest on open.""" return { "board": {"design_settings": {}, "layer_presets": [], "viewports": []}, "boards": [], "cvpcb": {"equivalence_files": []}, "libraries": {"pinned_footprint_libs": [], "pinned_symbol_libs": []}, "meta": {"filename": f"{name}.kicad_pro", "version": 3}, "net_settings": { "classes": [ { "bus_width": 12, "clearance": 0.2, "diff_pair_gap": 0.25, "diff_pair_width": 0.2, "line_style": 0, "microvia_diameter": 0.3, "microvia_drill": 0.1, "name": "Default", "pcb_color": "rgba(0, 0, 0, 0.000)", "schematic_color": "rgba(0, 0, 0, 0.000)", "track_width": 0.8, "via_diameter": 1.2, "via_drill": 0.6, "wire_width": 6, } ], "meta": {"version": 4}, }, "pcbnew": {"page_layout_descr_file": ""}, "schematic": { "legacy_lib_dir": "", "legacy_lib_list": [], "meta": {"version": 1}, "page_layout_descr_file": "", }, "sheets": [], "text_variables": {}, } def _lib_table(kind: str, entries: list[tuple[str, str]]) -> str: """Render a sym-lib-table or fp-lib-table.""" lines = [f"({kind}", "\t(version 7)"] for nickname, uri in entries: lines.append( f'\t(lib (name "{nickname}")(type "KiCad")(uri "{uri}")' f'(options "")(descr "Visaton crossover parts"))' ) lines.append(")") return "\n".join(lines) + "\n" def resolve_library_root(library: SymbolLibrary) -> Path: """The checkout root, derived from the symbol file location.""" path = library.path.resolve() if path.parent.name == "symbols": return path.parent.parent return path.parent def build( project: Project, raw_name: str, outdir: Path, catalog: Catalog, library: SymbolLibrary, *, pitch: float = DEFAULT_PITCH_MM, crossover_index: int = 0, ) -> BuildResult: if not project.crossovers: raise ValueError("project contains no crossover") if project.amp_count > 1: raise NotImplementedError( f"project has {project.amp_count} amplifier channels; " "this version handles single-channel projects only" ) crossover = project.crossovers[crossover_index] resolver = Resolver(catalog, library) resolutions = resolver.resolve_all(crossover.components) builder = SchematicBuilder(library, pitch=pitch) sheet = builder.build(project, crossover, resolutions, title=raw_name) text = builder.render(sheet, raw_name) outdir.mkdir(parents=True, exist_ok=True) sch_path = outdir / f"{raw_name}.kicad_sch" sch_path.write_text(text, encoding="utf-8") pro_path = outdir / f"{raw_name}.kicad_pro" pro_path.write_text(json.dumps(_pro_contents(raw_name), indent=2) + "\n", encoding="utf-8") root = resolve_library_root(library) (outdir / "sym-lib-table").write_text( _lib_table("sym_lib_table", [(LIB_NICKNAME, str(root / "symbols" / "Visaton.kicad_sym"))]), encoding="utf-8", ) (outdir / "fp-lib-table").write_text( _lib_table("fp_lib_table", [(LIB_NICKNAME, str(root / "footprints" / "Visaton.pretty"))]), encoding="utf-8", ) return BuildResult( directory=outdir, schematic=sch_path, project_file=pro_path, sheet=sheet, resolutions=resolutions, ) def load_parts( catalog_path: str | None = None, library_path: str | None = None ) -> tuple[Catalog, SymbolLibrary]: from . import catalog as catalog_mod cat = catalog_mod.load(catalog_path) lib = symbols_mod.load(library_path) return cat, lib