fix: check_wire_collisions reports false positives for unannotated components

PinLocator.get_all_symbol_pins resolves symbols by reference designator,
so when multiple components share the same unannotated reference (e.g. "Q?"),
it always returned the first match's pin positions. Every duplicate then
got an identical bounding box, causing a single wire to be flagged against
all N instances instead of only the ones it actually crosses.

Fix: add _compute_pin_positions_direct() that computes absolute pin positions
directly from each symbol's own (at x y rotation) and (mirror ...) data plus
pin definitions fetched by lib_id — no reference-name lookup involved.
Also extend _parse_symbols to capture mirror_x/mirror_y flags.

Add regression test: two "R?" at different positions, wire crossing only
one → must produce 0 collisions against the far-away component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-15 10:55:11 +00:00
parent 9141e33b70
commit 53564cbc58
2 changed files with 96 additions and 5 deletions

View File

@@ -382,6 +382,39 @@ class TestIntegrationCheckWireCollisions:
d1_collisions = [c for c in result if c["component"]["reference"] == "D1"]
assert len(d1_collisions) >= 1
def test_unannotated_duplicates_not_over_reported(self):
"""
Regression: two components with the same unannotated reference ("R?") at
different positions should each produce independent bounding boxes.
A wire crossing only one of them must produce exactly 1 collision, not 2.
Before the fix, PinLocator.get_all_symbol_pins always resolved "R?" to
the first match, so both symbols got identical bboxes and the same wire
was counted against both.
"""
# R? at (100, 100): Device:R pins are at (100, 96.19) and (100, 103.81).
# Effective bbox (after expansion + margin) ≈ x=[99,101], y=[96.69,103.31].
# R? at (200, 100): identical type but far away → no intersection with wire.
r_at_100 = _make_resistor_sexp("R?", 100, 100)
r_at_200 = _make_resistor_sexp("R?", 200, 100)
# Horizontal wire crossing the body of the first R? only
wire = """
(wire (pts (xy 95 100) (xy 105 100))
(stroke (width 0) (type default))
(uuid "w-collision"))
"""
tmp = _make_temp_schematic(r_at_100 + r_at_200 + wire)
result = check_wire_collisions(tmp)
# The wire must not be reported against the far-away R? at (200, 100)
collisions_at_200 = [
c for c in result
if abs(c["component"]["position"]["x"] - 200) < 0.5
]
assert len(collisions_at_200) == 0, (
"Wire at x≈100 must not be flagged against the R? at x=200; "
"likely caused by reference-lookup always returning the first 'R?'"
)
@pytest.mark.integration
class TestIntegrationGetElementsInRegion: