handlePortCollapseThrough method

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

Handle recursive port collapse (Shift+click on a boundary port).

Reverse of handlePortExpandThrough: removes the wire and connected trivial gates recursively across module boundaries.

Implements smart focus recovery: if collapsing is the last connection to the original port, finds the closest non-constant connected gate to focus on after collapse, similar to port collapse behavior.

Implementation

Future<SchematicLayoutResult?> handlePortCollapseThrough(
  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
    // and find the closest non-constant connected gate for focus recovery
    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
    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,
      );
    }

    final collapsed = schematicAdapter!.collapsePortRecursive(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 pan after collapse. Case 1: Original port
    // still exists → focus on it Case 2: Was last connection and found a
    // close non-constant gate → focus there Case 3: No good focus point →
    // keep viewport where it was
    String? newFocusPortId;
    final portStillExists = newLayout.ports.any((p) => p.id == portId);

    if (portStillExists) {
      // Port still exists—keep it in focus
      newFocusPortId = portId;
    } else if (closestConnectedGateId != null) {
      // Port was deleted but we found a non-constant gate to focus on
      // Find a port on that gate to focus on
      try {
        final gatePort = newLayout.ports.firstWhere(
          (p) => p.instanceId == closestConnectedGateId,
        );
        newFocusPortId = gatePort.id;
      } on Object catch (_) {
        // Port not found in closest gate
      }
    }

    setState(() {
      layout = newLayout;
      isToggling = false;
      pendingToggleNodeId = null;
      if (newFocusPortId != null) {
        focusPortId = newFocusPortId;
      }
    });

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        setState(() {
          focusPortId = null;
        });
      }
    });

    return newLayout;
  } on Exception {
    if (mounted) {
      setState(() {
        isToggling = false;
        pendingToggleNodeId = null;
      });
    }
    return null;
  }
}