expandPort method

bool expandPort(
  1. String nodeId,
  2. String portId
)

Incrementally expand a single port of a collapsed child module.

Reveals only the internal edges connected to portId and the submodules at the other end of those edges. This is a partial expansion — the node is not fully toggled, just the port's immediate fan-in/fan-out becomes visible.

Returns true if new children/edges were revealed.

Implementation

bool expandPort(String nodeId, String portId) {
  final node = nodeMap[nodeId];
  if (node == null) {
    return false;
  }

  // Node must have hidden children for partial expansion to make sense.
  if (node.hiddenChildren == null || node.hiddenChildren!.isEmpty) {
    return false;
  }

  final hyperedges = node.hyperedges;
  if (hyperedges == null || hyperedges.isEmpty) {
    return false;
  }

  // Use the port→hyperedge index for O(1) lookup.
  // Convert string portId to local port index.
  // The port might belong to this node or one of its children.
  final resolved = node.resolvePortId(portId);
  if (resolved == null) {
    return false;
  }
  final resolvedNodeId = resolved.$1;
  final portIdx = resolved.$2;
  final idx = node.portHyperedgeIndex;
  List<LayoutHyperedge>? matching;
  if (resolvedNodeId == node.id) {
    matching = idx[portIdx];
  } else {
    matching = _findHyperedgesForPort(node, resolvedNodeId, portIdx);
  }
  if (matching == null || matching.isEmpty) {
    return false;
  }

  final matchingHyperedgeIds = <String>{};
  final connectedChildIds = <String>{};

  // When the clicked port belongs to a specific child instance, only
  // reveal that child plus children on the OTHER side of the hyperedge.
  // Without this, clicking one TagManager's "count" port would reveal
  // ALL sibling TagManager instances connected to the same bus.
  final clickedIsChild = resolvedNodeId != node.id;

  for (final h in matching) {
    matchingHyperedgeIds.add(h.id);
    if (clickedIsChild) {
      // Determine which side the clicked child is on.
      final clickedInSources = h.sources.any((s) => s.$1 == resolvedNodeId);
      final clickedInTargets = h.targets.any((t) => t.$1 == resolvedNodeId);
      // Always include the clicked child itself.
      connectedChildIds.add(resolvedNodeId);
      // Include children on the opposite side (and the parent port side).
      if (clickedInSources) {
        for (final (nId, _) in h.targets) {
          if (nId != node.id) {
            connectedChildIds.add(nId);
          }
        }
      }
      if (clickedInTargets) {
        for (final (nId, _) in h.sources) {
          if (nId != node.id) {
            connectedChildIds.add(nId);
          }
        }
      }
    } else {
      // Clicked port is on the parent node itself — reveal all children.
      for (final (nId, _) in h.sources) {
        if (nId != node.id) {
          connectedChildIds.add(nId);
        }
      }
      for (final (nId, _) in h.targets) {
        if (nId != node.id) {
          connectedChildIds.add(nId);
        }
      }
    }
  }

  if (connectedChildIds.isEmpty && matchingHyperedgeIds.isEmpty) {
    return false;
  }

  // Check if everything is already visible (idempotent click)
  final existingChildren = node.partialChildIds ?? {};
  final existingEdges = node.partialHyperedgeIds ?? {};
  if (existingChildren.containsAll(connectedChildIds) &&
      existingEdges.containsAll(matchingHyperedgeIds)) {
    return false; // Already visible — no change
  }

  // Additive: merge into existing partial sets.
  final mergedChildren = {...existingChildren, ...connectedChildIds};
  final mergedEdges = {...existingEdges, ...matchingHyperedgeIds};

  // Also include any inter-child hyperedge that shares at least one
  // exact (nodeId, portIndex) endpoint with the directly matched
  // hyperedges from *this* click.  This ensures we only pull in edges
  // on the same signal network — not unrelated internal signals whose
  // children happen to all be visible (e.g. in blocks-only mode).
  //
  // Collect the child-side endpoints of the currently matched edges.
  final matchedEndpoints = <(String, int)>{};
  for (final h in matching) {
    for (final s in h.sources) {
      if (s.$1 != node.id) {
        matchedEndpoints.add(s);
      }
    }
    for (final t in h.targets) {
      if (t.$1 != node.id) {
        matchedEndpoints.add(t);
      }
    }
  }

  for (final h in hyperedges) {
    if (mergedEdges.contains(h.id)) {
      continue;
    }
    final allChildEndpoints =
        h.sources.every((s) => mergedChildren.contains(s.$1)) &&
            h.targets.every((t) => mergedChildren.contains(t.$1));
    if (!allChildEndpoints) {
      continue;
    }
    // Only add if this edge shares at least one (nodeId, portIndex)
    // with the directly matched hyperedges from this click.
    final sharesEndpoint = h.sources.any(matchedEndpoints.contains) ||
        h.targets.any(matchedEndpoints.contains);
    if (!sharesEndpoint) {
      continue;
    }
    mergedEdges.add(h.id);
  }

  node
    ..partialChildIds = mergedChildren
    ..partialHyperedgeIds = mergedEdges;

  return true;
}