collapsePortThroughRecursive method

bool collapsePortThroughRecursive(
  1. String nodeId,
  2. String portId, {
  3. Set<(String, String)>? visited,
})

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;
}