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

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',
    );
  }

  // Cannot connect a port to itself
  if (temp.startNodeId == targetNodeId && temp.startPortId == targetPortId) {
    return const ConnectionValidationResult.deny(
      reason: 'Cannot connect a port to itself',
    );
  }

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

  // Cannot connect same direction ports
  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
  final targetPort = targetNode.allPorts
      .where((p) => p.id == targetPortId)
      .firstOrNull;
  if (targetPort == null) {
    return const ConnectionValidationResult.deny(
      reason: 'Target port not found',
    );
  }

  // 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',
    );
  }

  // Direction compatibility
  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
  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();

  // 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',
    );
  }

  // Max connections limit
  if (actualTargetPort.maxConnections != null) {
    if (existingTargetConnections.length >=
        actualTargetPort.maxConnections!) {
      return const ConnectionValidationResult.deny(
        reason: 'Target port has maximum connections',
      );
    }
  }

  // Custom validation callback
  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();
}