expandPortThroughRecursive method
Recursively expand a port through trivial gates and across already-opened module boundaries.
First runs expandPortThrough on nodeId/portId, then:
- Downward: for each revealed non-trivial child, finds the boundary port(s) and recurses into that child, partially expanding the connected path even when the child was previously collapsed.
- Upward: if the traversal reached a port on
nodeIditself (i.e. the signal exits through the module boundary) and the parent node is visible, reveals connected siblings at the parent level and recurses into siblings that are already open. If the signal reaches the parent's own boundary, climbs further up using an iterative loop — never re-entering an intermediate module, which would redundantly expand unrelated internal wires.
Traversal continues only while each step has a unique exit. It stops at fan-out rather than expanding multiple downstream branches.
A visited set of (nodeId, portId) pairs prevents infinite loops
when signals form combinational cycles or the same boundary port
is reachable from multiple directions.
Returns true if any new children/edges were revealed at any
level of the hierarchy.
Implementation
bool expandPortThroughRecursive(
String nodeId,
String portId, {
Set<(String, String)>? visited,
}) {
visited ??= {};
final key = (nodeId, portId);
if (visited.contains(key)) {
return false;
}
visited.add(key);
// Expand at this level (may be a no-op for fully expanded nodes).
final changed = expandPortThrough(nodeId, portId);
final node = nodeMap[nodeId];
if (node == null) {
return changed;
}
final hyperedges = node.hyperedges;
if (hyperedges == null || hyperedges.isEmpty) {
final parent = node.parent;
if (parent == null ||
(!parent.isExpanded && !parent.isPartiallyExpanded)) {
return changed;
}
final parentChanged = expandPortThroughRecursive(
parent.id,
portId,
visited: visited,
);
return changed || parentChanged;
}
// Convert string portId to (ownerNodeId, portIndex).
// The port might belong to this node or one of its children.
final resolved = node.resolvePortId(portId);
if (resolved == null) {
return changed;
}
final startNodeId = resolved.$1;
final startPortIdx = resolved.$2;
// BFS from `portId` through this node's hyperedges (mirrors the
// traversal in expandPortThrough) to discover:
// childPorts: childId → {portId strings on that child}
// parentPorts: portId strings on `node` itself (boundary exits)
// This works for partially expanded AND fully expanded nodes.
final allChildren = <String, LayoutNode>{};
for (final child in node.children) {
allChildren[child.id] = child;
}
if (node.hiddenChildren != null) {
for (final child in node.hiddenChildren!) {
allChildren[child.id] = child;
}
}
final startOwner = startNodeId == node.id ? node : allChildren[startNodeId];
if (startOwner == null ||
startPortIdx < 0 ||
startPortIdx >= startOwner.elkPorts.length) {
return changed;
}
final traceUpstream =
startOwner.elkPorts[startPortIdx].direction == PortDirection.output;
// BFS queue: (nodeId, portIndex) tuples.
final portQueue = Queue<(String, int)>()..add((startNodeId, startPortIdx));
final visitedPorts = <(String, int)>{};
final childPorts = <String, Set<String>>{};
final parentPorts = <String>{};
final idx = node.portHyperedgeIndex;
while (portQueue.isNotEmpty) {
final current = portQueue.removeFirst();
if (!visitedPorts.add(current)) {
continue;
}
final currentNodeId = current.$1;
final currentPortIdx = current.$2;
List<LayoutHyperedge>? matching;
if (currentNodeId == node.id) {
matching = idx[currentPortIdx];
} else {
matching = _findHyperedgesForPort(node, currentNodeId, currentPortIdx);
}
if (matching == null || matching.isEmpty) {
continue;
}
for (final h in matching) {
for (final (nId, pIdx) in h.sources) {
if (nId == node.id) {
parentPorts.add(node.portIdAt(pIdx));
}
}
for (final (nId, pIdx) in h.targets) {
if (nId == node.id) {
parentPorts.add(node.portIdAt(pIdx));
}
}
final directionalEndpoints = traceUpstream ? h.sources : h.targets;
final childEndpoints = directionalEndpoints
.where((endpoint) => endpoint.$1 != node.id)
.toList();
if (childEndpoints.length != 1) {
continue;
}
final (nId, pIdx) = childEndpoints.single;
final child = allChildren[nId];
final portIdStr = child?.portIdAt(pIdx) ?? pIdx.toString();
childPorts.putIfAbsent(nId, () => <String>{}).add(portIdStr);
if (child != null && _isTrivialGate(child)) {
final exits = _exitPortIndices(child, pIdx);
if (exits.length == 1) {
portQueue.add((nId, exits.single));
}
}
}
}
var anyRecursive = false;
// --- Downward recursion into expanded/partial children ---
for (final entry in childPorts.entries) {
final childId = entry.key;
final child = allChildren[childId];
if (child == null) {
continue;
}
if (_isTrivialGate(child)) {
continue; // already traversed through
}
for (final childPortId in entry.value) {
if (expandPortThroughRecursive(
childId,
childPortId,
visited: visited,
)) {
anyRecursive = true;
}
}
}
// --- Upward recursion through the parent boundary ---
// For each boundary port, climb up through ancestor modules:
// 1. At the parent level, call expandPortThrough to reveal the
// connected siblings/edges.
// 2. Walk the parent's hyperedges to find siblings and further
// boundary exits.
// 3. Recurse into siblings that are already open.
// 4. If the signal exits through the parent's own boundary,
// continue climbing (repeat at grandparent level) WITHOUT
// re-entering the parent — that would redundantly expand
// unrelated internal wires.
if (parentPorts.isNotEmpty && node.parent != null) {
// Seed: current node's boundary ports at the first parent level.
var currentNode = node;
var currentParent = node.parent!;
var portsToPropagate = parentPorts;
while (portsToPropagate.isNotEmpty) {
if (!currentParent.isExpanded && !currentParent.isPartiallyExpanded) {
break;
}
final nextPorts = <String>{};
for (final pIdStr in portsToPropagate) {
final propKey = (currentParent.id, pIdStr);
if (visited.contains(propKey)) {
continue;
}
visited.add(propKey);
// Reveal siblings + edges at this level.
if (expandPortThrough(currentParent.id, pIdStr)) {
anyRecursive = true;
}
// Convert string portId to index for parent's lookup.
final pIdxVal = currentParent.portIndexById(pIdStr);
if (pIdxVal < 0) {
continue;
}
final pIdx = currentParent.portHyperedgeIndex;
final pMatching = pIdx[pIdxVal];
if (pMatching == null) {
continue;
}
// Also need current node's port index for the involves check.
final currentNodePortIdx = currentNode.portIndexById(pIdStr);
// Find hyperedges that reference (currentNode.id, portIdx).
for (final h in pMatching) {
var involves = false;
for (final (nId, hpIdx) in h.sources) {
if (nId == currentNode.id && hpIdx == currentNodePortIdx) {
involves = true;
break;
}
}
if (!involves) {
for (final (nId, hpIdx) in h.targets) {
if (nId == currentNode.id && hpIdx == currentNodePortIdx) {
involves = true;
break;
}
}
}
if (!involves) {
continue;
}
for (final (nId, hpIdx) in h.sources) {
if (nId == currentNode.id) {
continue;
}
if (nId == currentParent.id) {
nextPorts.add(currentParent.portIdAt(hpIdx));
} else {
final sibling = nodeMap[nId];
if (sibling != null &&
(sibling.isExpanded || sibling.isPartiallyExpanded)) {
final sibPortStr = sibling.portIdAt(hpIdx);
if (expandPortThroughRecursive(
nId,
sibPortStr,
visited: visited,
)) {
anyRecursive = true;
}
}
}
}
for (final (nId, hpIdx) in h.targets) {
if (nId == currentNode.id) {
continue;
}
if (nId == currentParent.id) {
nextPorts.add(currentParent.portIdAt(hpIdx));
} else {
final sibling = nodeMap[nId];
if (sibling != null &&
(sibling.isExpanded || sibling.isPartiallyExpanded)) {
final sibPortStr = sibling.portIdAt(hpIdx);
if (expandPortThroughRecursive(
nId,
sibPortStr,
visited: visited,
)) {
anyRecursive = true;
}
}
}
}
}
}
// Climb one level higher.
if (nextPorts.isEmpty || currentParent.parent == null) {
break;
}
currentNode = currentParent;
currentParent = currentParent.parent!;
portsToPropagate = nextPorts;
}
}
return changed || anyRecursive;
}