Files
boxsim-fileformat-rev/boxsim/project.py
T

161 lines
5.0 KiB
Python
Raw Normal View History

"""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