fix(editor): port docks elegantly — line terminates at perimeter
Stefan's "the connections to the round ports look unelegant" feedback was about three concrete things: (1) The bezier endpoint sat at the port dot's GEOMETRIC CENTRE. The dot was rendered ON TOP, so the line visually disappeared into the centre of the dot — looked like a thread being eaten by a bead. (2) A separate arrow-head triangle was drawn on top of the dot, doubling the visual terminator and making the endpoint look "noisy". (3) The dots were hollow rings the same colour as the line, so the boundary between line and dot blurred visually. They didn't read as connectors. Fix: - EdgeSegment now carries `fromSide` + `toSide` (left / right) so the painter knows which way to shorten each endpoint. The bezier ends `portRadius` px short of the centre — exactly on the dot's outer perimeter, on the side facing the line. The line now "docks" cleanly at the rim. - The end-cap triangle arrow is gone. The dot itself is the visual terminator; an arrow on top was redundant. - Port dot redesigned as a connector-socket: outer ring defines the footprint, inner 4-px surface-coloured pin appears when connected. The result reads as "an active socket with a plug seated in it" rather than "a hollow circle next to a card edge". Empty ports stay surface- filled rings — unambiguous empty-slot signal. - Bezier handle length floor raised from 40 to 60 px so vertical detours (e.g. inputs-endpoint → step far below) curve gracefully out of the start before dropping instead of kinking near the origin. - Stroke width nudged from 1.8 to 2.0 px for the normal accent so the line carries the right visual weight against the new docked-at-perimeter port treatment. Visual: lines now look like cables seated into sockets, not strings vanishing under beads. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
e4e2d45374
commit
9afb51a4b5
3 changed files with 115 additions and 47 deletions
|
|
@ -148,6 +148,7 @@ class _FlowCanvasState extends State<FlowCanvas> {
|
|||
.withValues(alpha: 0.55),
|
||||
highlightColor: theme.colorScheme.primary,
|
||||
draftColor: theme.colorScheme.primary,
|
||||
portRadius: NodeGeometry.portDotSize / 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -187,16 +188,25 @@ class _FlowCanvasState extends State<FlowCanvas> {
|
|||
child: IgnorePointer(
|
||||
child: CustomPaint(
|
||||
painter: EdgePainter(
|
||||
// Draft line follows the cursor;
|
||||
// pretend the cursor is on the
|
||||
// LEFT side so the line "approaches"
|
||||
// it horizontally (matches the
|
||||
// input-port orientation it will
|
||||
// most likely snap to).
|
||||
segments: [
|
||||
EdgeSegment(
|
||||
from: _draft!.from,
|
||||
to: _draft!.cursor,
|
||||
fromSide: EdgeSide.right,
|
||||
toSide: EdgeSide.left,
|
||||
accent: EdgeAccent.draftDrag,
|
||||
),
|
||||
],
|
||||
baseColor: theme.colorScheme.primary,
|
||||
highlightColor: theme.colorScheme.primary,
|
||||
draftColor: theme.colorScheme.primary,
|
||||
portRadius: NodeGeometry.portDotSize / 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -445,12 +455,22 @@ class _FlowCanvasState extends State<FlowCanvas> {
|
|||
for (final edge in graph.edges) {
|
||||
Offset? from;
|
||||
Offset? to;
|
||||
EdgeSide? fromSide;
|
||||
EdgeSide? toSide;
|
||||
|
||||
if (edge.fromKind == EdgeEndpointKind.inputs) {
|
||||
final idx = inputsList.indexOf(edge.fromField);
|
||||
if (idx >= 0) from = _inputsEndpointPortPosition(idx, layout);
|
||||
if (idx >= 0) {
|
||||
from = _inputsEndpointPortPosition(idx, layout);
|
||||
// Inputs endpoint ports live on the node's right edge
|
||||
// — that's where the dot sits, and where the bezier
|
||||
// should originate.
|
||||
fromSide = EdgeSide.right;
|
||||
}
|
||||
} else if (edge.fromKind == EdgeEndpointKind.step) {
|
||||
from = _outputPortPosition(edge.fromId, layout);
|
||||
// Step output is on the right edge.
|
||||
fromSide = EdgeSide.right;
|
||||
}
|
||||
if (edge.toKind == EdgeEndpointKind.step) {
|
||||
final step = graph.steps.firstWhere(
|
||||
|
|
@ -460,15 +480,19 @@ class _FlowCanvasState extends State<FlowCanvas> {
|
|||
final idx = step.with_.keys.toList().indexOf(edge.toField);
|
||||
if (idx >= 0) {
|
||||
to = _inputPortPosition(edge.toId, idx, layout);
|
||||
toSide = EdgeSide.left;
|
||||
}
|
||||
} else if (edge.toKind == EdgeEndpointKind.outputs) {
|
||||
final outputsList = graph.outputs.keys.toList();
|
||||
final idx = outputsList.indexOf(edge.toField);
|
||||
if (idx >= 0) {
|
||||
to = _inputPortPosition(AutoLayout.outputsNodeId, idx, layout);
|
||||
toSide = EdgeSide.left;
|
||||
}
|
||||
}
|
||||
if (from == null || to == null) continue;
|
||||
if (from == null || to == null || fromSide == null || toSide == null) {
|
||||
continue;
|
||||
}
|
||||
final highlight =
|
||||
edge.fromId == widget.controller.selectedStepId ||
|
||||
edge.toId == widget.controller.selectedStepId;
|
||||
|
|
@ -476,6 +500,8 @@ class _FlowCanvasState extends State<FlowCanvas> {
|
|||
EdgeSegment(
|
||||
from: from,
|
||||
to: to,
|
||||
fromSide: fromSide,
|
||||
toSide: toSide,
|
||||
accent: highlight ? EdgeAccent.highlight : EdgeAccent.normal,
|
||||
),
|
||||
);
|
||||
|
|
@ -745,20 +771,48 @@ class _FlowCanvasState extends State<FlowCanvas> {
|
|||
onSecondaryTapDown: onContextMenu == null
|
||||
? null
|
||||
: (details) => onContextMenu(details.globalPosition),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: filled ? accent : theme.colorScheme.surface,
|
||||
border: Border.all(color: accent, width: isClosest ? 2.5 : 1.8),
|
||||
boxShadow: isClosest
|
||||
? [
|
||||
BoxShadow(
|
||||
color: accent.withValues(alpha: 0.5),
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Outer ring — defines the port's footprint.
|
||||
// Filled when connected (so the line "docks"
|
||||
// into a visible target), surface-filled when
|
||||
// dangling (a clear empty socket). Drop-target
|
||||
// halo grows + glows.
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: filled ? accent : theme.colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: accent,
|
||||
width: isClosest ? 2.5 : 1.8,
|
||||
),
|
||||
boxShadow: isClosest
|
||||
? [
|
||||
BoxShadow(
|
||||
color: accent.withValues(alpha: 0.5),
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
// Inner pin — tiny surface-coloured dot at the
|
||||
// centre when connected, giving the port the
|
||||
// "socket with a pin in it" look that reads as
|
||||
// an active electrical connector rather than a
|
||||
// free-floating indicator. Skipped on empty
|
||||
// ports so the hollow ring is unambiguous.
|
||||
if (connected)
|
||||
Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue