diff --git a/src/OriginalCircuit.Altium/Connectivity/Internal/HierarchyWalker.cs b/src/OriginalCircuit.Altium/Connectivity/Internal/HierarchyWalker.cs index 2cbc837..09319b2 100644 --- a/src/OriginalCircuit.Altium/Connectivity/Internal/HierarchyWalker.cs +++ b/src/OriginalCircuit.Altium/Connectivity/Internal/HierarchyWalker.cs @@ -68,8 +68,21 @@ public HierarchyWalker(Dictionary docsByName, List Instances { get; } = new(); public List Boundaries { get; } = new(); - public void Walk(AltiumProject project) + private bool _dedupeDuplicateSheetRefs; + private readonly HashSet _instantiated = new(StringComparer.OrdinalIgnoreCase); + + /// The project whose hierarchy to walk. + /// + /// When set, a document referenced by several plain sheet symbols is instantiated only + /// once and the remaining references are treated as navigation aids. This matches the + /// flat/global net-identifier scopes, where block-diagram overview pages routinely + /// reference the working sheets a second time and Altium does not clone channels + /// (components exist once per document; identifiers merge by name). Repeat() channels + /// are unaffected. + /// + public void Walk(AltiumProject project, bool dedupeDuplicateSheetRefs = false) { + _dedupeDuplicateSheetRefs = dedupeDuplicateSheetRefs; foreach (var (rootName, rootDoc) in DetermineRoots(project)) { var ancestors = new HashSet(StringComparer.OrdinalIgnoreCase) { rootName }; @@ -118,8 +131,18 @@ private void WalkNode(SchDocument doc, string fileName, string? designator, continue; } + if (_dedupeDuplicateSheetRefs && !sym.Repeat.IsRepeated && !_instantiated.Add(childName)) + { + _diagnostics.Add(new AltiumDiagnostic(DiagnosticSeverity.Info, + $"Sheet '{childName}' is referenced again under '{path}'; flat scope keeps a single instance.")); + continue; + } + var repeat = sym.Repeat; - var repeatedChild = childInstanceCounts.GetValueOrDefault(childName) > 1; + // Under flat-scope dedupe a plain symbol's document is instantiated exactly once, + // so it must not count as a repeated (channel-private) sheet. + var repeatedChild = childInstanceCounts.GetValueOrDefault(childName) > 1 && + !(_dedupeDuplicateSheetRefs && !repeat.IsRepeated); var symbolName = string.IsNullOrEmpty(sym.SheetName) ? StripExt(childName) : sym.SheetName; var symUid = sym.UniqueId ?? symbolName; diff --git a/src/OriginalCircuit.Altium/Connectivity/ProjectNetlistBuilder.cs b/src/OriginalCircuit.Altium/Connectivity/ProjectNetlistBuilder.cs index c3c9967..96faba4 100644 --- a/src/OriginalCircuit.Altium/Connectivity/ProjectNetlistBuilder.cs +++ b/src/OriginalCircuit.Altium/Connectivity/ProjectNetlistBuilder.cs @@ -56,8 +56,12 @@ public static async Task BuildAsync( var scope = NetIdentifierScopeReader.Resolve(rawScope, hasSheetSymbols); // --- Walk the hierarchy into sheet instances --- + // In flat/global scope, duplicate plain sheet references (block-diagram overview + // pages) do not clone channels: identifiers merge by name and components exist once + // per document, so each document is instantiated once. var walker = new HierarchyWalker(docsByName, diagnostics); - walker.Walk(project); + walker.Walk(project, + dedupeDuplicateSheetRefs: scope is NetIdentifierScope.Flat or NetIdentifierScope.Global); var instances = walker.Instances; if (instances.Count == 0)