diff --git a/python/commands/pin_locator.py b/python/commands/pin_locator.py index 4786406..da6f7d2 100644 --- a/python/commands/pin_locator.py +++ b/python/commands/pin_locator.py @@ -384,9 +384,6 @@ class PinLocator: logger.debug(f"Pin {pin_number} relative position: ({pin_rel_x}, {pin_rel_y})") - # lib_symbols uses y-up; schematic uses y-down - pin_rel_y = -pin_rel_y - # Mirror in local coords after y-negate (KiCad transform order) # mirror_x = flip across X axis → negate y # mirror_y = flip across Y axis → negate x diff --git a/tests/test_pin_locator_y_flip.py b/tests/test_pin_locator_y_flip.py new file mode 100644 index 0000000..ec83f0d --- /dev/null +++ b/tests/test_pin_locator_y_flip.py @@ -0,0 +1,112 @@ +""" +Regression test for the symbol-to-schematic Y-axis flip in PinLocator. + +Before the fix, pin_locator.py's get_pin_location() negated pin_data["y"] +twice in sequence (two identical blocks, one commented "Negate y here before +rotation" and a second commented "lib_symbols uses y-up; schematic uses y-down" +doing the exact same flip). The double-negation cancelled out, leaving pin +Y-coordinates mirrored about the symbol placement Y. For symmetric passives +(pin 1 and pin 2 electrically equivalent) the bug was invisible; for ICs with +non-equivalent pins it caused misrouted connections. + +This test places a stock Device:R at a known absolute position and verifies +that pin 1 (symbol y=+3.81) resolves to an absolute Y *above* the placement +centre (i.e. placement_y - 3.81), matching KiCad's actual render and its +kicad-cli net extraction. The pre-fix code put pin 1 below the centre. +""" + +import shutil +import sys +import tempfile +from pathlib import Path + +import pytest + +PYTHON_DIR = Path(__file__).parent.parent / "python" +sys.path.insert(0, str(PYTHON_DIR)) + +from commands.component_schematic import ComponentManager # noqa: E402 +from commands.pin_locator import PinLocator # noqa: E402 +from commands.schematic import SchematicManager # noqa: E402 + + +@pytest.mark.unit +def test_stock_resistor_pin_y_matches_render_convention(): + """Stock Device:R pin 1 must resolve to placement_y - 3.81 (above centre).""" + template = ( + Path(__file__).resolve().parent.parent + / "python" + / "templates" + / "template_with_symbols.kicad_sch" + ) + if not template.exists(): + pytest.skip(f"Test template not found at {template}") + + with tempfile.TemporaryDirectory() as tmp: + sch_path = Path(tmp) / "regression.kicad_sch" + shutil.copy(template, sch_path) + + sch = SchematicManager.load_schematic(str(sch_path)) + ComponentManager.add_component( + sch, + {"type": "R", "reference": "R1", "value": "10k", "x": 100.0, "y": 100.0, "rotation": 0}, + sch_path, + ) + SchematicManager.save_schematic(sch, str(sch_path)) + + locator = PinLocator() + p1 = locator.get_pin_location(sch_path, "R1", "1") + p2 = locator.get_pin_location(sch_path, "R1", "2") + + assert p1 is not None and p2 is not None, "PinLocator returned None" + + # Device:R defines pin 1 at symbol (0, +3.81) and pin 2 at (0, -3.81). + # KiCad symbol space is +Y up; schematic space is +Y down. After the + # correct single negation, pin 1 lands at placement_y - 3.81 and pin 2 + # at placement_y + 3.81. + assert p1[0] == pytest.approx(100.0), f"pin 1 X wrong: {p1[0]}" + assert p1[1] == pytest.approx(96.19), f"pin 1 Y wrong: {p1[1]} (expected 96.19)" + assert p2[0] == pytest.approx(100.0), f"pin 2 X wrong: {p2[0]}" + assert p2[1] == pytest.approx(103.81), f"pin 2 Y wrong: {p2[1]} (expected 103.81)" + + +@pytest.mark.unit +def test_rotated_capacitor_pin_x_matches_render_convention(): + """Device:C rotated 90 CCW: pin 1 (was at +Y) should land on -X of placement.""" + template = ( + Path(__file__).resolve().parent.parent + / "python" + / "templates" + / "template_with_symbols.kicad_sch" + ) + if not template.exists(): + pytest.skip(f"Test template not found at {template}") + + with tempfile.TemporaryDirectory() as tmp: + sch_path = Path(tmp) / "rot_regression.kicad_sch" + shutil.copy(template, sch_path) + + sch = SchematicManager.load_schematic(str(sch_path)) + ComponentManager.add_component( + sch, + { + "type": "C", + "reference": "C1", + "value": "100nF", + "x": 150.0, + "y": 100.0, + "rotation": 90, + }, + sch_path, + ) + SchematicManager.save_schematic(sch, str(sch_path)) + + locator = PinLocator() + p1 = locator.get_pin_location(sch_path, "C1", "1") + assert p1 is not None + + # Device:C pin 1 is at symbol (0, +3.81). After y-negate → (0, -3.81). + # Rotated 90° CCW in screen coords: (0, -3.81) → (3.81, 0). + # Absolute: (150+3.81, 100+0) = (153.81, 100). + assert p1[0] == pytest.approx(153.81), f"rotated pin 1 X wrong: {p1[0]}" + assert p1[1] == pytest.approx(100.0), f"rotated pin 1 Y wrong: {p1[1]}"