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