moveNodeDrag method
Moves nodes during a drag operation.
Call this from NodeWidget's GestureDetector.onPanUpdate. The delta is already in graph coordinates since GestureDetector is inside InteractiveViewer's transformed space - no conversion needed.
Position Model
This method uses a two-position model:
- position: The user's intended position (accumulates raw deltas)
- visualPosition: The displayed position (may be snapped)
The snap delegate transforms intended → visual position. This prevents "sticky snap" behavior where small movements can't escape a snap point.
The method applies snapping in this order:
- Snap delegate (alignment guides to other nodes)
- Snap-to-grid (quantizes final position if enabled in config)
Parameters:
graphDelta: The movement delta in graph coordinates
Implementation
void moveNodeDrag(Offset graphDelta) {
final draggedNodeId = interaction.draggedNodeId.value;
if (draggedNodeId == null) return;
// Get nodes to move
final nodeIdsToMove = selectedNodeIds.contains(draggedNodeId)
? selectedNodeIds.toSet()
: {draggedNodeId};
// Collect nodes that will be moved for event firing
final movedNodes = <Node<T>>[];
final context = _createDragContext();
// First pass: Update all nodes' intended positions (raw movement)
runInAction(() {
for (final nodeId in nodeIdsToMove) {
final node = _nodes[nodeId];
if (node != null) {
node.position.value = node.position.value + graphDelta;
movedNodes.add(node);
}
}
});
// Get the primary node's intended position for snap calculation
final primaryNode = _nodes[draggedNodeId];
final intendedPosition = primaryNode?.position.value ?? Offset.zero;
// Calculate snap adjustment
var snappingX = false;
var snappingY = false;
var snapDelta = Offset.zero;
if (_snapDelegate != null) {
final snapResult = _snapDelegate!.snapPosition(
draggedNodeIds: nodeIdsToMove,
intendedPosition: intendedPosition,
visibleBounds: visibleGraphBounds,
);
// Calculate the delta between intended and snapped positions
// This delta will be applied to all selected nodes to maintain relative positions
snapDelta = snapResult.position - intendedPosition;
snappingX = snapResult.snappingX;
snappingY = snapResult.snappingY;
}
// Second pass: Apply visual positions to all moved nodes
runInAction(() {
for (final node in movedNodes) {
// Visual position = intended position + snap delta
final snappedPosition = node.position.value + snapDelta;
// Apply grid snapping only to axes not handled by snap delegate
// This allows alignment snap (when active) to take priority over grid snap
final visualPosition = _applyGridSnapPerAxis(
snappedPosition,
skipX: snappingX,
skipY: snappingY,
);
node.setVisualPosition(visualPosition);
// Notify the node of its own drag move
node.onDragMove(graphDelta, context);
}
});
// Mark moved nodes dirty for spatial index
_markNodesDirty(movedNodes.map((n) => n.id));
// Fire drag event for all moved nodes
for (final node in movedNodes) {
events.node?.onDrag?.call(node);
}
}