Connections - Rete.js

Connections

This guide is based on the Basic guide. It is recommended to review it for a comprehensive understanding of this guide.

ValidationChatbot

This guide provides a more detailed exploration of the functionalities offered by the rete-connection-plugin plugin, enabling user interaction with connections.

When a user clicks on a socket, the connection that trails behind the cursor is referred to as a pseudo-connection, which is is an object with an additional property isPseudo: true.

Presets

You may have already seen the following preset usage, which allows users to add connections by clicking/pressing on an input/output socket and clicking/releasing on an output/input socket:

ts
connection.addPreset(ConnectionPresets.classic.setup())

which is equivalent to the code below:

ts
import { ClassicFlow } from 'rete-connection-plugin'

connection.addPreset(() => new ClassicFlow())

If the input socket is already connected, clicking or pressing on it will remove the connection and substitute it with a pseudo-connection.

Custom preset

If you prefer an alternative method for adding connections, you can make use of BidirectFlow. In this mode, adding a node is done by clicking on the input/output socket and dragging the pseudo-connection onto a socket of the opposite type.

ts
import { ClassicFlow } from 'rete-connection-plugin'

connection.addPreset(() => new BidirectFlow())

Additionally, utilizing the initial socket data, you have the option to select a specific flow or disable interaction with a particular socket altogether

ts
connection.addPreset(({ nodeId, side, key }) => {
  if (isReadonly(nodeId, side, key)) return undefined
  if (usesBidirect(nodeId, side, key)) return new BidirectFlow()
  return new ClassicFlow()
})

If the existing workflows don't meet your needs, you have the option to implement your own solution by referencing the source code of the existing ones.

Enhanced behavior

Enhancing the behavior of existing presets can involve tracking the events connectionpick and connectiondrop

ts
connection.addPipe(context => {
  if (context.type === 'connectionpick') { // when the user clicks on the socket
    const { socket } = context.data
  }
  if (context.type === 'connectiondrop') { // when the user clicks on the socket or any area
    const { socket, initial, created } = context.data
  }
  return context
})

The connectionpick event can be prevented

ts
connection.addPipe(context => {
  if (context.type === 'connectionpick') {
    if (readonly) return
  }
  return context
})

User-created connection

By default, when a user creates connections using the UI, the plugin adds the connection as an object, rather than an instance of a class, such as ClassicPreset.Connection. If you want to customize the process of adding these connection, specify the makeConnection option in ClassicFlow or BidirectFlow.

ts
import { getSourceTarget } from 'rete-connection-plugin'

connection.addPreset(() => new ClassicFlow({
  makeConnection(from, to, context) {
    const [source, target] = getSourceTarget(from, to) || [null, null];
    const { editor } = context;

    if (source && target) {
      editor.addConnection(
        new MyConnection(
          editor.getNode(source.nodeId),
          source.key,
          editor.getNode(target.nodeId),
          target.key
        )
      );
      return true; // ensure that the connection has been successfully added
    }
  }
}))

Additionally, the usage of getSourceTarget is essential in this case, as the from and to options carry data about the initial and final sockets, which might not necessarily match the output and input sockets.

Configure connection start/end positions

Every connection is associated with a starting and ending point, directly connected to a socket (excluding pseudo-connections where end point depends on the cursor position). By default, the right side of the output socket serves as the starting point, while the left side of the input socket serves as the starting point. Modifying the socket size will cause a shift in the starting/ending point of the connection.

To configure the start and end positions of the connection, you can provide getDOMSocketPosition with these offset coordinates relative to the socket center. This method is used by default when the socketPositionWatcher option is not specified.

ts
import { getDOMSocketPosition } from 'rete-render-utils'

render.addPreset(Presets.classic.setup({
  socketPositionWatcher: getDOMSocketPosition({
    offset({ x, y }, nodeId, side, key) {

      return {
        x: x + 10 * (side === 'input' ? -1 : 1),
        y: y
      }
    },
  })
}))

In this scenario, socket position calculation relies on DOM elements. There are cases when such an approach may prove inefficient due to performance or other implementation nuances (for instance, in the LOD example where displaying connections is required even when the actual sockets are not present in the DOM).

In order to make use of your calculation approach for connection start/end positions, you can extend the abstract class BaseSocketPosition and implement the calculatePosition method.

ts
import { BaseSocketPosition } from 'rete-render-utils'

type Position = { x: number, y: number }
type Side = 'input' | 'output'

export class ComputedSocketPosition<S extends Schemes, K> extends BaseSocketPosition<S, K> {
  async calculatePosition(nodeId: string, side: Side, key: string): Promise<Position | null> {
    if (!this.area) return null

    return {
      x: side === 'input' ? 0 : getNodeWith(nodeId)
      y: 0
    }
  }
}

render.addPreset(Presets.classic.setup({
  socketPositionWatcher: new ComputedSocketPosition()
}))

where calculatePosition is expected to return the position relative to the node's position, or null if it cannot be calculated