Vyuh Node Flow

NodeFlowController

API reference for the NodeFlowController class

NodeFlowController

The NodeFlowController manages all graph state including nodes, connections, selection, and viewport. It's the central point for programmatic graph manipulation.

Constructor

NodeFlowController<T>({
  GraphViewport? initialViewport,
  NodeFlowConfig? config,
})
ParameterTypeDescription
initialViewportGraphViewport?Initial viewport position and zoom
configNodeFlowConfig?Behavioral configuration (snap-to-grid, zoom limits, etc.)

Node Operations

addNode

Add a node to the graph.

void addNode(Node<T> node)

Example:

final node = Node<MyData>(
  id: 'node-1',
  position: Offset(100, 100),
  size: Size(150, 80),
  data: MyData(label: 'Process'),
  inputPorts: [Port(id: 'in-1', name: 'Input')],
  outputPorts: [Port(id: 'out-1', name: 'Output')],
);
controller.addNode(node);

removeNode

Remove a node and all its connections.

void removeNode(String nodeId)

Removing a node automatically removes all connections to and from that node, and removes the node from any group annotations.

moveNode

Move a node by a delta offset.

void moveNode(String nodeId, Offset delta)

moveSelectedNodes

Move all selected nodes by a delta offset.

void moveSelectedNodes(Offset delta)

setNodeSize

Update a node's size.

void setNodeSize(String nodeId, Size size)

setNodePorts

Replace a node's ports.

void setNodePorts(String nodeId, {
  List<Port>? inputPorts,
  List<Port>? outputPorts,
})

addInputPort / addOutputPort

Add ports to an existing node.

void addInputPort(String nodeId, Port port)
void addOutputPort(String nodeId, Port port)

removePort

Remove a port and all its connections.

void removePort(String nodeId, String portId)

getNode

Get a node by ID.

Node<T>? getNode(String nodeId)

Connection Operations

addConnection

Create a connection between ports.

void addConnection(Connection connection)

Example:

final connection = Connection(
  id: 'conn-1',
  sourceNodeId: 'node-1',
  sourcePortId: 'out-1',
  targetNodeId: 'node-2',
  targetPortId: 'in-1',
);
controller.addConnection(connection);

removeConnection

Remove a connection by ID.

void removeConnection(String connectionId)

getConnectionsForNode

Get all connections for a node.

List<Connection> getConnectionsForNode(String nodeId)

connections

Access all connections (read-only list).

List<Connection> get connections

Control Points

Connections support user-defined control points for custom routing.

addControlPoint

void addControlPoint(String connectionId, Offset position, {int? index})

updateControlPoint

void updateControlPoint(String connectionId, int index, Offset position)

removeControlPoint

void removeControlPoint(String connectionId, int index)

clearControlPoints

void clearControlPoints(String connectionId)

Selection

selectNode

Select a node. Use toggle: true for multi-select behavior.

void selectNode(String nodeId, {bool toggle = false})

selectNodes

Select multiple nodes.

void selectNodes(List<String> nodeIds, {bool toggle = false})

selectConnection

Select a connection.

void selectConnection(String connectionId, {bool toggle = false})

clearSelection

Clear all selections (nodes, connections, annotations).

void clearSelection()

clearNodeSelection / clearConnectionSelection

Clear specific selection types.

void clearNodeSelection()
void clearConnectionSelection()

selectedNodeIds

Get IDs of selected nodes.

Set<String> get selectedNodeIds

hasSelection

Check if anything is selected.

bool get hasSelection

isNodeSelected

Check if a specific node is selected.

bool isNodeSelected(String nodeId)

Viewport

viewport

Current viewport state.

GraphViewport get viewport

Returns GraphViewport with:

  • x, y - Pan offset
  • zoom - Zoom level

currentZoom / currentPan

Access current zoom level and pan position.

double get currentZoom
ScreenOffset get currentPan

setViewport

Set viewport directly.

void setViewport(GraphViewport viewport)

panBy

Pan viewport by a delta.

void panBy(ScreenOffset delta)

zoomBy

Zoom by a delta amount (positive = zoom in), keeping the viewport center fixed.

void zoomBy(double delta)

zoomTo

Set zoom to a specific level.

void zoomTo(double zoom)

fitToView

Fit all nodes in the viewport with padding.

void fitToView()

fitSelectedNodes

Fit only selected nodes in the viewport with padding.

void fitSelectedNodes()

centerOnNode

Center viewport on a specific node without changing zoom.

void centerOnNode(String nodeId)

centerOnSelection

Center viewport on the geometric center of selected nodes.

void centerOnSelection()

centerViewport

Center viewport on the geometric center of all nodes.

void centerViewport()

centerOn

Center viewport on a specific point in graph coordinates.

void centerOn(GraphOffset point)

getViewportCenter

Get the center point of the viewport in graph coordinates.

GraphPosition getViewportCenter()

resetViewport

Reset viewport to zoom 1.0 and center on all nodes.

void resetViewport()

Coordinate Transformations

Node Flow uses typed coordinate systems to prevent accidentally mixing screen and graph coordinates.

globalToGraph

Convert global screen position to graph coordinates.

GraphPosition globalToGraph(ScreenPosition globalPosition)

Example:

// In a gesture callback
final graphPos = controller.globalToGraph(
  ScreenPosition(details.globalPosition)
);

graphToScreen

Convert graph coordinates to screen coordinates.

ScreenPosition graphToScreen(GraphPosition graphPoint)

screenToGraph

Convert screen coordinates to graph coordinates.

GraphPosition screenToGraph(ScreenPosition screenPoint)

Example:

// Convert mouse position to graph coordinates
final graphPos = controller.screenToGraph(
  ScreenPosition(event.localPosition)
);

Visibility & Bounds

viewportExtent

Get the visible area in graph coordinates.

GraphRect get viewportExtent

viewportScreenBounds

Get the viewport bounds in global screen coordinates.

ScreenRect get viewportScreenBounds

isPointVisible

Check if a graph coordinate point is visible.

bool isPointVisible(GraphPosition graphPoint)

isRectVisible

Check if a graph coordinate rectangle is visible (useful for culling).

bool isRectVisible(GraphRect graphRect)

selectedNodesBounds

Get the bounding rectangle of all selected nodes.

GraphRect? get selectedNodesBounds

nodesBounds

Get the bounding rectangle of all nodes.

GraphRect get nodesBounds

Viewport Animations

All animation methods use smooth easing and accept optional duration and curve parameters.

Animation

Viewport Animation Methods

Side-by-side comparison showing animateToNode() (smooth pan+zoom to a specific node), animateToBounds() (fit selection with animation), and animateToScale() (smooth zoom to target level). Each animation uses easeInOut curve.

PROTOTYPE PREVIEW

animateToViewport

Animate to a target viewport state.

void animateToViewport(
  GraphViewport target, {
  Duration duration = const Duration(milliseconds: 400),
  Curve curve = Curves.easeInOut,
})

Example:

controller.animateToViewport(
  GraphViewport(x: 100, y: 50, zoom: 1.5),
  duration: Duration(milliseconds: 300),
);

animateToNode

Animate to center on a specific node.

void animateToNode(
  String nodeId, {
  double? zoom = 1.0,
  Duration duration = const Duration(milliseconds: 400),
  Curve curve = Curves.easeInOut,
})

Example:

// Animate to node with zoom
controller.animateToNode('node-123', zoom: 1.5);

// Animate to node, keeping current zoom
controller.animateToNode('node-123', zoom: null);

animateToPosition

Animate to center on a specific graph position.

void animateToPosition(
  GraphOffset position, {
  double? zoom,
  Duration duration = const Duration(milliseconds: 400),
  Curve curve = Curves.easeInOut,
})

animateToBounds

Animate to fit a bounding rectangle with padding.

void animateToBounds(
  GraphRect bounds, {
  double padding = 50.0,
  Duration duration = const Duration(milliseconds: 400),
  Curve curve = Curves.easeInOut,
})

Example:

// Animate to fit selected nodes
final bounds = controller.selectedNodesBounds;
if (bounds != null) {
  controller.animateToBounds(bounds, padding: 100);
}

animateToScale

Animate to a specific zoom level, keeping the center fixed.

void animateToScale(
  double scale, {
  Duration duration = const Duration(milliseconds: 400),
  Curve curve = Curves.easeInOut,
})

centerOnNodeWithZoom

Immediately center on a node with a specific zoom (non-animated).

void centerOnNodeWithZoom(String nodeId, {double zoom = 1.0})

Mouse Position

mousePositionWorld

Get the current mouse position in graph coordinates.

GraphPosition? get mousePositionWorld

Returns null if the mouse is outside the canvas.

Graph Operations

loadGraph

Load a complete graph (nodes, connections, annotations, viewport).

void loadGraph(NodeGraph<T> graph)

exportGraph

Export current graph state.

NodeGraph<T> exportGraph()

Example:

// Save
final graph = controller.exportGraph();
final json = graph.toJson((data) => data.toJson());
await saveToFile(jsonEncode(json));

// Load
final json = jsonDecode(await loadFromFile());
final graph = NodeGraph.fromJson(json, (map) => MyData.fromJson(map));
controller.loadGraph(graph);

clearGraph

Remove all nodes, connections, and annotations.

void clearGraph()

Alignment & Distribution

alignNodes

Align nodes to a specific edge or center.

void alignNodes(List<String> nodeIds, NodeAlignment alignment)
AlignmentDescription
NodeAlignment.leftAlign to left edge
NodeAlignment.rightAlign to right edge
NodeAlignment.topAlign to top edge
NodeAlignment.bottomAlign to bottom edge
NodeAlignment.horizontalCenterCenter horizontally
NodeAlignment.verticalCenterCenter vertically

distributeNodesHorizontally / distributeNodesVertically

Distribute nodes evenly.

void distributeNodesHorizontally(List<String> nodeIds)
void distributeNodesVertically(List<String> nodeIds)

Annotations

Annotations are accessed via controller.annotations:

addAnnotation / removeAnnotation

controller.annotations.addAnnotation(annotation);
controller.annotations.removeAnnotation(annotationId);

getAnnotation

Annotation? annotation = controller.annotations.getAnnotation(annotationId);

selectAnnotation / clearAnnotationSelection

controller.annotations.selectAnnotation(annotationId, toggle: false);
controller.annotations.clearAnnotationSelection();

Factory Methods

Convenience methods for creating common annotation types (on controller.annotations):

// Create a sticky note
final sticky = controller.annotations.createStickyAnnotation(
  id: 'sticky-1',
  position: Offset(100, 100),
  text: 'Remember this!',
  width: 200.0,
  height: 100.0,
  color: Colors.yellow,
);
controller.annotations.addAnnotation(sticky);

// Create a group annotation with position and size
final group = controller.annotations.createGroupAnnotation(
  id: 'group-1',
  title: 'My Group',
  position: Offset(50, 50),
  size: Size(300, 200),
  color: Colors.blue,
);
controller.annotations.addAnnotation(group);

// Create a group around existing nodes
final groupAroundNodes = controller.annotations.createGroupAnnotationAroundNodes(
  id: 'group-2',
  title: 'Node Group',
  nodeIds: {'node-1', 'node-2'},
  padding: EdgeInsets.all(20.0),
  color: Colors.green,
);
controller.annotations.addAnnotation(groupAroundNodes);

// Create a marker
final marker = controller.annotations.createMarkerAnnotation(
  id: 'marker-1',
  position: Offset(200, 200),
  markerType: MarkerType.info,
  size: 24.0,
  color: Colors.red,
  tooltip: 'Important point',
);
controller.annotations.addAnnotation(marker);

Lifecycle

dispose

Dispose the controller and release resources.

void dispose()

Always call dispose() when the controller is no longer needed to prevent memory leaks.

Complete Example

class WorkflowEditor extends StatefulWidget {
  @override
  State<WorkflowEditor> createState() => _WorkflowEditorState();
}

class _WorkflowEditorState extends State<WorkflowEditor> {
  late final NodeFlowController<WorkflowData> controller;

  @override
  void initState() {
    super.initState();
    controller = NodeFlowController<WorkflowData>();
    _setupGraph();
  }

  void _setupGraph() {
    controller.addNode(Node(
      id: 'start',
      position: Offset(100, 100),
      size: Size(120, 60),
      data: WorkflowData(label: 'Start', type: 'trigger'),
      outputPorts: [Port(id: 'start-out', name: 'Next')],
    ));

    controller.addNode(Node(
      id: 'process',
      position: Offset(300, 100),
      size: Size(120, 60),
      data: WorkflowData(label: 'Process', type: 'action'),
      inputPorts: [Port(id: 'process-in', name: 'Input')],
      outputPorts: [Port(id: 'process-out', name: 'Output')],
    ));

    controller.addConnection(Connection(
      id: 'conn-1',
      sourceNodeId: 'start',
      sourcePortId: 'start-out',
      targetNodeId: 'process',
      targetPortId: 'process-in',
    ));

    WidgetsBinding.instance.addPostFrameCallback((_) {
      controller.fitToView();
    });
  }

  void _addNode() {
    final id = 'node-${DateTime.now().millisecondsSinceEpoch}';
    controller.addNode(Node(
      id: id,
      position: Offset(200, 200),
      size: Size(120, 60),
      data: WorkflowData(label: 'New Node', type: 'action'),
      inputPorts: [Port(id: '$id-in', name: 'Input')],
      outputPorts: [Port(id: '$id-out', name: 'Output')],
    ));
    controller.selectNode(id);
  }

  void _deleteSelected() {
    for (final nodeId in controller.selectedNodeIds.toList()) {
      controller.removeNode(nodeId);
    }
  }

  void _saveGraph() async {
    final graph = controller.exportGraph();
    final json = graph.toJson((data) => data.toJson());
    // Save to file or API
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Workflow Editor'),
        actions: [
          IconButton(icon: Icon(Icons.add), onPressed: _addNode),
          IconButton(icon: Icon(Icons.delete), onPressed: _deleteSelected),
          IconButton(icon: Icon(Icons.fit_screen), onPressed: controller.fitToView),
          IconButton(icon: Icon(Icons.save), onPressed: _saveGraph),
        ],
      ),
      body: NodeFlowEditor<WorkflowData>(
        controller: controller,
        theme: NodeFlowTheme.light,
        nodeBuilder: (context, node) => Center(child: Text(node.data.label)),
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

On this page