handlePortCollapse method
Handle port collapse (click on a boundary port with a visible wire).
Reverse of handlePortExpand: removes the wire and connected trivial
gates from partial expansion.
Implementation
Future<SchematicLayoutResult?> handlePortCollapse(
String nodeId,
String portId,
) async {
if (isToggling) {
return null;
}
final engine = _layoutEngine;
if (engine == null || schematicAdapter == null) {
return null;
}
setState(() {
isToggling = true;
togglingLabel = 'Collapsing...';
pendingToggleNodeId = nodeId;
});
final completer = Completer<void>();
WidgetsBinding.instance.addPostFrameCallback((_) {
completer.complete();
});
await completer.future;
SchedulerBinding.instance.ensureVisualUpdate();
try {
// Before collapsing, check if this is the last connection to the port
// being collapsed and find the closest non-constant connected gate if so
String? closestConnectedGateId;
final oldEdges = layout?.edges ?? <SchematicEdgeData>[];
// Count connections specifically to THIS PORT being collapsed
var connectionsFromThisPort = 0;
String? otherPortOnConnection;
for (final edge in oldEdges) {
// Check if this edge connects to the specific port being collapsed
if (edge.sourcePort == portId) {
connectionsFromThisPort++;
otherPortOnConnection = edge.targetPort;
} else if (edge.targetPort == portId) {
connectionsFromThisPort++;
otherPortOnConnection = edge.sourcePort;
}
}
// Helper function to check if a gate is a constant (like 0x0, 0x1, etc.)
bool isConstantGate(String? gateId) {
if (gateId == null) {
return false;
}
final node = schematicAdapter?.schematic.nodeMap[gateId];
if (node == null) {
return false;
}
return isConstantName(node.hwMeta.name);
}
// Helper function to find the closest non-constant gate by tracing
// connections
String? findNonConstantConnectedGate(String startPortId) {
final visited = <String>{};
final queue = <String>[startPortId];
while (queue.isNotEmpty) {
final currentPortId = queue.removeAt(0);
if (visited.contains(currentPortId)) {
continue;
}
visited.add(currentPortId);
// Find the gate for this port
String? currentGateId;
try {
final port = layout!.ports.firstWhere((p) => p.id == currentPortId);
currentGateId = port.instanceId;
} on Object catch (_) {
continue;
}
// If this gate is not constant, return it
if (!isConstantGate(currentGateId)) {
return currentGateId;
}
// If it's constant, queue its connected ports
for (final edge in oldEdges) {
if (edge.sourcePort == currentPortId &&
edge.targetPort != null &&
!visited.contains(edge.targetPort)) {
queue.add(edge.targetPort!);
} else if (edge.targetPort == currentPortId &&
edge.sourcePort != null &&
!visited.contains(edge.sourcePort)) {
queue.add(edge.sourcePort!);
}
}
}
return null; // No non-constant gate found
}
// If this port has only 1 connection, find the closest non-constant gate
if (connectionsFromThisPort == 1 && otherPortOnConnection != null) {
closestConnectedGateId = findNonConstantConnectedGate(
otherPortOnConnection,
);
}
// Use the new method that returns removed gate IDs for smart focus
// recovery
final (collapsed, removedGates) =
schematicAdapter!.collapsePortWithRemoved(nodeId, portId);
if (!collapsed) {
if (mounted) {
setState(() {
isToggling = false;
pendingToggleNodeId = null;
});
}
return null;
}
final elkGraph = schematicAdapter!.schematic.toJsGraph();
final newLayout = await engine.computeLayoutFromElkGraph(
elkGraph,
sessionId: _sessionId,
);
if (newLayout.hasError || !mounted) {
if (mounted) {
setState(() {
isToggling = false;
pendingToggleNodeId = null;
});
}
return null;
}
// Determine focus point for zoom after collapse.
// Case 1: Original port still exists → focus on it
// Case 2: Original gate was removed → find closest connected gate that
// wasn't
// removed and focus on one of its ports
// Case 3: Gates connected to the port were removed → find what those
// gates were connected to and focus on a surviving connection
// Case 4: No good focus point → leave viewport where it was (implicit)
String? newFocusPortId;
final portStillExists = newLayout.ports.any((p) => p.id == portId);
if (portStillExists) {
// Port still exists—keep it in focus
newFocusPortId = portId;
} else if (removedGates.contains(nodeId)) {
// The gate that owned this port was removed. Find what it was connected
// to and focus on a gate that wasn't removed.
String? bestConnectedPort;
// Look at old edges to find what was connected to portId
for (final edge in layout?.edges ?? <SchematicEdgeData>[]) {
if (edge.sourcePort == portId) {
bestConnectedPort = edge.targetPort;
break;
} else if (edge.targetPort == portId) {
bestConnectedPort = edge.sourcePort;
break;
}
}
// If we found a connected port, check if it still exists and its gate
// wasn't also removed
if (bestConnectedPort != null) {
SchematicPortData? connectedPort;
try {
connectedPort = newLayout.ports.firstWhere(
(p) => p.id == bestConnectedPort,
);
} on Object catch (_) {
// Port not found in new layout
connectedPort = null;
}
if (connectedPort != null &&
!removedGates.contains(connectedPort.instanceId)) {
newFocusPortId = bestConnectedPort;
}
}
} else if (removedGates.isNotEmpty) {
// Gates connected to the collapsed port were removed. Find what those
// removed gates were connected to and focus on a survivor.
String? bestConnectedPort;
final oldEdges = layout?.edges ?? <SchematicEdgeData>[];
// Look at edges in the old layout to find what removed gates were
// connected to
for (final edge in oldEdges) {
String? otherPortId;
// Check if the source port belongs to a removed gate
try {
final sourcePort = newLayout.ports.firstWhere(
(p) => p.id == edge.sourcePort,
);
if (removedGates.contains(sourcePort.instanceId)) {
otherPortId = edge.targetPort;
}
} on Object catch (_) {
// Source port not found in new layout
}
// Check if the target port belongs to a removed gate
if (otherPortId == null) {
try {
final targetPort = newLayout.ports.firstWhere(
(p) => p.id == edge.targetPort,
);
if (removedGates.contains(targetPort.instanceId)) {
otherPortId = edge.sourcePort;
}
} on Object catch (_) {
// Target port not found in new layout
}
}
// If we found a port connected to a removed gate, check if it
// still exists in the new layout
if (otherPortId != null) {
try {
newLayout.ports.firstWhere((p) => p.id == otherPortId);
// Found a port connected to a removed gate that still exists
bestConnectedPort = otherPortId;
break;
} on Object catch (_) {
// Port not found, continue searching
}
}
}
if (bestConnectedPort != null) {
newFocusPortId = bestConnectedPort;
}
}
// Case 5: This was the last connection - pan to th e closest connected
// gate's port
if (newFocusPortId == null &&
closestConnectedGateId != null &&
!removedGates.contains(closestConnectedGateId)) {
try {
final closestPort = newLayout.ports.firstWhere(
(p) => p.instanceId == closestConnectedGateId,
);
newFocusPortId = closestPort.id;
} on Object catch (_) {
// No port found in closest gate
}
}
// When port-level focus is unavailable, zoom to the parent
// boundary so the collapsed block is visible in context.
final parentId = schematicAdapter!.schematic.nodeMap[nodeId]?.parent?.id;
setState(() {
layout = newLayout;
isToggling = false;
pendingToggleNodeId = null;
if (newFocusPortId != null) {
focusPortId = newFocusPortId;
}
recentlyToggledNodeId = parentId ?? nodeId;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
focusPortId = null;
recentlyToggledNodeId = null;
});
}
});
return newLayout;
} on Exception {
if (mounted) {
setState(() {
isToggling = false;
pendingToggleNodeId = null;
});
}
return null;
}
}