Nodes

Understanding nodes in Vyuh Node Flow

Nodes

Nodes are the fundamental building blocks of your flow graph. They represent entities in your visual programming interface, workflow, or diagram.

Node Structure

A Node consists of several key components:

class Node<T extends NodeData> {
  final String id;              // Unique identifier
  final String type;            // Node type for categorization
  final Observable<Offset> position;  // Position on canvas
  final Size size;              // Dimensions
  final T data;                 // Your custom data
  final List<Port> inputPorts;  // Input connection points
  final List<Port> outputPorts; // Output connection points
  final Observable<int> zIndex; // Layer order
}

Creating Nodes

Basic Node

final node = Node<MyNodeData>(
  id: 'node-1',
  type: 'process',
  position: Offset(100, 100),
  size: Size(200, 100),
  data: MyNodeData(title: 'Process Step'),
  inputPorts: [
    Port(
      id: 'input-1',
      name: 'Input',
      position: PortPosition.left,
      type: PortType.target,
    ),
  ],
  outputPorts: [
    Port(
      id: 'output-1',
      name: 'Output',
      position: PortPosition.right,
      type: PortType.source,
    ),
  ],
);

Node with Multiple Ports

final conditionalNode = Node<MyNodeData>(
  id: 'condition-1',
  type: 'condition',
  position: Offset(300, 100),
  size: Size(180, 120),
  data: MyNodeData(title: 'If/Else'),
  inputPorts: [
    Port(
      id: 'cond-input',
      name: 'Input',
      position: PortPosition.left,
      type: PortType.target,
    ),
  ],
  outputPorts: [
    Port(
      id: 'true-output',
      name: 'True',
      position: PortPosition.right,
      type: PortType.source,
      offset: Offset(0, -20),
    ),
    Port(
      id: 'false-output',
      name: 'False',
      position: PortPosition.right,
      type: PortType.source,
      offset: Offset(0, 20),
    ),
  ],
);

Custom Node Data

Your node data must extend NodeData:

class ProcessNodeData extends NodeData {
  final String title;
  final String description;
  final Map<String, dynamic> config;

  ProcessNodeData({
    required this.title,
    this.description = '',
    this.config = const {},
  });

  @override
  Map<String, dynamic> toJson() => {
    'title': title,
    'description': description,
    'config': config,
  };

  @override
  void fromJson(Map<String, dynamic> json) {
    // Reconstruct from JSON if needed
  }
}

Node Types

Use the type field to categorize nodes:

enum NodeType {
  start,
  process,
  condition,
  end,
}

// Create typed nodes
final startNode = Node<MyData>(
  type: NodeType.start.name,
  // ...
);

final processNode = Node<MyData>(
  type: NodeType.process.name,
  // ...
);

Benefits:

  • Different visual styles based on type
  • Type-specific validation rules
  • Easy filtering and querying

Node Positioning

Absolute Positioning

node.position.value = Offset(100, 200);

Relative Positioning

// Move right by 50 pixels
final currentPos = node.position.value;
node.position.value = currentPos + Offset(50, 0);

Center in Viewport

final viewport = controller.viewport;
final centerX = viewport.x + (viewport.width / 2) - (node.size.width / 2);
final centerY = viewport.y + (viewport.height / 2) - (node.size.height / 2);
node.position.value = Offset(centerX, centerY);

Z-Index and Layering

Control which nodes appear on top:

// Bring node to front
node.zIndex.value = controller.maxZIndex + 1;

// Send to back
node.zIndex.value = controller.minZIndex - 1;

Node Widget Rendering

Provide a custom widget builder:

NodeFlowEditor<MyData>(
  controller: controller,
  nodeBuilder: (context, node) {
    switch (node.type) {
      case 'start':
        return StartNodeWidget(node: node);
      case 'process':
        return ProcessNodeWidget(node: node);
      case 'condition':
        return ConditionNodeWidget(node: node);
      default:
        return DefaultNodeWidget(node: node);
    }
  },
)

Example Node Widget

class ProcessNodeWidget extends StatelessWidget {
  final Node<ProcessNodeData> node;

  const ProcessNodeWidget({required this.node});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: node.size.width,
      height: node.size.height,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.blue, width: 2),
        boxShadow: [
          BoxShadow(
            color: Colors.black26,
            blurRadius: 8,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.settings, size: 32, color: Colors.blue),
          SizedBox(height: 8),
          Text(
            node.data.title,
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 14,
            ),
          ),
          if (node.data.description.isNotEmpty)
            Text(
              node.data.description,
              style: TextStyle(
                fontSize: 10,
                color: Colors.grey[600],
              ),
            ),
        ],
      ),
    );
  }
}

Node Selection

Single Selection

controller.selectNode('node-1');

Multi-Selection

controller.selectNode('node-1', multiSelect: true);
controller.selectNode('node-2', multiSelect: true);

Clear Selection

controller.clearSelection();

Get Selected Nodes

final selectedIds = controller.selectedNodeIds;
final selectedNodes = selectedIds
    .map((id) => controller.graph.getNode(id))
    .whereType<Node<MyData>>()
    .toList();

Node Operations

Add Node

controller.addNode(node);

Remove Node

controller.removeNode('node-1');

Update Node

final node = controller.graph.getNode('node-1');
if (node != null) {
  node.position.value = Offset(200, 200);
  // Node data is mutable if needed
}

Find Nodes

// Get all nodes
final allNodes = controller.graph.nodes.values.toList();

// Get nodes by type
final processNodes = allNodes
    .where((n) => n.type == 'process')
    .toList();

// Get nodes in viewport
final viewportNodes = controller.graph.getNodesInRect(
  Rect.fromLTWH(
    viewport.x,
    viewport.y,
    viewport.width,
    viewport.height,
  ),
);

Interactive Nodes

Make nodes respond to interactions:

class InteractiveNodeWidget extends StatelessWidget {
  final Node<MyData> node;
  final VoidCallback onTap;

  const InteractiveNodeWidget({
    required this.node,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        // Node UI
        child: Text(node.data.title),
      ),
    );
  }
}

// Usage in node builder
nodeBuilder: (context, node) {
  return InteractiveNodeWidget(
    node: node,
    onTap: () {
      // Handle tap
      showDialog(
        context: context,
        builder: (_) => NodePropertiesDialog(node: node),
      );
    },
  );
}

Best Practices

  1. Unique IDs: Always use unique, meaningful IDs
  2. Type Naming: Use consistent type naming convention
  3. Data Immutability: Consider making NodeData immutable
  4. Size Consistency: Keep similar node types at similar sizes
  5. Port Placement: Place ports logically for flow direction
  6. Z-Index: Use sparingly, only when needed
  7. Widget Performance: Keep node widgets lightweight

Common Patterns

Factory Pattern for Nodes

class NodeFactory {
  static Node<MyData> createStartNode(Offset position) {
    return Node<MyData>(
      id: 'start-${DateTime.now().millisecondsSinceEpoch}',
      type: 'start',
      position: position,
      size: Size(100, 60),
      data: MyData(title: 'Start'),
      outputPorts: [
        Port(
          id: 'start-out',
          name: 'Output',
          position: PortPosition.right,
          type: PortType.source,
        ),
      ],
    );
  }

  static Node<MyData> createProcessNode(Offset position, String title) {
    return Node<MyData>(
      id: 'process-${DateTime.now().millisecondsSinceEpoch}',
      type: 'process',
      position: position,
      size: Size(150, 80),
      data: MyData(title: title),
      inputPorts: [/* ... */],
      outputPorts: [/* ... */],
    );
  }
}

Next Steps

On this page