handlePortCollapse method

Future<SchematicLayoutResult?> handlePortCollapse(
  1. String nodeId,
  2. String portId
)

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