canConnect method

ConnectionValidationResult canConnect({
  1. required String targetNodeId,
  2. required String targetPortId,
  3. bool skipCustomValidation = false,
})

Validates whether a connection can be made from the current drag state to the specified target port.

This method checks:

  1. Not connecting a port to itself (same node + same port is invalid, but same node with different ports is OK for self-loops)
  2. Direction compatibility (output→input or input←output)
  3. Port is connectable
  4. No duplicate connections
  5. Max connections limit not exceeded
  6. Custom validation via ConnectionEvents.onBeforeComplete callback (only when skipCustomValidation is false)

The skipCustomValidation parameter allows skipping the expensive custom validation callback during drag updates. This is useful for hover feedback where only basic structural validation is needed. The full validation including custom callbacks runs at connection completion time.

Returns a ConnectionValidationResult indicating if the connection is valid.

Implementation

ConnectionValidationResult canConnect({
  required String targetNodeId,
  required String targetPortId,
  bool skipCustomValidation = false,
}) {
  final temp = interaction.temporaryConnection.value;
  if (temp == null) {
    return const ConnectionValidationResult.deny(
      reason: 'No active connection drag',
    );
  }

  // 1. Cannot connect a port to itself
  // Same node is OK (self-loops), but same port on same node is NOT
  if (temp.startNodeId == targetNodeId && temp.startPortId == targetPortId) {
    return const ConnectionValidationResult.deny(
      reason: 'Cannot connect a port to itself',
    );
  }

  // Get target node first (needed for direction check)
  final targetNode = _nodes[targetNodeId];
  if (targetNode == null) {
    return const ConnectionValidationResult.deny(
      reason: 'Target node not found',
    );
  }

  // 2. Cannot connect same direction ports (output→output or input→input)
  // Determine if target port is in outputPorts or inputPorts list
  final targetIsOutput = targetNode.outputPorts.any(
    (p) => p.id == targetPortId,
  );
  if (temp.isStartFromOutput == targetIsOutput) {
    return ConnectionValidationResult.deny(
      reason: targetIsOutput
          ? 'Cannot connect output to output'
          : 'Cannot connect input to input',
    );
  }

  // Get source node and port
  final sourceNode = _nodes[temp.startNodeId];
  if (sourceNode == null) {
    return const ConnectionValidationResult.deny(
      reason: 'Source node not found',
    );
  }
  final sourcePort = sourceNode.allPorts
      .where((p) => p.id == temp.startPortId)
      .firstOrNull;
  if (sourcePort == null) {
    return const ConnectionValidationResult.deny(
      reason: 'Source port not found',
    );
  }

  // Get target port (targetNode already looked up above)
  final targetPort = targetNode.allPorts
      .where((p) => p.id == targetPortId)
      .firstOrNull;
  if (targetPort == null) {
    return const ConnectionValidationResult.deny(
      reason: 'Target port not found',
    );
  }

  // 2. Both ports must be connectable
  if (!sourcePort.isConnectable) {
    return const ConnectionValidationResult.deny(
      reason: 'Source port is not connectable',
    );
  }
  if (!targetPort.isConnectable) {
    return const ConnectionValidationResult.deny(
      reason: 'Target port is not connectable',
    );
  }

  // 3. Direction compatibility (port type check)
  // If started from output, target must be able to accept input
  // If started from input, target must be able to emit output
  if (temp.isStartFromOutput) {
    if (!targetPort.isInput) {
      return const ConnectionValidationResult.deny(
        reason: 'Target port cannot receive connections',
      );
    }
  } else {
    if (!targetPort.isOutput) {
      return const ConnectionValidationResult.deny(
        reason: 'Target port cannot emit connections',
      );
    }
  }

  // Determine actual source/target for duplicate and max connection checks
  final Node<T> actualSourceNode;
  final Port actualSourcePort;
  final Node<T> actualTargetNode;
  final Port actualTargetPort;

  if (temp.isStartFromOutput) {
    actualSourceNode = sourceNode;
    actualSourcePort = sourcePort;
    actualTargetNode = targetNode;
    actualTargetPort = targetPort;
  } else {
    actualSourceNode = targetNode;
    actualSourcePort = targetPort;
    actualTargetNode = sourceNode;
    actualTargetPort = sourcePort;
  }

  // Get existing connections for both ports
  final existingSourceConnections = _connections
      .where(
        (conn) =>
            conn.sourceNodeId == actualSourceNode.id &&
            conn.sourcePortId == actualSourcePort.id,
      )
      .map((c) => c.id)
      .toList();
  final existingTargetConnections = _connections
      .where(
        (conn) =>
            conn.targetNodeId == actualTargetNode.id &&
            conn.targetPortId == actualTargetPort.id,
      )
      .map((c) => c.id)
      .toList();

  // 4. No duplicate connections
  final duplicateExists = _connections.any(
    (conn) =>
        conn.sourceNodeId == actualSourceNode.id &&
        conn.sourcePortId == actualSourcePort.id &&
        conn.targetNodeId == actualTargetNode.id &&
        conn.targetPortId == actualTargetPort.id,
  );
  if (duplicateExists) {
    return const ConnectionValidationResult.deny(
      reason: 'Connection already exists',
    );
  }

  // 5. Max connections limit (only check target port since it receives the connection)
  if (actualTargetPort.maxConnections != null) {
    if (existingTargetConnections.length >=
        actualTargetPort.maxConnections!) {
      return const ConnectionValidationResult.deny(
        reason: 'Target port has maximum connections',
      );
    }
  }

  // 6. Call custom validation callback if provided (skip during drag for performance)
  if (!skipCustomValidation) {
    final onBeforeComplete = events.connection?.onBeforeComplete;
    if (onBeforeComplete != null) {
      final context = ConnectionCompleteContext<T>(
        sourceNode: actualSourceNode,
        sourcePort: actualSourcePort,
        targetNode: actualTargetNode,
        targetPort: actualTargetPort,
        existingSourceConnections: existingSourceConnections,
        existingTargetConnections: existingTargetConnections,
      );
      final result = onBeforeComplete(context);
      if (!result.allowed) {
        return result;
      }
    }
  }

  return const ConnectionValidationResult.allow();
}