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
.
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:
connection.addPreset(ConnectionPresets.classic.setup())
which is equivalent to the code below:
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.
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.
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
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.
Enhancing the behavior of existing presets can involve tracking the events connectionpick
and connectiondrop
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
connection.addPipe(context => {
if (context.type === 'connectionpick') {
if (readonly) return
}
return context
})
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
.
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.
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.
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.
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