Skip to content

Commit 64a1772

Browse files
committed
feat: enable per-node theming fallback with node.theme ?? nodeTheme in NodesLayer
feat: remove control point APIs and add `HeroShowcaseExample` for visual effects pipeline demo
1 parent 133d463 commit 64a1772

File tree

12 files changed

+1166
-407
lines changed

12 files changed

+1166
-407
lines changed

packages/demo/lib/examples/hero_showcase.dart

Lines changed: 1029 additions & 0 deletions
Large diffs are not rendered by default.

packages/demo/lib/main.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,29 @@ import 'package:go_router/go_router.dart';
44
import 'design_kit/theme.dart';
55
import 'embed_wrapper.dart';
66
import 'example_browser.dart';
7+
import 'example_model.dart';
78
import 'example_not_found.dart';
89
import 'example_registry.dart';
10+
import 'examples/hero_showcase.dart' deferred as hero_showcase;
911

1012
void main() {
1113
runApp(const MyApp());
1214
}
1315

16+
// Hero showcase examples (not in registry, accessed via direct routes)
17+
final _heroExamples = {
18+
'image': Example(
19+
id: 'image',
20+
title: 'Image Effects Pipeline',
21+
description: 'Visual effects pipeline with image, color, and blur nodes',
22+
icon: Icons.auto_awesome,
23+
loader: () async {
24+
await hero_showcase.loadLibrary();
25+
return (_) => hero_showcase.HeroShowcaseExample();
26+
},
27+
),
28+
};
29+
1430
final _router = GoRouter(
1531
initialLocation: '/',
1632
routes: [
@@ -28,6 +44,32 @@ final _router = GoRouter(
2844
return null;
2945
},
3046
),
47+
// Direct route for hero showcase examples
48+
GoRoute(
49+
path: '/hero/:exampleId',
50+
builder: (context, state) {
51+
final exampleId = state.pathParameters['exampleId']!;
52+
final isEmbed = state.uri.queryParameters['embed'] == 'true';
53+
54+
final example = _heroExamples[exampleId];
55+
if (example == null) {
56+
return ExampleNotFound(categoryId: 'hero', exampleId: exampleId);
57+
}
58+
59+
// Hero examples are always shown in embed mode style (clean, no chrome)
60+
if (isEmbed) {
61+
return EmbedWrapper(example: example);
62+
}
63+
64+
// Non-embed: wrap in scaffold with minimal chrome
65+
return Scaffold(
66+
body: EmbedContext(
67+
isEmbed: false,
68+
child: DeferredExampleLoader(example: example),
69+
),
70+
);
71+
},
72+
),
3173
GoRoute(
3274
path: '/:categoryId/:exampleId',
3375
builder: (context, state) {

packages/vyuh_node_flow/lib/src/connections/connection.dart

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ class Connection<C> {
9494
/// - [startGap]: Optional gap between source port and start endpoint (defaults to theme if null)
9595
/// - [endGap]: Optional gap between target port and end endpoint (defaults to theme if null)
9696
/// - [animationEffect]: Optional animation effect to apply (overrides animated flag)
97-
/// - [controlPoints]: Optional list of control points for editable path connections
9897
/// - [locked]: Whether this connection is locked from deletion (default: false)
9998
/// - [color]: Optional custom color for the connection line (overrides theme)
10099
/// - [selectedColor]: Optional custom color when the connection is selected (overrides theme)
@@ -118,7 +117,6 @@ class Connection<C> {
118117
this.startGap,
119118
this.endGap,
120119
ConnectionEffect? animationEffect,
121-
List<Offset>? controlPoints,
122120
this.locked = false,
123121
this.color,
124122
this.selectedColor,
@@ -129,8 +127,7 @@ class Connection<C> {
129127
_startLabel = Observable(startLabel),
130128
_label = Observable(label),
131129
_endLabel = Observable(endLabel),
132-
_animationEffect = Observable(animationEffect),
133-
_controlPoints = ObservableList.of(controlPoints ?? []);
130+
_animationEffect = Observable(animationEffect);
134131

135132
/// Unique identifier for this connection.
136133
final String id;
@@ -153,7 +150,6 @@ class Connection<C> {
153150
final Observable<ConnectionLabel?> _label;
154151
final Observable<ConnectionLabel?> _endLabel;
155152
final Observable<ConnectionEffect?> _animationEffect;
156-
final ObservableList<Offset> _controlPoints;
157153

158154
/// Optional typed data to attach to the connection.
159155
///
@@ -316,28 +312,6 @@ class Connection<C> {
316312
set endLabel(ConnectionLabel? value) =>
317313
runInAction(() => _endLabel.value = value);
318314

319-
/// The list of control points for editable path connections.
320-
///
321-
/// Control points define intermediate waypoints through which the connection
322-
/// path should pass. This is used by editable connection styles to allow
323-
/// users to customize the connection path by adding, moving, or removing
324-
/// control points.
325-
///
326-
/// An empty list indicates the connection uses the default algorithmic path.
327-
@JsonKey(includeFromJson: false, includeToJson: false)
328-
ObservableList<Offset> get controlPoints => _controlPoints;
329-
330-
/// Sets the list of control points for this connection.
331-
///
332-
/// Updating control points will invalidate the cached path and trigger
333-
/// a repaint of the connection with the new path through the waypoints.
334-
set controlPoints(List<Offset> value) {
335-
runInAction(() {
336-
_controlPoints.clear();
337-
_controlPoints.addAll(value);
338-
});
339-
}
340-
341315
/// The list of all non-null labels displayed along the connection path.
342316
///
343317
/// This getter returns a list containing the non-null labels from [startLabel],
@@ -524,17 +498,6 @@ class Connection<C> {
524498
json['endLabel'] as Map<String, dynamic>,
525499
);
526500
}
527-
// Initialize control points from JSON
528-
if (json['controlPoints'] != null) {
529-
final pointsList = json['controlPoints'] as List;
530-
connection._controlPoints.clear();
531-
connection._controlPoints.addAll(
532-
pointsList.map(
533-
(p) =>
534-
Offset((p['dx'] as num).toDouble(), (p['dy'] as num).toDouble()),
535-
),
536-
);
537-
}
538501
return connection;
539502
}
540503

@@ -568,12 +531,6 @@ class Connection<C> {
568531
if (_endLabel.value != null) {
569532
json['endLabel'] = _endLabel.value!.toJson();
570533
}
571-
// Include control points in JSON
572-
if (_controlPoints.isNotEmpty) {
573-
json['controlPoints'] = _controlPoints
574-
.map((offset) => {'dx': offset.dx, 'dy': offset.dy})
575-
.toList();
576-
}
577534
return json;
578535
}
579536
}

packages/vyuh_node_flow/lib/src/connections/connection_path_cache.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,6 @@ class ConnectionPathCache {
324324
cornerRadius: connectionTheme.cornerRadius,
325325
offset: connectionTheme.portExtension,
326326
backEdgeGap: connectionTheme.backEdgeGap,
327-
controlPoints: connection.controlPoints
328-
.toList(), // Convert ObservableList to List
329327
sourceNodeBounds: sourceNodeBounds,
330328
targetNodeBounds: targetNodeBounds,
331329
);

packages/vyuh_node_flow/lib/src/editor/controller/connection_api.dart

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -411,129 +411,6 @@ extension ConnectionApi<T, C> on NodeFlowController<T, C> {
411411
return Rect.fromLTRB(minX, minY, maxX, maxY);
412412
}
413413

414-
// ============================================================================
415-
// Control Point APIs
416-
// ============================================================================
417-
418-
/// Adds a control point to a connection at the specified position.
419-
///
420-
/// Control points are intermediate waypoints that define the path of
421-
/// editable connections. The new control point is inserted at the given
422-
/// index in the control points list.
423-
///
424-
/// Example:
425-
/// ```dart
426-
/// // Add a control point at the end
427-
/// controller.addControlPoint('conn1', Offset(150, 200));
428-
///
429-
/// // Insert a control point at a specific index
430-
/// controller.addControlPoint('conn1', Offset(100, 150), index: 0);
431-
/// ```
432-
void addControlPoint(String connectionId, Offset position, {int? index}) {
433-
final connection = _connections.firstWhere(
434-
(c) => c.id == connectionId,
435-
orElse: () => throw ArgumentError('Connection $connectionId not found'),
436-
);
437-
438-
runInAction(() {
439-
final controlPoints = List<Offset>.from(connection.controlPoints);
440-
441-
if (index != null && index >= 0 && index <= controlPoints.length) {
442-
controlPoints.insert(index, position);
443-
} else {
444-
controlPoints.add(position);
445-
}
446-
447-
connection.controlPoints = controlPoints;
448-
});
449-
450-
// Invalidate cached path and rebuild spatial index
451-
_connectionPainter?.pathCache.removeConnection(connectionId);
452-
_rebuildSingleConnectionSpatialIndex(connection);
453-
}
454-
455-
/// Updates the position of a control point on a connection.
456-
///
457-
/// This method is typically called during drag operations to move an
458-
/// existing control point to a new position.
459-
///
460-
/// Example:
461-
/// ```dart
462-
/// controller.updateControlPoint('conn1', 0, Offset(180, 220));
463-
/// ```
464-
void updateControlPoint(String connectionId, int index, Offset position) {
465-
final connection = _connections.firstWhere(
466-
(c) => c.id == connectionId,
467-
orElse: () => throw ArgumentError('Connection $connectionId not found'),
468-
);
469-
470-
if (index < 0 || index >= connection.controlPoints.length) {
471-
return; // Invalid index
472-
}
473-
474-
runInAction(() {
475-
final controlPoints = List<Offset>.from(connection.controlPoints);
476-
controlPoints[index] = position;
477-
connection.controlPoints = controlPoints;
478-
});
479-
480-
// Invalidate cached path and rebuild spatial index
481-
_connectionPainter?.pathCache.removeConnection(connectionId);
482-
_rebuildSingleConnectionSpatialIndex(connection);
483-
}
484-
485-
/// Removes a control point from a connection.
486-
///
487-
/// Deletes the control point at the specified index.
488-
///
489-
/// Example:
490-
/// ```dart
491-
/// controller.removeControlPoint('conn1', 1);
492-
/// ```
493-
void removeControlPoint(String connectionId, int index) {
494-
final connection = _connections.firstWhere(
495-
(c) => c.id == connectionId,
496-
orElse: () => throw ArgumentError('Connection $connectionId not found'),
497-
);
498-
499-
if (index < 0 || index >= connection.controlPoints.length) {
500-
return; // Invalid index
501-
}
502-
503-
runInAction(() {
504-
final controlPoints = List<Offset>.from(connection.controlPoints);
505-
controlPoints.removeAt(index);
506-
connection.controlPoints = controlPoints;
507-
});
508-
509-
// Invalidate cached path and rebuild spatial index
510-
_connectionPainter?.pathCache.removeConnection(connectionId);
511-
_rebuildSingleConnectionSpatialIndex(connection);
512-
}
513-
514-
/// Clears all control points from a connection.
515-
///
516-
/// This reverts the connection to using its default algorithmic path.
517-
///
518-
/// Example:
519-
/// ```dart
520-
/// controller.clearControlPoints('conn1');
521-
/// ```
522-
void clearControlPoints(String connectionId) {
523-
final connection = _connections.firstWhere(
524-
(c) => c.id == connectionId,
525-
orElse: () => throw ArgumentError('Connection $connectionId not found'),
526-
);
527-
528-
runInAction(() {
529-
connection.controlPoints = [];
530-
});
531-
532-
// Invalidate cached path and rebuild spatial index
533-
_connectionPainter?.pathCache.removeConnection(connectionId);
534-
_rebuildSingleConnectionSpatialIndex(connection);
535-
}
536-
537414
// ============================================================================
538415
// Selection APIs
539416
// ============================================================================

packages/vyuh_node_flow/lib/src/editor/layers/connections_layer.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@ class ConnectionsLayer<T, C> extends StatelessWidget {
7878
}
7979

8080
connection.animationEffect;
81-
for (var i = 0; i < connection.controlPoints.length; i++) {
82-
connection.controlPoints[i];
83-
}
8481
}
8582

8683
return CustomPaint(
@@ -129,9 +126,6 @@ class ConnectionsLayer<T, C> extends StatelessWidget {
129126
}
130127

131128
connection.animationEffect;
132-
for (var i = 0; i < connection.controlPoints.length; i++) {
133-
connection.controlPoints[i];
134-
}
135129
}
136130

137131
return CustomPaint(

packages/vyuh_node_flow/lib/src/editor/layers/nodes_layer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ class NodesLayer<T> extends StatelessWidget {
274274
portSnapDistance: portSnapDistance,
275275
child: NodeWidget<T>(
276276
node: node,
277-
theme: nodeTheme,
277+
theme: node.theme ?? nodeTheme,
278278
shape: shape,
279279
showContent: showNodeContent,
280280
child: content,

packages/vyuh_node_flow/lib/src/nodes/node.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:mobx/mobx.dart';
33

44
import '../ports/port.dart';
55
import 'node_drag_context.dart';
6+
import 'node_theme.dart';
67

78
export 'node_drag_context.dart';
89
export 'node_port_geometry.dart';
@@ -130,6 +131,7 @@ class Node<T> {
130131
this.locked = false,
131132
this.selectable = true,
132133
this.widgetBuilder,
134+
this.theme,
133135
}) : size = Observable(size ?? const Size(150, 100)),
134136
position = Observable(position),
135137
visualPosition = Observable(position),
@@ -210,6 +212,26 @@ class Node<T> {
210212
/// 4. Default content - framework default
211213
final NodeWidgetBuilder<T>? widgetBuilder;
212214

215+
/// Optional theme override for this node.
216+
///
217+
/// When provided, this theme overrides the global [NodeTheme] from
218+
/// [NodeFlowTheme.nodeTheme]. Use this to customize individual node
219+
/// appearance without affecting other nodes.
220+
///
221+
/// Example:
222+
/// ```dart
223+
/// Node<MyData>(
224+
/// id: 'custom',
225+
/// type: 'custom',
226+
/// position: Offset.zero,
227+
/// data: MyData(),
228+
/// theme: NodeTheme.light.copyWith(
229+
/// backgroundColor: Colors.blue.shade50,
230+
/// ),
231+
/// )
232+
/// ```
233+
final NodeTheme? theme;
234+
213235
// ===========================================================================
214236
// Capability Indicators
215237
// ===========================================================================

0 commit comments

Comments
 (0)