Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ build/
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
AGENTS.md
17 changes: 14 additions & 3 deletions lib/Graph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,31 @@ class Graph {
void addEdgeS(Edge edge) {
var sourceSet = false;
var destinationSet = false;
_nodes.forEach((node) {
for (var node in _nodes) {
if (!sourceSet && node == edge.source) {
edge.source = node;
sourceSet = true;
} else if (!destinationSet && node == edge.destination) {
}

if (!destinationSet && node == edge.destination) {
edge.destination = node;
destinationSet = true;
}
});

if (sourceSet && destinationSet) {
break;
}
}
if (!sourceSet) {
_nodes.add(edge.source);
sourceSet = true;
if (!destinationSet && edge.destination == edge.source) {
destinationSet = true;
}
}
if (!destinationSet) {
_nodes.add(edge.destination);
destinationSet = true;
}

if (!_edges.contains(edge)) {
Expand Down
44 changes: 40 additions & 4 deletions lib/edgerenderer/ArrowEdgeRenderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,44 @@ class ArrowEdgeRenderer extends EdgeRenderer {
var source = edge.source;
var destination = edge.destination;

final currentPaint = (edge.paint ?? paint)..style = PaintingStyle.stroke;
final lineType = _getLineType(destination);

if (source == destination) {
final loopResult = buildSelfLoopPath(
edge,
arrowLength: noArrow ? 0.0 : ARROW_LENGTH,
);

if (loopResult != null) {
drawStyledPath(canvas, loopResult.path, currentPaint, lineType: lineType);

if (!noArrow) {
final trianglePaint = Paint()
..color = edge.paint?.color ?? paint.color
..style = PaintingStyle.fill;
final triangleCentroid = drawTriangle(
canvas,
trianglePaint,
loopResult.arrowBase.dx,
loopResult.arrowBase.dy,
loopResult.arrowTip.dx,
loopResult.arrowTip.dy,
);

drawStyledLine(
canvas,
loopResult.arrowBase,
triangleCentroid,
currentPaint,
lineType: lineType,
);
}

return;
}
}

var sourceOffset = getNodePosition(source);
var destinationOffset = getNodePosition(destination);

Expand All @@ -46,16 +84,14 @@ class ArrowEdgeRenderer extends EdgeRenderer {
destination.width,
destination.height);

final currentPaint = edge.paint ?? paint;

if (noArrow) {
// Draw line without arrow, respecting line type
drawStyledLine(
canvas,
Offset(clippedLine[0], clippedLine[1]),
Offset(clippedLine[2], clippedLine[3]),
currentPaint,
lineType: _getLineType(destination),
lineType: lineType,
);
} else {
var trianglePaint = Paint()
Expand Down Expand Up @@ -84,7 +120,7 @@ class ArrowEdgeRenderer extends EdgeRenderer {
Offset(clippedLine[0], clippedLine[1]),
triangleCentroid,
currentPaint,
lineType: _getLineType(destination),
lineType: lineType,
);
}
}
Expand Down
71 changes: 70 additions & 1 deletion lib/edgerenderer/EdgeRenderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,73 @@ abstract class EdgeRenderer {
canvas.drawPath(path, paint);
paint.strokeWidth = originalStrokeWidth;
}
}

/// Builds a loop path for self-referential edges and returns geometry
/// data that renderers can use to draw arrows or style the segment.
LoopRenderResult? buildSelfLoopPath(
Edge edge, {
double loopPadding = 16.0,
double arrowLength = 12.0,
}) {
if (edge.source != edge.destination) {
return null;
}

final node = edge.source;
final nodeCenter = getNodeCenter(node);

final anchorRadius = node.size.shortestSide * 0.5;

final start = nodeCenter + Offset(anchorRadius, 0);

final end = nodeCenter + Offset(0, -anchorRadius);

final loopRadius = max(
loopPadding + anchorRadius,
anchorRadius * 1.5,
);

final controlPoint1 = start + Offset(loopRadius, 0);

final controlPoint2 = end + Offset(0, -loopRadius);

final path = Path()
..moveTo(start.dx, start.dy)
..cubicTo(
controlPoint1.dx,
controlPoint1.dy,
controlPoint2.dx,
controlPoint2.dy,
end.dx,
end.dy,
);

final metrics = path.computeMetrics().toList();
if (metrics.isEmpty) {
return LoopRenderResult(path, start, end);
}

final metric = metrics.first;
final totalLength = metric.length;
final effectiveArrowLength = arrowLength <= 0
? 0.0
: min(arrowLength, totalLength * 0.3);
final arrowBaseOffset = max(0.0, totalLength - effectiveArrowLength);
final arrowBaseTangent = metric.getTangentForOffset(arrowBaseOffset);
final arrowTipTangent = metric.getTangentForOffset(totalLength);

return LoopRenderResult(
path,
arrowBaseTangent?.position ?? end,
arrowTipTangent?.position ?? end,
);
}
}

class LoopRenderResult {
final Path path;
final Offset arrowBase;
final Offset arrowTip;

const LoopRenderResult(this.path, this.arrowBase, this.arrowTip);
}
49 changes: 36 additions & 13 deletions lib/layered/SugiyamaAlgorithm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ class SugiyamaAlgorithm extends Algorithm {
}
visited.add(node);
stack.add(node);
graph.getOutEdges(node).forEach((edge) {
graph.getOutEdges(node).toList().forEach((edge) {
final target = edge.destination;
if (stack.contains(target)) {
final storedData = edgeData.remove(edge);
graph.removeEdge(edge);
graph.addEdge(target, node);
final reversedEdge = graph.addEdge(target, node);
edgeData[reversedEdge] = storedData ?? SugiyamaEdgeData();
nodeData[node]!.reversed.add(target);
} else {
dfs(target);
Expand Down Expand Up @@ -772,6 +774,12 @@ class SugiyamaAlgorithm extends Algorithm {
break;
}

if (coordinates.isEmpty) {
for (final node in graph.nodes) {
coordinates[node] = 0.0;
}
}

// Get the minimum coordinate value
var minValue = coordinates.values.reduce(min);

Expand All @@ -791,6 +799,10 @@ class SugiyamaAlgorithm extends Algorithm {

void resolveOverlaps(Map<Node, double> coordinates) {
for (var layer in layers) {
if (layer.isEmpty) {
continue;
}

var layerNodes = List<Node>.from(layer);
layerNodes.sort(
(a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position));
Expand Down Expand Up @@ -1181,18 +1193,27 @@ class SugiyamaAlgorithm extends Algorithm {

void restoreCycle() {
graph.nodes.forEach((n) {
if (nodeData[n]!.isReversed) {
nodeData[n]!.reversed.forEach((target) {
final bendPoints =
this.edgeData[graph.getEdgeBetween(target, n)!]!.bendPoints;
graph.removeEdgeFromPredecessor(target, n);
final edge = graph.addEdge(n, target);
final nodeInfo = nodeData[n];
if (nodeInfo == null || !nodeInfo.isReversed) {
return;
}

final edgeData = SugiyamaEdgeData();
edgeData.bendPoints = bendPoints;
this.edgeData[edge] = edgeData;
});
for (final target in nodeInfo.reversed.toList()) {
final existingEdge = graph.getEdgeBetween(target, n);
if (existingEdge == null) {
continue;
}
final existingData = this.edgeData.remove(existingEdge);
final bendPoints = existingData?.bendPoints ?? <double>[];
graph.removeEdgeFromPredecessor(target, n);
final edge = graph.addEdge(n, target);

final restoredData = existingData ?? SugiyamaEdgeData();
restoredData.bendPoints = bendPoints;
this.edgeData[edge] = restoredData;
}

nodeInfo.reversed.clear();
});
}

Expand Down Expand Up @@ -1220,8 +1241,10 @@ class SugiyamaAlgorithm extends Algorithm {
for (var edge in feedbackArcs) {
var source = edge.source;
var target = edge.destination;
final storedData = edgeData.remove(edge);
graph.removeEdge(edge);
graph.addEdge(target, source);
final reversedEdge = graph.addEdge(target, source);
edgeData[reversedEdge] = storedData ?? SugiyamaEdgeData();
nodeData[source]!.reversed.add(target);
}
}
Expand Down
37 changes: 35 additions & 2 deletions lib/layered/SugiyamaEdgeRenderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,42 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer {
..style = PaintingStyle.fill;
}

var currentPaint = edge.paint ?? paint
var currentPaint = (edge.paint ?? paint)
..style = PaintingStyle.stroke;

if (edge.source == edge.destination) {
final loopResult = buildSelfLoopPath(
edge,
arrowLength: addTriangleToEdge ? ARROW_LENGTH : 0.0,
);

if (loopResult != null) {
final lineType = nodeData[edge.destination]?.lineType;
drawStyledPath(canvas, loopResult.path, currentPaint, lineType: lineType);

if (addTriangleToEdge) {
final triangleCentroid = drawTriangle(
canvas,
edgeTrianglePaint ?? trianglePaint,
loopResult.arrowBase.dx,
loopResult.arrowBase.dy,
loopResult.arrowTip.dx,
loopResult.arrowTip.dy,
);

drawStyledLine(
canvas,
loopResult.arrowBase,
triangleCentroid,
currentPaint,
lineType: lineType,
);
}

return;
}
}

if (hasBendEdges(edge)) {
_renderEdgeWithBendPoints(canvas, edge, currentPaint, edgeTrianglePaint ?? trianglePaint);
} else {
Expand Down Expand Up @@ -159,4 +192,4 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer {
}
}
}
}
}
10 changes: 9 additions & 1 deletion lib/tree/TreeEdgeRenderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ class TreeEdgeRenderer extends EdgeRenderer {
var node = edge.source;
var child = edge.destination;

if (node == child) {
final loopPath = buildSelfLoopPath(edge, arrowLength: 0.0);
if (loopPath != null) {
drawStyledPath(canvas, loopPath.path, edgePaint, lineType: child.lineType);
}
return;
}

final parentPos = getNodePosition(node);
final childPos = getNodePosition(child);

Expand Down Expand Up @@ -214,4 +222,4 @@ class TreeEdgeRenderer extends EdgeRenderer {
..lineTo(childRightX, childCenterY);
}
}
}
}
Loading