Merge pull request #133 from johndev7000/fix/pin-locator-y-double-negation
fix: remove duplicate Y-axis negation in PinLocator.get_pin_location
This commit is contained in:
@@ -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
|
||||
|
||||
112
tests/test_pin_locator_y_flip.py
Normal file
112
tests/test_pin_locator_y_flip.py
Normal file
@@ -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]}"
|
||||
Reference in New Issue
Block a user