Skip to content

Commit c5ea7d9

Browse files
committed
feat: consolidating the step and smooth step connection styles
1 parent 0dc0a8a commit c5ea7d9

21 files changed

+1473
-250
lines changed

packages/demo/lib/example_registry.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import 'examples/advanced/animated_connections.dart';
99
import 'examples/advanced/annotations.dart';
1010
// Connection labels examples
1111
import 'examples/advanced/connection_labels.dart';
12+
// // Editable connections examples
13+
// import 'examples/advanced/editable_connections.dart';
1214
// Advanced examples
1315
import 'examples/advanced/serialization.dart';
1416
import 'examples/advanced/shortcuts.dart';
@@ -157,6 +159,14 @@ class ExampleRegistry {
157159
icon: Icons.label,
158160
builder: (_) => const ConnectionLabelsExample(),
159161
),
162+
// Example(
163+
// id: 'editable-connections',
164+
// title: 'Editable Connections',
165+
// description:
166+
// 'Interactive control points for customizing connection paths with drag-and-drop waypoint editing',
167+
// icon: Icons.edit_road,
168+
// builder: (_) => const EditableConnectionsExample(),
169+
// ),
160170
Example(
161171
id: 'theming',
162172
title: 'Theme Customization',
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:vyuh_node_flow/vyuh_node_flow.dart';
5+
6+
import '../../shared/ui_widgets.dart';
7+
8+
class EditableConnectionsExample extends StatefulWidget {
9+
const EditableConnectionsExample({super.key});
10+
11+
@override
12+
State<EditableConnectionsExample> createState() =>
13+
_EditableConnectionsExampleState();
14+
}
15+
16+
class _EditableConnectionsExampleState
17+
extends State<EditableConnectionsExample> {
18+
late final NodeFlowController<Map<String, dynamic>> _controller;
19+
late NodeFlowTheme _theme;
20+
21+
@override
22+
void initState() {
23+
super.initState();
24+
_theme = _createTheme();
25+
_controller = NodeFlowController<Map<String, dynamic>>(
26+
config: NodeFlowConfig(),
27+
);
28+
29+
// Add initial nodes and connections after first frame
30+
WidgetsBinding.instance.addPostFrameCallback((_) {
31+
_addInitialNodesAndConnections();
32+
});
33+
}
34+
35+
NodeFlowTheme _createTheme() {
36+
return NodeFlowTheme.light.copyWith(
37+
connectionTheme: NodeFlowTheme.light.connectionTheme.copyWith(
38+
style: EditableSmoothStepConnectionStyle(),
39+
selectedColor: Colors.deepPurple,
40+
selectedStrokeWidth: 3.0,
41+
cornerRadius: 12.0,
42+
),
43+
);
44+
}
45+
46+
void _addInitialNodesAndConnections() {
47+
// Create nodes in a grid layout
48+
final nodes = [
49+
Node<Map<String, dynamic>>(
50+
id: 'node-1',
51+
type: 'input',
52+
position: const Offset(100, 100),
53+
data: {'label': 'Input A'},
54+
size: const Size(150, 100),
55+
outputPorts: const [
56+
Port(
57+
id: 'output',
58+
name: 'Out',
59+
position: PortPosition.right,
60+
offset: Offset(0, 50),
61+
),
62+
],
63+
),
64+
Node<Map<String, dynamic>>(
65+
id: 'node-2',
66+
type: 'input',
67+
position: const Offset(100, 250),
68+
data: {'label': 'Input B'},
69+
size: const Size(150, 100),
70+
outputPorts: const [
71+
Port(
72+
id: 'output',
73+
name: 'Out',
74+
position: PortPosition.right,
75+
offset: Offset(0, 50),
76+
),
77+
],
78+
),
79+
Node<Map<String, dynamic>>(
80+
id: 'node-3',
81+
type: 'process',
82+
position: const Offset(400, 150),
83+
data: {'label': 'Process'},
84+
size: const Size(150, 120),
85+
inputPorts: const [
86+
Port(
87+
id: 'input1',
88+
name: 'In 1',
89+
position: PortPosition.left,
90+
offset: Offset(0, 40),
91+
),
92+
Port(
93+
id: 'input2',
94+
name: 'In 2',
95+
position: PortPosition.left,
96+
offset: Offset(0, 80),
97+
),
98+
],
99+
outputPorts: const [
100+
Port(
101+
id: 'output',
102+
name: 'Out',
103+
position: PortPosition.right,
104+
offset: Offset(0, 60),
105+
),
106+
],
107+
),
108+
Node<Map<String, dynamic>>(
109+
id: 'node-4',
110+
type: 'output',
111+
position: const Offset(700, 175),
112+
data: {'label': 'Output'},
113+
size: const Size(150, 100),
114+
inputPorts: const [
115+
Port(
116+
id: 'input',
117+
name: 'In',
118+
position: PortPosition.left,
119+
offset: Offset(0, 50),
120+
),
121+
],
122+
),
123+
];
124+
125+
for (final node in nodes) {
126+
_controller.addNode(node);
127+
}
128+
129+
// Create connections with editable control points
130+
final connections = [
131+
// Simple connection without control points (uses default algorithm)
132+
Connection(
133+
id: 'conn-1',
134+
sourceNodeId: 'node-1',
135+
sourcePortId: 'output',
136+
targetNodeId: 'node-3',
137+
targetPortId: 'input1',
138+
),
139+
// Connection with control points (manually edited path)
140+
Connection(
141+
id: 'conn-2',
142+
sourceNodeId: 'node-2',
143+
sourcePortId: 'output',
144+
targetNodeId: 'node-3',
145+
targetPortId: 'input2',
146+
controlPoints: [const Offset(320, 300), const Offset(320, 230)],
147+
),
148+
// Another connection without control points
149+
Connection(
150+
id: 'conn-3',
151+
sourceNodeId: 'node-3',
152+
sourcePortId: 'output',
153+
targetNodeId: 'node-4',
154+
targetPortId: 'input',
155+
),
156+
];
157+
158+
for (final connection in connections) {
159+
_controller.addConnection(connection);
160+
}
161+
162+
// Fit view to show all nodes
163+
WidgetsBinding.instance.addPostFrameCallback((_) {
164+
_controller.fitToView();
165+
});
166+
}
167+
168+
@override
169+
void dispose() {
170+
_controller.dispose();
171+
super.dispose();
172+
}
173+
174+
void _addControlPoint() {
175+
// Add a control point to the middle of the first connection
176+
_controller.addControlPoint('conn-1', const Offset(300, 100));
177+
}
178+
179+
void _removeControlPoints() {
180+
// Clear all control points from the second connection
181+
_controller.clearControlPoints('conn-2');
182+
}
183+
184+
void _toggleEditableStyle() {
185+
setState(() {
186+
final currentStyle = _theme.connectionTheme.style;
187+
final isEditable = currentStyle is EditablePathConnectionStyle;
188+
189+
_theme = _theme.copyWith(
190+
connectionTheme: _theme.connectionTheme.copyWith(
191+
style: isEditable
192+
? ConnectionStyles.smoothstep
193+
: const EditableSmoothStepConnectionStyle(),
194+
),
195+
);
196+
});
197+
}
198+
199+
Widget _buildNode(BuildContext context, Node<Map<String, dynamic>> node) {
200+
final outerBorderRadius = _theme.nodeTheme.borderRadius;
201+
final borderWidth = _theme.nodeTheme.borderWidth;
202+
final outerRadius = outerBorderRadius.topLeft.x;
203+
final innerRadius = math.max(0.0, outerRadius - borderWidth);
204+
205+
final theme = Theme.of(context);
206+
207+
// Different colors for different node types
208+
final Color nodeColor;
209+
final Color iconColor;
210+
211+
switch (node.type) {
212+
case 'input':
213+
nodeColor = Colors.green.shade100;
214+
iconColor = Colors.green.shade700;
215+
break;
216+
case 'process':
217+
nodeColor = Colors.blue.shade100;
218+
iconColor = Colors.blue.shade700;
219+
break;
220+
case 'output':
221+
nodeColor = Colors.orange.shade100;
222+
iconColor = Colors.orange.shade700;
223+
break;
224+
default:
225+
nodeColor = Colors.grey.shade100;
226+
iconColor = Colors.grey.shade700;
227+
}
228+
229+
return Container(
230+
padding: const EdgeInsets.all(16),
231+
decoration: BoxDecoration(
232+
color: nodeColor,
233+
borderRadius: BorderRadius.circular(innerRadius),
234+
),
235+
child: Center(
236+
child: Text(
237+
node.data['label'] ?? '',
238+
style: theme.textTheme.titleSmall?.copyWith(
239+
fontWeight: FontWeight.bold,
240+
color: iconColor,
241+
),
242+
textAlign: TextAlign.center,
243+
),
244+
),
245+
);
246+
}
247+
248+
@override
249+
Widget build(BuildContext context) {
250+
final isEditable =
251+
_theme.connectionTheme.style is EditablePathConnectionStyle;
252+
253+
return ResponsiveControlPanel(
254+
title: 'Controls',
255+
width: 340,
256+
child: NodeFlowEditor<Map<String, dynamic>>(
257+
controller: _controller,
258+
nodeBuilder: _buildNode,
259+
theme: _theme,
260+
),
261+
children: [
262+
const SectionTitle('Editable Connections'),
263+
const SizedBox(height: 12),
264+
ControlButton(
265+
label: isEditable ? 'Disable Editing' : 'Enable Editing',
266+
icon: isEditable ? Icons.lock : Icons.edit,
267+
onPressed: _toggleEditableStyle,
268+
),
269+
const SizedBox(height: 8),
270+
ControlButton(
271+
label: 'Add Control Point',
272+
icon: Icons.add_location,
273+
onPressed: isEditable ? _addControlPoint : null,
274+
),
275+
const SizedBox(height: 8),
276+
ControlButton(
277+
label: 'Clear Control Points',
278+
icon: Icons.clear_all,
279+
onPressed: isEditable ? _removeControlPoints : null,
280+
),
281+
const SizedBox(height: 16),
282+
const InfoCard(
283+
title: 'Instructions',
284+
content:
285+
'1. Enable editing mode to see control points\n'
286+
'2. Drag control points to modify connection paths\n'
287+
'3. Control points define waypoints for smooth step routing\n'
288+
'4. Connections without control points use automatic routing',
289+
),
290+
const SizedBox(height: 16),
291+
const InfoCard(
292+
title: 'Features',
293+
content:
294+
'• Drag control points to customize paths\n'
295+
'• Add/remove control points programmatically\n'
296+
'• Maintains orthogonal (90°) routing\n'
297+
'• Smooth rounded corners at bends',
298+
),
299+
],
300+
);
301+
}
302+
}

packages/demo/lib/examples/advanced/serialization.dart

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -483,44 +483,28 @@ class _SerializationExampleState extends State<SerializationExample> {
483483
children: [
484484
const SectionTitle('Actions'),
485485
const SizedBox(height: 12),
486-
Row(
487-
children: [
488-
Expanded(
489-
child: ElevatedButton.icon(
490-
icon: const Icon(Icons.upload, size: 16),
491-
label: const Text('Export', style: TextStyle(fontSize: 11)),
492-
onPressed: _exportGraph,
493-
),
494-
),
495-
const SizedBox(width: 8),
496-
Expanded(
497-
child: ElevatedButton.icon(
498-
icon: const Icon(Icons.download, size: 16),
499-
label: const Text('Import', style: TextStyle(fontSize: 11)),
500-
onPressed: _importGraph,
501-
),
502-
),
503-
],
486+
ControlButton(
487+
icon: Icons.upload,
488+
label: 'Export',
489+
onPressed: _exportGraph,
504490
),
505491
const SizedBox(height: 8),
506-
Row(
507-
children: [
508-
Expanded(
509-
child: ElevatedButton.icon(
510-
icon: const Icon(Icons.clear, size: 16),
511-
label: const Text('Clear', style: TextStyle(fontSize: 11)),
512-
onPressed: _clearGraph,
513-
),
514-
),
515-
const SizedBox(width: 8),
516-
Expanded(
517-
child: ElevatedButton.icon(
518-
icon: const Icon(Icons.refresh, size: 16),
519-
label: const Text('Reset', style: TextStyle(fontSize: 11)),
520-
onPressed: _resetToInitial,
521-
),
522-
),
523-
],
492+
ControlButton(
493+
icon: Icons.download,
494+
label: 'Import',
495+
onPressed: _importGraph,
496+
),
497+
const SizedBox(height: 8),
498+
ControlButton(
499+
icon: Icons.clear,
500+
label: 'Clear',
501+
onPressed: _clearGraph,
502+
),
503+
const SizedBox(height: 8),
504+
ControlButton(
505+
icon: Icons.refresh,
506+
label: 'Reset',
507+
onPressed: _resetToInitial,
524508
),
525509
],
526510
);

0 commit comments

Comments
 (0)