collapsePortThroughWithRemoved method
Collapse the children and edges that expandPortThrough would reveal.
Runs the same BFS from portId through hyperedges and trivial gates, but
removes the discovered children and edges from the partial expansion
sets instead of adding them. A child is only removed when no remaining
visible hyperedge still references it.
Non-trivial child modules are always kept in partialChildIds even when
no remaining edge references them — only trivial pass-through gates are
removed. This ensures that block-mode children survive port collapse: if
a module was visible before the port was expanded, collapsing the port
must not hide it.
If the node is fully expanded, initializes it to a partially-expanded
state containing all visible children and edges before removing the ones
associated with portId.
Returns a tuple of (changed, removedChildIds) where:
- changed: true if any children/edges were removed
- removedChildIds: set of child node IDs that were removed (for focus recovery)
Implementation
(bool, Set<String>) collapsePortThroughWithRemoved(
String nodeId,
String portId,
) {
final node = nodeMap[nodeId];
if (node == null) {
return (false, <String>{});
}
final hyperedges = node.hyperedges;
if (hyperedges == null || hyperedges.isEmpty) {
return (false, <String>{});
}
// If the node is fully expanded, physically move all children to
// hiddenChildren so that _applyPartialExpansions can selectively
// restore only the ones we keep. Just setting partialChildIds without
// moving children would leave them all visible because
// _applyPartialExpansions only acts when hiddenChildren is non-empty.
// We deliberately do NOT call node.toggle() here because toggle()
// calls clearDescendantMarks which would discard any partial/blocks-only
// expansion state on sub-children.
if (node.isExpanded && !node.isPartiallyExpanded) {
final allChildIds = node.children.map((c) => c.id).toSet();
final allEdgeIds = hyperedges.map((h) => h.id).toSet();
node
..hiddenChildren = List.of(node.children)
..children = []
..partialChildIds = allChildIds
..partialHyperedgeIds = allEdgeIds;
}
// Must be partially expanded for collapse to make sense.
if (!node.isPartiallyExpanded) {
return (false, <String>{});
}
// Use shared collection logic: same BFS traversal as expand.
// This ensures we remove exactly what expand added.
final (collectedChildIds, collectedHyperedgeIds) =
_collectPortThroughGatesAndEdges(node, portId);
if (collectedChildIds.isEmpty && collectedHyperedgeIds.isEmpty) {
return (false, <String>{});
}
final existingChildren = node.partialChildIds;
final existingEdges = node.partialHyperedgeIds;
if (existingChildren == null || existingEdges == null) {
return (false, <String>{});
}
// Remove ONLY the gates and edges discovered on THIS BFS path.
// This ensures perfect symmetry with expand.
var remainingChildren = existingChildren.difference(collectedChildIds);
final remainingEdges = existingEdges.difference(collectedHyperedgeIds);
// A collected child must only be hidden when ALL of the following hold:
// 1. No remaining visible edge still references it (e.g. a BUF whose
// input wire is still shown must stay visible), AND
// 2. It is a primitive leaf node (operator/gate with no sub-children).
// Non-primitive submodules are kept visible even after their last
// wire is collapsed — the user must explicitly toggle them closed.
final allChildMap = <String, LayoutNode>{};
for (final c in node.children) {
allChildMap[c.id] = c;
}
if (node.hiddenChildren != null) {
for (final c in node.hiddenChildren!) {
allChildMap[c.id] = c;
}
}
for (final childId in collectedChildIds) {
// Keep if a remaining edge still references this child.
var keep = false;
for (final h in hyperedges) {
if (!remainingEdges.contains(h.id)) {
continue;
}
if (h.sources.any((s) => s.$1 == childId) ||
h.targets.any((t) => t.$1 == childId)) {
keep = true;
break;
}
}
// Also keep if this is a non-primitive submodule (has sub-children).
if (!keep) {
final child = allChildMap[childId];
if (child != null &&
(child.children.isNotEmpty ||
(child.hiddenChildren != null &&
child.hiddenChildren!.isNotEmpty))) {
keep = true;
}
}
if (keep) {
remainingChildren = {...remainingChildren, childId};
}
}
// Check if anything actually changed.
if (remainingChildren.length == existingChildren.length &&
remainingEdges.length == existingEdges.length) {
return (false, <String>{});
}
if (remainingChildren.isEmpty && remainingEdges.isEmpty) {
// Fully collapsed — clear partial state.
node
..partialChildIds = null
..partialHyperedgeIds = null;
} else {
node
..partialChildIds = remainingChildren
..partialHyperedgeIds = remainingEdges;
}
return (true, collectedChildIds);
}