collapsePortThroughRecursive method
Recursively collapse a port through trivial gates and across already-opened module boundaries.
Uses the same directional discovery as expandPortThroughRecursive, but
only descends into children that remain expanded or partially expanded.
Returns true if any children/edges were removed at any level.
Implementation
bool collapsePortThroughRecursive(
String nodeId,
String portId, {
Set<(String, String)>? visited,
}) {
visited ??= {};
final key = (nodeId, portId);
if (visited.contains(key)) {
return false;
}
visited.add(key);
// Collapse at this level.
final changed = collapsePortThrough(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 = collapsePortThroughRecursive(
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` — identical to expandPortThroughRecursive.
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)>{};
// childPorts: childNodeId → set of port IDs (strings for recursive calls)
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;
}
if (!child.isExpanded && !child.isPartiallyExpanded) {
continue;
}
for (final childPortId in entry.value) {
if (collapsePortThroughRecursive(
childId,
childPortId,
visited: visited,
)) {
anyRecursive = true;
}
}
}
// --- Upward recursion through the parent boundary ---
if (parentPorts.isNotEmpty && node.parent != null) {
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);
if (collapsePortThrough(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);
for (final h in pMatching) {
// Check if this hyperedge involves (currentNode.id, portIdx).
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 (collapsePortThroughRecursive(
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 (collapsePortThroughRecursive(
nId,
sibPortStr,
visited: visited,
)) {
anyRecursive = true;
}
}
}
}
}
}
if (nextPorts.isEmpty || currentParent.parent == null) {
break;
}
currentNode = currentParent;
currentParent = currentParent.parent!;
portsToPropagate = nextPorts;
}
}
return changed || anyRecursive;
}