From f317ec12ab41424c5cdb710e56fc9f4a14fcf14e Mon Sep 17 00:00:00 2001 From: Andrei Errapart Date: Wed, 19 Aug 2026 17:17:39 +0200 Subject: [PATCH] Connectivity: Prevent duplicate logical pin assignments Ignore pins from inactive component display modes so alternate header graphics cannot capture adjacent nets. Unify graphical endpoints that share a physical package pad and emit one logical pin per sheet instance. --- .../Connectivity/Internal/NetlistAssembler.cs | 7 +- .../Connectivity/Internal/SheetGraph.cs | 39 ++++++++- .../Connectivity/SchematicNetlistTests.cs | 81 +++++++++++++++++++ 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/src/OriginalCircuit.Altium/Connectivity/Internal/NetlistAssembler.cs b/src/OriginalCircuit.Altium/Connectivity/Internal/NetlistAssembler.cs index 0b44ba0..c69cd83 100644 --- a/src/OriginalCircuit.Altium/Connectivity/Internal/NetlistAssembler.cs +++ b/src/OriginalCircuit.Altium/Connectivity/Internal/NetlistAssembler.cs @@ -59,9 +59,14 @@ public AssembleResult Assemble() foreach (var (root, group) in groups) { var pins = new List(); + var pinKeys = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var e in group) if (e.Kind == ElementKind.Pin && e.NetPin is not null) - pins.Add(e.NetPin); + { + var logicalKey = $"{e.NetPin.SheetInstanceId}\0{e.NetPin.Key}"; + if (pinKeys.Add(logicalKey)) + pins.Add(e.NetPin); + } var hasLabel = _labelsByRoot.TryGetValue(root, out var labels) && labels.Count > 0; var (name, scope, explicitName) = ChooseName(root, group, pins, hasLabel ? labels! : null); diff --git a/src/OriginalCircuit.Altium/Connectivity/Internal/SheetGraph.cs b/src/OriginalCircuit.Altium/Connectivity/Internal/SheetGraph.cs index 7700e97..02872c5 100644 --- a/src/OriginalCircuit.Altium/Connectivity/Internal/SheetGraph.cs +++ b/src/OriginalCircuit.Altium/Connectivity/Internal/SheetGraph.cs @@ -87,12 +87,15 @@ private void Extract(List global) if (ip is not SchPin pin) continue; - // A multi-part component record carries pins from every part, but only the displayed - // part (CurrentPartId) is actually placed on this sheet — the other parts' pins keep - // stale positions. Include only the current part's pins (plus part-shared pins). + // A component record carries pins from every part and every alternative display mode, + // but only the selected part/mode is actually placed on this sheet. The other pins + // retain coordinates for their inactive representation and can otherwise connect to + // adjacent wires (notably on dual-row headers). if (sc.PartCount > 1 && sc.CurrentPartId > 0 && pin.OwnerPartId > 0 && pin.OwnerPartId != sc.CurrentPartId) continue; + if (pin.OwnerPartDisplayMode != sc.DisplayMode) + continue; var tip = SchDesignators.PinTip(pin); var e = NewElement(ElementKind.Pin, pin, global); @@ -261,6 +264,11 @@ public void ApplyRules(UnionFind uf) // Rule 1 & 3a: coincident connection points connect (shared endpoints, pin-tip on wire vertex). Points.UnionCoincident(uf); + // A symbol may expose the same physical package pad at multiple graphical locations. Altium + // identifies that pad by component plus pin designator, so all of those endpoints are one + // electrical node even when the drawing locations differ. + UnifyDuplicatePhysicalPins(uf); + // Rule 2 & 3b: a connection point on the INTERIOR of a conductor connects (T-junction). // Applies to any element's points landing on a different conductor's interior. foreach (var e in Elements) @@ -292,6 +300,31 @@ public void ApplyRules(UnionFind uf) ApplyCollinearOverlap(uf); } + private void UnifyDuplicatePhysicalPins(UnionFind uf) + { + var pinsByComponent = new Dictionary>( + ReferenceEqualityComparer.Instance); + + foreach (var element in Elements) + { + if (element.Kind != ElementKind.Pin + || element.NetPin is null + || string.IsNullOrEmpty(element.PinDesignator)) + continue; + + if (!pinsByComponent.TryGetValue(element.NetPin.Component, out var pinsByDesignator)) + { + pinsByDesignator = new Dictionary(StringComparer.OrdinalIgnoreCase); + pinsByComponent.Add(element.NetPin.Component, pinsByDesignator); + } + + if (pinsByDesignator.TryGetValue(element.PinDesignator, out var firstElementId)) + uf.Union(firstElementId, element.Id); + else + pinsByDesignator.Add(element.PinDesignator, element.Id); + } + } + private void ApplyCollinearOverlap(UnionFind uf) { // Conductors only; pairwise within shared coarse buckets would be ideal, but conductor counts diff --git a/tests/OriginalCircuit.Altium.Tests/Connectivity/SchematicNetlistTests.cs b/tests/OriginalCircuit.Altium.Tests/Connectivity/SchematicNetlistTests.cs index ad03b0e..f6587d7 100644 --- a/tests/OriginalCircuit.Altium.Tests/Connectivity/SchematicNetlistTests.cs +++ b/tests/OriginalCircuit.Altium.Tests/Connectivity/SchematicNetlistTests.cs @@ -144,6 +144,87 @@ public void SameName_NetLabels_Unify_Across_Disjoint_Wires() Assert.True(net.IsNamedExplicitly); } + [Theory] + [InlineData(0, "MODE0")] + [InlineData(1, "MODE1")] + public void Only_Selected_Display_Mode_Pins_Participate_In_Connectivity( + int displayMode, string expectedNet) + { + var component = new SchComponent + { + Name = "U1", + PartCount = 1, + CurrentPartId = 1, + DisplayModeCount = 2, + DisplayMode = displayMode, + }; + component.AddParameter(new SchParameter { Name = "Designator", Value = "U1" }); + + var mode0Pin = SchPin.Create("1") + .At(Coord.FromMils(0), Coord.FromMils(0)) + .Length(Coord.Zero) + .Orient(PinOrientation.Right) + .Build(); + mode0Pin.OwnerPartDisplayMode = 0; + component.AddPin(mode0Pin); + + var mode1Pin = SchPin.Create("1") + .At(Coord.FromMils(0), Coord.FromMils(200)) + .Length(Coord.Zero) + .Orient(PinOrientation.Right) + .Build(); + mode1Pin.OwnerPartDisplayMode = 1; + component.AddPin(mode1Pin); + + var netlist = Solve( + component, + Wire((0, 0), (100, 0)), + Wire((0, 200), (100, 200)), + Label("MODE0", 50, 0), + Label("MODE1", 50, 200)); + + Assert.Equal(expectedNet, netlist.NetForPin("U1", "1")?.Name); + Assert.Single( + netlist.Nets.SelectMany(net => net.Pins), + pin => pin.Key == "U1.1"); + } + + [Fact] + public void Duplicate_Pin_Designators_On_Component_Unify_By_Physical_Pad() + { + var component = new SchComponent + { + Name = "U1", + PartCount = 1, + CurrentPartId = 1, + }; + component.AddParameter(new SchParameter { Name = "Designator", Value = "U1" }); + + component.AddPin(SchPin.Create("6") + .At(Coord.FromMils(100), Coord.FromMils(0)) + .Length(Coord.Zero) + .Orient(PinOrientation.Right) + .Build()); + component.AddPin(SchPin.Create("6") + .At(Coord.FromMils(100), Coord.FromMils(200)) + .Length(Coord.Zero) + .Orient(PinOrientation.Right) + .Build()); + + var netlist = Solve( + component, + Wire((0, 0), (100, 0)), + Wire((0, 200), (100, 200)), + Power("GND", 0, 0)); + + var padNets = netlist.Nets + .Where(net => net.Pins.Any(pin => pin.Key == "U1.6")) + .ToList(); + var padNet = Assert.Single(padNets); + Assert.Equal("GND", padNet.Name); + Assert.Single(padNet.Pins, pin => pin.Key == "U1.6"); + } + [Fact] public void Power_Objects_Unify_Globally_By_Name() {