Skip to content

Commit 7c12d7f

Browse files
committed
feat: Add startGap and endGap support for connections
- Allow configurable gaps between endpoints and ports. - Update connection caching, rendering logic, and hit testing to include gap values. - Extend theme cascade to prioritize connection-level gaps over theme defaults. - Enhance demo examples with sliders to adjust gap values. - Update README and documentation with gap-related properties.
1 parent 48034eb commit 7c12d7f

File tree

7 files changed

+173
-23
lines changed

7 files changed

+173
-23
lines changed

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

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -580,10 +580,28 @@ class _ThemingExampleState extends State<ThemingExample> {
580580
}).toList(),
581581
),
582582
const SizedBox(height: 12),
583-
_buildSlider('Endpoint Size', _endpointSize.shortestSide, 3.0, 15.0, (
583+
_buildSlider('Endpoint Width', _endpointSize.width, 3.0, 20.0, (value) {
584+
final newSize = Size(value, _endpointSize.height);
585+
setState(() {
586+
_endpointSize = newSize;
587+
});
588+
_updateTheme(
589+
_theme.copyWith(
590+
connectionTheme: _theme.connectionTheme.copyWith(
591+
startPoint: _theme.connectionTheme.startPoint.copyWith(
592+
size: newSize,
593+
),
594+
endPoint: _theme.connectionTheme.endPoint.copyWith(
595+
size: newSize,
596+
),
597+
),
598+
),
599+
);
600+
}),
601+
_buildSlider('Endpoint Height', _endpointSize.height, 3.0, 20.0, (
584602
value,
585603
) {
586-
final newSize = Size.square(value);
604+
final newSize = Size(_endpointSize.width, value);
587605
setState(() {
588606
_endpointSize = newSize;
589607
});
@@ -600,6 +618,25 @@ class _ThemingExampleState extends State<ThemingExample> {
600618
),
601619
);
602620
}),
621+
const SizedBox(height: 12),
622+
_buildSlider('Start Gap', _theme.connectionTheme.startGap, 0.0, 20.0, (
623+
value,
624+
) {
625+
_updateTheme(
626+
_theme.copyWith(
627+
connectionTheme: _theme.connectionTheme.copyWith(startGap: value),
628+
),
629+
);
630+
}),
631+
_buildSlider('End Gap', _theme.connectionTheme.endGap, 0.0, 20.0, (
632+
value,
633+
) {
634+
_updateTheme(
635+
_theme.copyWith(
636+
connectionTheme: _theme.connectionTheme.copyWith(endGap: value),
637+
),
638+
);
639+
}),
603640
],
604641
);
605642
}
@@ -774,6 +811,53 @@ class _ThemingExampleState extends State<ThemingExample> {
774811
);
775812
}).toList(),
776813
),
814+
const SizedBox(height: 16),
815+
const Text('Animation Effect', style: TextStyle(fontSize: 12)),
816+
const SizedBox(height: 8),
817+
Wrap(
818+
spacing: 8,
819+
runSpacing: 8,
820+
children:
821+
[
822+
('None', null),
823+
('Flowing Dash', ConnectionEffects.flowingDash),
824+
('Particles', ConnectionEffects.particles),
825+
('Gradient', ConnectionEffects.gradientFlow),
826+
('Pulse', ConnectionEffects.pulse),
827+
].map((entry) {
828+
final (name, effect) = entry;
829+
return ChoiceChip(
830+
label: Text(name, style: const TextStyle(fontSize: 11)),
831+
selected: tempTheme.animationEffect == effect,
832+
onSelected: (selected) {
833+
if (selected) {
834+
_updateTheme(
835+
_theme.copyWith(
836+
temporaryConnectionTheme: tempTheme.copyWith(
837+
animationEffect: effect,
838+
),
839+
),
840+
);
841+
}
842+
},
843+
);
844+
}).toList(),
845+
),
846+
const SizedBox(height: 12),
847+
_buildSlider('Start Gap', tempTheme.startGap, 0.0, 20.0, (value) {
848+
_updateTheme(
849+
_theme.copyWith(
850+
temporaryConnectionTheme: tempTheme.copyWith(startGap: value),
851+
),
852+
);
853+
}),
854+
_buildSlider('End Gap', tempTheme.endGap, 0.0, 20.0, (value) {
855+
_updateTheme(
856+
_theme.copyWith(
857+
temporaryConnectionTheme: tempTheme.copyWith(endGap: value),
858+
),
859+
);
860+
}),
777861
],
778862
);
779863
}
@@ -923,10 +1007,23 @@ class _ThemingExampleState extends State<ThemingExample> {
9231007
children: [
9241008
const SectionTitle('Ports'),
9251009
const SizedBox(height: 12),
926-
_buildSlider('Size', _theme.portTheme.size.width, 6.0, 16.0, (value) {
1010+
_buildSlider('Width', _theme.portTheme.size.width, 6.0, 20.0, (value) {
1011+
_updateTheme(
1012+
_theme.copyWith(
1013+
portTheme: _theme.portTheme.copyWith(
1014+
size: Size(value, _theme.portTheme.size.height),
1015+
),
1016+
),
1017+
);
1018+
}),
1019+
_buildSlider('Height', _theme.portTheme.size.height, 6.0, 20.0, (
1020+
value,
1021+
) {
9271022
_updateTheme(
9281023
_theme.copyWith(
929-
portTheme: _theme.portTheme.copyWith(size: Size.square(value)),
1024+
portTheme: _theme.portTheme.copyWith(
1025+
size: Size(_theme.portTheme.size.width, value),
1026+
),
9301027
),
9311028
);
9321029
}),

packages/vyuh_node_flow/README.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -445,10 +445,12 @@ ConnectionTheme(
445445
`ConnectionStyles.step`, `ConnectionStyles.straight`
446446

447447
**Model-level overrides**: Each `Connection` can override `style`, `animationEffect`,
448-
`startPoint`, `endPoint`.
448+
`startPoint`, `endPoint`, `startGap`, `endGap`.
449449

450450
**Endpoint color cascade**: `ConnectionEndPoint.color``endpointColor` (theme fallback)
451451

452+
**Gap cascade**: `Connection.startGap``ConnectionTheme.startGap` (theme fallback)
453+
452454
</details>
453455

454456
<details>
@@ -1797,16 +1799,24 @@ final connection = Connection(
17971799
targetNodeId: 'node-2',
17981800
targetPortId: 'input',
17991801
1800-
// Override just for this connection
1802+
// Override endpoint shapes for this connection
18011803
startPoint: ConnectionEndPoint.circle,
18021804
endPoint: ConnectionEndPoint(
18031805
shape: MarkerShapes.diamond,
18041806
size: Size.square(10.0),
18051807
color: Colors.orange,
18061808
),
1809+
1810+
// Override gap values for this connection (falls back to theme if null)
1811+
startGap: 5.0, // 5px gap at source port
1812+
endGap: 3.0, // 3px gap at target port
18071813
);
18081814
```
18091815

1816+
**Gap Cascade**: Connection-level `startGap` and `endGap` take precedence over
1817+
theme values. If not set on the connection, the values from `ConnectionTheme`
1818+
are used.
1819+
18101820
</details>
18111821

18121822
### Connection Labels
@@ -2928,14 +2938,22 @@ class ProcessViewer extends StatelessWidget {
29282938

29292939
### Connection
29302940

2931-
| Property | Type | Description |
2932-
| -------------- | ------- | ----------------- |
2933-
| `id` | String | Unique identifier |
2934-
| `sourceNodeId` | String | Source node ID |
2935-
| `sourcePortId` | String | Source port ID |
2936-
| `targetNodeId` | String | Target node ID |
2937-
| `targetPortId` | String | Target port ID |
2938-
| `label` | String? | Connection label |
2941+
| Property | Type | Description |
2942+
| ----------------- | ------------------- | ------------------------------------------------ |
2943+
| `id` | String | Unique identifier |
2944+
| `sourceNodeId` | String | Source node ID |
2945+
| `sourcePortId` | String | Source port ID |
2946+
| `targetNodeId` | String | Target node ID |
2947+
| `targetPortId` | String | Target port ID |
2948+
| `label` | ConnectionLabel? | Center label (anchor 0.5) |
2949+
| `startLabel` | ConnectionLabel? | Start label (anchor 0.0) |
2950+
| `endLabel` | ConnectionLabel? | End label (anchor 1.0) |
2951+
| `style` | ConnectionStyle? | Override connection style |
2952+
| `animationEffect` | ConnectionEffect? | Override animation effect |
2953+
| `startPoint` | ConnectionEndPoint? | Override start endpoint |
2954+
| `endPoint` | ConnectionEndPoint? | Override end endpoint |
2955+
| `startGap` | double? | Gap at source port (falls back to theme) |
2956+
| `endGap` | double? | Gap at target port (falls back to theme) |
29392957

29402958
---
29412959

packages/vyuh_node_flow/lib/connections/connection.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class Connection {
8181
/// - [endLabel]: Optional label at the end of the connection (anchor 1.0)
8282
/// - [startPoint]: Optional custom start endpoint marker (defaults to theme if null)
8383
/// - [endPoint]: Optional custom end endpoint marker (defaults to theme if null)
84+
/// - [startGap]: Optional gap between source port and start endpoint (defaults to theme if null)
85+
/// - [endGap]: Optional gap between target port and end endpoint (defaults to theme if null)
8486
/// - [animationEffect]: Optional animation effect to apply (overrides animated flag)
8587
/// - [controlPoints]: Optional list of control points for editable path connections
8688
Connection({
@@ -98,6 +100,8 @@ class Connection {
98100
ConnectionLabel? endLabel,
99101
this.startPoint,
100102
this.endPoint,
103+
this.startGap,
104+
this.endGap,
101105
ConnectionEffect? animationEffect,
102106
List<Offset>? controlPoints,
103107
}) : _animated = Observable(animated),
@@ -154,6 +158,16 @@ class Connection {
154158
/// If null, the connection will use the endPoint from [ConnectionTheme].
155159
final ConnectionEndPoint? endPoint;
156160

161+
/// Optional gap between source port and start endpoint in logical pixels.
162+
///
163+
/// If null, the connection will use the startGap from [ConnectionTheme].
164+
final double? startGap;
165+
166+
/// Optional gap between target port and end endpoint in logical pixels.
167+
///
168+
/// If null, the connection will use the endGap from [ConnectionTheme].
169+
final double? endGap;
170+
157171
// Getters and setters for accessing observable values
158172

159173
/// Whether the connection shows flowing animation.

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

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/vyuh_node_flow/lib/connections/connection_painter.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,13 @@ class ConnectionPainter {
169169
sourcePortPosition,
170170
sourcePort.position,
171171
startPointSize,
172-
gap: connectionTheme.startGap,
172+
gap: connection.startGap ?? connectionTheme.startGap,
173173
);
174174
final target = EndpointPositionCalculator.calculatePortConnectionPoints(
175175
targetPortPosition,
176176
targetPort.position,
177177
endPointSize,
178-
gap: connectionTheme.endGap,
178+
gap: connection.endGap ?? connectionTheme.endGap,
179179
);
180180

181181
// Configure paint for the connection line using cached path

packages/vyuh_node_flow/lib/connections/connection_path_cache.dart

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class _CachedConnectionPath {
1616
required this.hitTestPath,
1717
required this.sourcePosition,
1818
required this.targetPosition,
19+
required this.startGap,
20+
required this.endGap,
1921
});
2022

2123
/// The original geometric path for drawing
@@ -27,6 +29,10 @@ class _CachedConnectionPath {
2729
/// Cached node positions for invalidation
2830
final Offset sourcePosition;
2931
final Offset targetPosition;
32+
33+
/// Cached gap values for invalidation
34+
final double startGap;
35+
final double endGap;
3036
}
3137

3238
/// Manages connection path caching and hit testing
@@ -132,12 +138,17 @@ class ConnectionPathCache {
132138
}) {
133139
final currentSourcePos = sourceNode.position.value;
134140
final currentTargetPos = targetNode.position.value;
141+
final connectionTheme = theme.connectionTheme;
142+
final currentStartGap = connection.startGap ?? connectionTheme.startGap;
143+
final currentEndGap = connection.endGap ?? connectionTheme.endGap;
135144

136145
// Check if cache needs updating
137146
final existing = _getCachedPath(connection.id);
138147
if (existing != null &&
139148
existing.sourcePosition == currentSourcePos &&
140-
existing.targetPosition == currentTargetPos) {
149+
existing.targetPosition == currentTargetPos &&
150+
existing.startGap == currentStartGap &&
151+
existing.endGap == currentEndGap) {
141152
return existing.originalPath; // Cache hit
142153
}
143154

@@ -149,6 +160,8 @@ class ConnectionPathCache {
149160
sourcePosition: currentSourcePos,
150161
targetPosition: currentTargetPos,
151162
connectionStyle: connectionStyle,
163+
startGap: currentStartGap,
164+
endGap: currentEndGap,
152165
);
153166

154167
return newPath?.originalPath;
@@ -162,6 +175,8 @@ class ConnectionPathCache {
162175
required Offset sourcePosition,
163176
required Offset targetPosition,
164177
required ConnectionStyle connectionStyle,
178+
required double startGap,
179+
required double endGap,
165180
}) {
166181
// Get connection and port themes
167182
final connectionTheme = theme.connectionTheme;
@@ -225,18 +240,18 @@ class ConnectionPathCache {
225240
? Size.zero
226241
: effectiveEndPoint.size;
227242

228-
// Calculate connection points
243+
// Calculate connection points using passed gap values
229244
final source = EndpointPositionCalculator.calculatePortConnectionPoints(
230245
sourcePortPosition,
231246
sourcePort.position,
232247
startPointSize,
233-
gap: connectionTheme.startGap,
248+
gap: startGap,
234249
);
235250
final target = EndpointPositionCalculator.calculatePortConnectionPoints(
236251
targetPortPosition,
237252
targetPort.position,
238253
endPointSize,
239-
gap: connectionTheme.endGap,
254+
gap: endGap,
240255
);
241256

242257
// Create path parameters for both original and hit test paths
@@ -263,12 +278,14 @@ class ConnectionPathCache {
263278
pathParams: pathParams,
264279
);
265280

266-
// Cache both paths
281+
// Cache both paths with gap values for invalidation
267282
final cachedPath = _CachedConnectionPath(
268283
originalPath: originalPath,
269284
hitTestPath: hitTestPath,
270285
sourcePosition: sourcePosition,
271286
targetPosition: targetPosition,
287+
startGap: startGap,
288+
endGap: endGap,
272289
);
273290

274291
_pathCache[connection.id] = cachedPath;

packages/vyuh_node_flow/lib/graph/layers/connection_labels_layer.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ class _ConnectionLabelWidget<T> extends StatelessWidget {
186186
labelTheme: currentTheme.labelTheme,
187187
pathCache: controller.connectionPainter.pathCache,
188188
portExtension: currentTheme.connectionTheme.portExtension,
189-
startGap: currentTheme.connectionTheme.startGap,
190-
endGap: currentTheme.connectionTheme.endGap,
189+
startGap: connection.startGap ?? currentTheme.connectionTheme.startGap,
190+
endGap: connection.endGap ?? currentTheme.connectionTheme.endGap,
191191
);
192192

193193
if (labelRects.isEmpty) {

0 commit comments

Comments
 (0)