diff --git a/src/faebryk/core/reference.py b/src/faebryk/core/reference.py index 4a4713ef..c64816bc 100644 --- a/src/faebryk/core/reference.py +++ b/src/faebryk/core/reference.py @@ -55,9 +55,9 @@ def __construct__(self, obj: Node) -> None: return None -def reference[ - O: Node -](out_type: type[O] | None = None, optional: bool = False) -> O | Reference: +def reference[O: Node]( + out_type: type[O] | None = None, optional: bool = False +) -> O | Reference: """ Create a simple reference to other nodes properly encoded in the graph. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index e539dcef..fcab3d25 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -152,7 +152,9 @@ def draw_text( font.render_to(scr, (pt.x, pt.y), txt, color) -def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font", **options): +def draw_part( + part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font", **options +): """Draw part bounding box. Args: @@ -185,6 +187,7 @@ def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Fo # TODO: remove debug things import pygame + pygame.draw.circle(scr, (255, 0, 0), (100, 100), 10) pygame.draw.circle(scr, (0, 255, 0), (150, 100), 10) pygame.draw.circle(scr, (0, 0, 255), (100, 150), 10) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index 2e899d61..3e338fed 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -31,6 +31,7 @@ def to_mms(mils): class Tx: """Transformation matrix.""" + ROT_CCW_90: "Tx" ROT_CW_0: "Tx" @@ -204,26 +205,11 @@ def _check(tx: Tx) -> bool: # Some common rotations. # DANGER! These keywords are out of order! -Tx.ROT_CCW_90 = Tx( - a=0, b=1, - c=-1, d=0 -) -Tx.ROT_CW_0 = Tx( - a=1, b=0, - c=0, d=1 -) -Tx.ROT_CW_90 = Tx( - a=0, b=-1, - c=1, d=0 -) -Tx.ROT_CW_180 = Tx( - a=-1, b=0, - c=0, d=-1 -) -Tx.ROT_CW_270 = Tx( - a=0, b=1, - c=-1, d=0 -) +Tx.ROT_CCW_90 = Tx(a=0, b=1, c=-1, d=0) +Tx.ROT_CW_0 = Tx(a=1, b=0, c=0, d=1) +Tx.ROT_CW_90 = Tx(a=0, b=-1, c=1, d=0) +Tx.ROT_CW_180 = Tx(a=-1, b=0, c=0, d=-1) +Tx.ROT_CW_270 = Tx(a=0, b=1, c=-1, d=0) # Some common flips. Tx.FLIP_X = Tx(a=-1, b=0, c=0, d=1) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index e9012522..66806988 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -41,7 +41,9 @@ def __init__( title="", flatness=0.0, ): - logger.debug(f"Creating node {top_name=} with {circuit=} and {len(self.all_created_nodes)=}") + logger.debug( + f"Creating node {top_name=} with {circuit=} and {len(self.all_created_nodes)=}" + ) self.all_created_nodes.append(self) self.parent = None self.children = defaultdict( diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index a158bd72..eb325347 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -26,9 +26,6 @@ from faebryk.exporters.schematic.kicad.skidl import shims from faebryk.libs.exceptions import FaebrykException from faebryk.libs.geometry.basic import Geometry -from faebryk.libs.kicad.fileformats import ( - C_kicad_fp_lib_table_file, -) from faebryk.libs.kicad.fileformats import ( gen_uuid as _gen_uuid, ) @@ -45,7 +42,6 @@ C_rect, C_stroke, ) -from faebryk.libs.kicad.paths import GLOBAL_FP_DIR_PATH, GLOBAL_FP_LIB_PATH from faebryk.libs.sexp.dataclass_sexp import dataclass_dfs from faebryk.libs.util import ( FuncDict, @@ -185,41 +181,42 @@ def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): Transformer.has_linked_sch_pins([pin], sym_inst) ) + def index_symbol_libraries( + self, symbol_lib_paths: PathLike | list[PathLike] + ) -> None: + """Index the symbol libraries""" + if isinstance(symbol_lib_paths, (str, Path)): + symbol_lib_paths = [Path(symbol_lib_paths)] + else: + symbol_lib_paths = [Path(p) for p in symbol_lib_paths] + + for path in symbol_lib_paths: + self._symbol_files_index[path.stem] = path + def index_symbol_files( - self, fp_lib_tables: PathLike | list[PathLike], load_globals: bool = True + self, + symbol_lib_paths: PathLike | list[PathLike], + symbol_lib_search_paths: PathLike | list[PathLike], + load_globals: bool = False, ) -> None: """ Index the symbol files in the given library tables """ + if load_globals: + # TODO: this should work like GLOBAL_FP_LIB_PATH, + # then pass the sym-lib-table files + raise NotImplementedError("Loading global symbol libraries not implemented") + if self._symbol_files_index is None: self._symbol_files_index = {} - if isinstance(fp_lib_tables, (str, Path)): - fp_lib_table_paths = [Path(fp_lib_tables)] + if isinstance(symbol_lib_search_paths, (str, Path)): + symbol_lib_search_paths = [Path(symbol_lib_search_paths)] else: - fp_lib_table_paths = [Path(p) for p in fp_lib_tables] - - # non-local lib, search in kicad global lib - if load_globals: - fp_lib_table_paths += [GLOBAL_FP_LIB_PATH] - - for lib_path in fp_lib_table_paths: - for lib in C_kicad_fp_lib_table_file.loads(lib_path).fp_lib_table.libs: - resolved_lib_dir = lib.uri.replace("${KIPRJMOD}", str(lib_path.parent)) - resolved_lib_dir = resolved_lib_dir.replace( - "${KICAD8_FOOTPRINT_DIR}", str(GLOBAL_FP_DIR_PATH) - ) - - resolved_lib_dir = Path(resolved_lib_dir) - - # HACK: paths typically look like .../libs/footprints/xyz.pretty - # we actually want the .../libs/ part of it, so we'll just knock - # off the last two directories - resolved_lib_dir = resolved_lib_dir.parent.parent + symbol_lib_search_paths = [Path(p) for p in symbol_lib_search_paths] - for path in resolved_lib_dir.glob("*.kicad_sym"): - if path.stem not in self._symbol_files_index: - self._symbol_files_index[path.stem] = path + for path in chain(symbol_lib_search_paths, symbol_lib_paths): + self.index_symbol_libraries(path) @staticmethod def flipped[T](input_list: list[tuple[T, int]]) -> list[tuple[T, int]]: diff --git a/src/faebryk/library/has_pcb_routing_strategy_manual.py b/src/faebryk/library/has_pcb_routing_strategy_manual.py index 60516e8d..c6f7f7b3 100644 --- a/src/faebryk/library/has_pcb_routing_strategy_manual.py +++ b/src/faebryk/library/has_pcb_routing_strategy_manual.py @@ -2,11 +2,10 @@ # SPDX-License-Identifier: MIT import logging -from typing import Sequence +from typing import TYPE_CHECKING, Sequence import faebryk.library._F as F from faebryk.core.node import Node -from faebryk.exporters.pcb.kicad.transformer import PCB_Transformer from faebryk.exporters.pcb.routing.util import ( Path, Route, @@ -14,6 +13,9 @@ get_pads_pos_of_mifs, ) +if TYPE_CHECKING: + from faebryk.exporters.pcb.kicad.transformer import PCB_Transformer + logger = logging.getLogger(__name__) @@ -30,7 +32,7 @@ def __init__( self.relative_to = relative_to self.absolute = absolute - def calculate(self, transformer: PCB_Transformer): + def calculate(self, transformer: "PCB_Transformer"): node = self.obj nets = get_internal_nets_of_node(node) @@ -56,9 +58,11 @@ def get_route_for_mifs_in_net(mifs, path): for net_or_mifs, path in self.paths_rel if ( route := get_route_for_mifs_in_net( - nets[net_or_mifs] - if isinstance(net_or_mifs, F.Net) - else net_or_mifs, + ( + nets[net_or_mifs] + if isinstance(net_or_mifs, F.Net) + else net_or_mifs + ), path, ) ) diff --git a/src/faebryk/library/has_pcb_routing_strategy_via_to_layer.py b/src/faebryk/library/has_pcb_routing_strategy_via_to_layer.py index 559079f9..76a14c11 100644 --- a/src/faebryk/library/has_pcb_routing_strategy_via_to_layer.py +++ b/src/faebryk/library/has_pcb_routing_strategy_via_to_layer.py @@ -2,9 +2,9 @@ # SPDX-License-Identifier: MIT import logging +from typing import TYPE_CHECKING import faebryk.library._F as F -from faebryk.exporters.pcb.kicad.transformer import PCB_Transformer from faebryk.exporters.pcb.routing.util import ( DEFAULT_TRACE_WIDTH, DEFAULT_VIA_SIZE_DRILL, @@ -17,6 +17,9 @@ ) from faebryk.libs.geometry.basic import Geometry +if TYPE_CHECKING: + from faebryk.exporters.pcb.kicad.transformer import PCB_Transformer + logger = logging.getLogger(__name__) @@ -26,7 +29,7 @@ def __init__(self, layer: str, vec: Geometry.Point2D): self.vec = vec self.layer = layer - def calculate(self, transformer: PCB_Transformer): + def calculate(self, transformer: "PCB_Transformer"): layer = transformer.get_layer_id(self.layer) node = self.obj diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index 5b0d98fb..10a7c459 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -11,7 +11,6 @@ from enum import auto from typing import Optional -from faebryk.exporters.pcb.kicad.transformer import gen_uuid from faebryk.libs.kicad.fileformats_common import ( UUID, C_effects, @@ -27,6 +26,12 @@ def uuid_field(): + def gen_uuid(): + # TODO: wtf? This is one heck of a circular import defense + from faebryk.exporters.pcb.kicad.transformer import gen_uuid + + return gen_uuid() + return field(default_factory=gen_uuid) @@ -228,6 +233,7 @@ class E_mirror(SymEnum): Mirroring is applied about the X or Y axes The allowed mirrors are dependent on the rotation of the part """ + x = "x" y = "y" @@ -399,9 +405,6 @@ def skeleton(cls) -> "C_kicad_sch_file": version=20211123, generator="faebryk", paper="A4", - # uuid=gen_uuid(), - # lib_symbols=C_kicad_sch_file.C_kicad_sch.C_lib_symbols(symbols={}), - # title_block=C_kicad_sch_file.C_kicad_sch.C_title_block(), ) ) diff --git a/test/exporters/schematic/kicad/test_skidl_gen_schematic.py b/test/exporters/schematic/kicad/test_skidl_gen_schematic.py index ac8d936e..e69de29b 100644 --- a/test/exporters/schematic/kicad/test_skidl_gen_schematic.py +++ b/test/exporters/schematic/kicad/test_skidl_gen_schematic.py @@ -1,54 +0,0 @@ -import pytest - -from faebryk.exporters.schematic.kicad.skidl.shims import Part, Pin - - -@pytest.fixture -def part() -> Part: - - part = Part() - - part.pins = [Pin() for _ in range(4)] - part.pins[0].orientation = "U" - part.pins[1].orientation = "D" - part.pins[2].orientation = "L" - part.pins[3].orientation = "R" - - part.pins_by_orientation = { - "U": part.pins[0], - "D": part.pins[1], - "L": part.pins[2], - "R": part.pins[3], - } - - return part - - -@pytest.mark.parametrize( - "pwr_pins, gnd_pins, expected, certainty", - [ - (["U"], ["D"], 180, 1.0), - (["L"], ["R"], 90, 1.0), - (["R"], ["L"], 270, 1.0), - ([], [], 0, 0), - ], -) -def test_ideal_part_rotation(part, pwr_pins, gnd_pins, expected, certainty): - from faebryk.exporters.schematic.kicad.skidl.gen_schematic import ( - _ideal_part_rotation, - ) - - assert isinstance(part, Part) - for orientation, pin in part.pins_by_orientation.items(): - assert isinstance(pin, Pin) - if orientation in pwr_pins: - pin.fab_is_pwr = True - pin.fab_is_gnd = False - elif orientation in gnd_pins: - pin.fab_is_gnd = True - pin.fab_is_pwr = False - else: - pin.fab_is_pwr = False - pin.fab_is_gnd = False - - assert _ideal_part_rotation(part) == (expected, certainty) diff --git a/test/exporters/schematic/kicad/test_skidl_geometry.py b/test/exporters/schematic/kicad/test_skidl_geometry.py index 9983265d..12c3c54e 100644 --- a/test/exporters/schematic/kicad/test_skidl_geometry.py +++ b/test/exporters/schematic/kicad/test_skidl_geometry.py @@ -6,10 +6,10 @@ @pytest.mark.parametrize( "tx, expected", [ - (Tx.ROT_CCW_90, (90, False)), - (Tx.FLIP_X * Tx.ROT_CCW_90, (90, True)), - (Tx.ROT_CW_90, (270, False)), - (Tx.FLIP_X * Tx.ROT_CW_90, (270, True)), + (Tx.ROT_CCW_90, (False, 90)), + (Tx.FLIP_X * Tx.ROT_CCW_90, (True, 90)), + (Tx.ROT_CW_90, (False, 270)), + (Tx.FLIP_X * Tx.ROT_CW_90, (True, 270)), ], ) def test_find_orientation(tx: Tx, expected: tuple[float, bool]): diff --git a/test/exporters/schematic/kicad/test_transformer.py b/test/exporters/schematic/kicad/test_transformer.py index c8f8a597..a13bcc35 100644 --- a/test/exporters/schematic/kicad/test_transformer.py +++ b/test/exporters/schematic/kicad/test_transformer.py @@ -4,6 +4,7 @@ import faebryk.library._F as F from faebryk.core.module import Module +from faebryk.exporters.schematic.kicad.skidl.shims import Part, Pin from faebryk.exporters.schematic.kicad.transformer import Transformer from faebryk.libs.exceptions import FaebrykException from faebryk.libs.kicad.fileformats_common import C_effects, C_stroke, C_wh, C_xy, C_xyr @@ -133,11 +134,13 @@ def test_get_bbox_arc(): def test_get_bbox_polyline(): polyline = C_polyline( - pts=C_pts(xys=[ - C_xy(0, 0), - C_xy(1, 1), - C_xy(2, 0), - ]), + pts=C_pts( + xys=[ + C_xy(0, 0), + C_xy(1, 1), + C_xy(2, 0), + ] + ), stroke=C_stroke(width=0, type=C_stroke.E_type.default), fill=C_fill(type=C_fill.E_type.background), ) @@ -196,20 +199,20 @@ def test_get_bbox_pin(): name="", effects=C_effects( font=C_effects.C_font(size=C_wh(w=1.27, h=1.27), thickness=0.127), - hide=False - ) + hide=False, + ), ), number=C_lib_symbol.C_symbol.C_pin.C_number( number="", effects=C_effects( font=C_effects.C_font(size=C_wh(w=1.27, h=1.27), thickness=0.127), - hide=False - ) - ) + hide=False, + ), + ), ) bbox = Transformer.get_bbox(pin) assert len(bbox) == 2 - assert bbox[0] == (1, 1) + assert bbox[0] == (-1.54, 1) assert bbox[1] == (3, 1) # Pin extends by length in x direction @@ -233,26 +236,30 @@ def test_get_bbox_symbol(): name=C_lib_symbol.C_symbol.C_pin.C_name( name="", effects=C_effects( - font=C_effects.C_font(size=C_wh(w=1.27, h=1.27), thickness=0.127), - hide=False - ) + font=C_effects.C_font( + size=C_wh(w=1.27, h=1.27), thickness=0.127 + ), + hide=False, + ), ), number=C_lib_symbol.C_symbol.C_pin.C_number( number="", effects=C_effects( - font=C_effects.C_font(size=C_wh(w=1.27, h=1.27), thickness=0.127), - hide=False - ) - ) + font=C_effects.C_font( + size=C_wh(w=1.27, h=1.27), thickness=0.127 + ), + hide=False, + ), + ), ) ], polylines=[], circles=[], - arcs=[] + arcs=[], ) bbox = Transformer.get_bbox(symbol) assert len(bbox) == 2 - assert bbox[0] == (0, 0) + assert bbox[0] == (-2.54, 0) assert bbox[1] == (2, 2) @@ -273,7 +280,7 @@ def test_get_bbox_lib_symbol(): polylines=[], circles=[], arcs=[], - pins=[] + pins=[], ), "unit2": C_lib_symbol.C_symbol( name="unit2", @@ -288,7 +295,7 @@ def test_get_bbox_lib_symbol(): polylines=[], circles=[], arcs=[], - pins=[] + pins=[], ), }, power=None, @@ -297,7 +304,7 @@ def test_get_bbox_lib_symbol(): in_bom=None, on_board=None, convert=None, - propertys={} + propertys={}, ) bbox = Transformer.get_bbox(lib_symbol) assert len(bbox) == 2 @@ -317,12 +324,53 @@ def test_get_bbox_empty_polyline(): def test_get_bbox_empty_symbol(): symbol = C_lib_symbol.C_symbol( - name="empty_symbol", - rectangles=[], - pins=[], - polylines=[], - circles=[], - arcs=[] + name="empty_symbol", rectangles=[], pins=[], polylines=[], circles=[], arcs=[] ) bbox = Transformer.get_bbox(symbol) assert bbox is None + + +@pytest.fixture +def part() -> Part: + part = Part() + + part.pins = [Pin() for _ in range(4)] + part.pins[0].orientation = "U" + part.pins[1].orientation = "D" + part.pins[2].orientation = "L" + part.pins[3].orientation = "R" + + part.pins_by_orientation = { + "U": part.pins[0], + "D": part.pins[1], + "L": part.pins[2], + "R": part.pins[3], + } + + return part + + +@pytest.mark.parametrize( + "pwr_pins, gnd_pins, expected, certainty", + [ + (["U"], ["D"], 180, 1.0), + (["L"], ["R"], 90, 1.0), + (["R"], ["L"], 270, 1.0), + ([], [], 0, 0), + ], +) +def test_ideal_part_rotation(part, pwr_pins, gnd_pins, expected, certainty): + assert isinstance(part, Part) + for orientation, pin in part.pins_by_orientation.items(): + assert isinstance(pin, Pin) + if orientation in pwr_pins: + pin.fab_is_pwr = True + pin.fab_is_gnd = False + elif orientation in gnd_pins: + pin.fab_is_gnd = True + pin.fab_is_pwr = False + else: + pin.fab_is_pwr = False + pin.fab_is_gnd = False + + assert Transformer._ideal_part_rotation(part) == (expected, certainty) diff --git a/test/library/test_schematic_hints.py b/test/library/test_schematic_hints.py index c57bf717..c4b838cf 100644 --- a/test/library/test_schematic_hints.py +++ b/test/library/test_schematic_hints.py @@ -13,7 +13,7 @@ def test_default_hint_values(): """Default hint values should match their defined defaults""" hints = has_schematic_hints() assert hints.lock_rotation_certainty == 0.6 - assert hints.symbol_rotation == 0 + assert hints.symbol_rotation is None def test_custom_hint_values(custom_hints): @@ -98,7 +98,7 @@ def extra_hint(self) -> int: # Test inherited hints assert hints.lock_rotation_certainty == 0.9 - assert hints.symbol_rotation == 0 + assert hints.symbol_rotation is None # Test new hint assert hints.extra_hint == 42 diff --git a/test/libs/kicad/test_fileformats.py b/test/libs/kicad/test_fileformats.py index f218b64c..5602223b 100644 --- a/test/libs/kicad/test_fileformats.py +++ b/test/libs/kicad/test_fileformats.py @@ -29,14 +29,15 @@ Path(__file__).parents, lambda p: p.name == "test" and (p / "common/resources").is_dir(), ) -TEST_FILES = TEST_DIR / "common/resources" +LIB_DIR = TEST_DIR / "common" / "libs" +TEST_FILES = TEST_DIR / "common" / "resources" PRJFILE = TEST_FILES / "test.kicad_pro" PCBFILE = TEST_FILES / "test.kicad_pcb" FPFILE = TEST_FILES / "test.kicad_mod" NETFILE = TEST_FILES / "test_e.net" FPLIBFILE = TEST_FILES / "fp-lib-table" SCHFILE = TEST_FILES / "test.kicad_sch" -SYMFILE = TEST_FILES / "test.kicad_sym" +SYMFILE = LIB_DIR / "test.kicad_sym" DUMP = ConfigFlag("DUMP", descr="dump load->save into /tmp") @@ -190,11 +191,23 @@ def _effects(pcb: C_kicad_pcb_file): (C_kicad_sym_file, SYMFILE), ], ) -def test_dump_load_equality(parser: type[SEXP_File | JSON_File], path: Path): +def test_dump_load_equality( + parser: type[SEXP_File | JSON_File], + path: Path, + tmp_path: Path, +): + # Load original file loaded = parser.loads(path) - dump = loaded.dumps(Path("/tmp") / path.name if DUMP else None) + + # Dump to temp file if DUMP flag set + dump_path = tmp_path / path.name if DUMP else None + dump = loaded.dumps(dump_path) + + # Load dumped content and dump again loaded_dump = parser.loads(dump) dump2 = loaded_dump.dumps() + + # Verify dumps match assert dump == dump2