diff --git a/lib/Graph.dart b/lib/Graph.dart index ac35f69..9f3d605 100644 --- a/lib/Graph.dart +++ b/lib/Graph.dart @@ -144,6 +144,13 @@ class Graph { } +enum LineType { + Default, + DottedLine, + DashedLine, + SineLine, +} + class Node { ValueKey? key; @@ -163,6 +170,8 @@ class Node { Offset position = Offset(0, 0); + LineType lineType = LineType.Default; + double get height => size.height; double get width => size.width; @@ -189,7 +198,7 @@ class Node { @override String toString() { - return 'Node{position: $position, key: $key, _size: $size}'; + return 'Node{position: $position, key: $key, _size: $size, lineType: $lineType}'; } } diff --git a/lib/GraphView.dart b/lib/GraphView.dart index c56b3c8..0e7e015 100644 --- a/lib/GraphView.dart +++ b/lib/GraphView.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'dart:ui'; import 'package:collection/collection.dart' show IterableExtension; part 'Graph.dart'; diff --git a/lib/layered/SugiyamaAlgorithm.dart b/lib/layered/SugiyamaAlgorithm.dart index e03525d..0607f35 100644 --- a/lib/layered/SugiyamaAlgorithm.dart +++ b/lib/layered/SugiyamaAlgorithm.dart @@ -85,7 +85,7 @@ class SugiyamaAlgorithm extends Algorithm { void initSugiyamaData() { graph.nodes.forEach((node) { node.position = Offset(0, 0); - nodeData[node] = SugiyamaNodeData(); + nodeData[node] = SugiyamaNodeData(node.lineType); }); graph.edges.forEach((edge) { @@ -149,7 +149,7 @@ class SugiyamaAlgorithm extends Algorithm { while (iterator.moveNext()) { final edge = iterator.current; final dummy = Node.Id(dummyId.hashCode); - final dummyNodeData = SugiyamaNodeData(); + final dummyNodeData = SugiyamaNodeData(node.lineType); dummyNodeData.isDummy = true; dummyNodeData.layer = indexNextLayer; nextLayer.add(dummy); diff --git a/lib/layered/SugiyamaEdgeRenderer.dart b/lib/layered/SugiyamaEdgeRenderer.dart index a7ab267..9d1c148 100644 --- a/lib/layered/SugiyamaEdgeRenderer.dart +++ b/lib/layered/SugiyamaEdgeRenderer.dart @@ -15,6 +15,7 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer { var trianglePaint = Paint() ..color = paint.color ..style = PaintingStyle.fill; + graph.edges.forEach((edge) { final source = edge.source; @@ -100,19 +101,48 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer { clippedLine = clipLine(startX, startY, stopX, stopY, destination); + var destinationPoint = Offset(stopX, stopY); + if (addTriangleToEdge) { final triangleCentroid = drawTriangle( canvas, edgeTrianglePaint ?? trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]); - canvas.drawLine( - Offset(clippedLine[0], clippedLine[1]), Offset(triangleCentroid[0], triangleCentroid[1]), currentPaint); - } else { - canvas.drawLine( - Offset(clippedLine[0], clippedLine[1]), Offset(stopX, stopY), currentPaint); + destinationPoint = Offset(triangleCentroid[0], triangleCentroid[1]); } - } - }); + + // Draw the line + switch (nodeData[destination]?.lineType) { + case LineType.DashedLine: + _drawDashedLine( + canvas, + Offset(clippedLine[0], clippedLine[1]), destinationPoint, + currentPaint, 0.6 + ); + break; + case LineType.DottedLine: + // dotted line uses the same method as dashed line, but with a lineLength of 0.0 + _drawDashedLine( + canvas, + Offset(clippedLine[0], clippedLine[1]), destinationPoint, + currentPaint, 0.0 + ); + break; + case LineType.SineLine: + _drawSineLine( + canvas, + Offset(clippedLine[0], clippedLine[1]), destinationPoint, + currentPaint + ); + break; + default: + canvas.drawLine( + Offset(clippedLine[0], clippedLine[1]), destinationPoint, + currentPaint + ); + break; + } + }}); } void _drawSharpBendPointsEdge(List bendPoints) { @@ -152,4 +182,87 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer { } } } + + +void _drawDashedLine(Canvas canvas, Offset source, Offset destination, Paint paint, double lineLength) { + // Calculate the distance between the source and destination points + var dx = destination.dx - source.dx; + var dy = destination.dy - source.dy; + + // Calculate the Euclidean distance + var distance = sqrt(dx * dx + dy * dy); + + var numLines = lineLength == 0.0 ? (distance / 5).ceil() : 14; + + // Calculate the step size for each line + var stepX = dx / numLines; + var stepY = dy / numLines; + + // Set a fixed radius for the circles + var circleRadius = 1.0; + + // Set a fixed stroke width for the circles + var circleStrokeWidth = 1.0; + var circlePaint = Paint() + ..color = paint.color + ..strokeWidth = circleStrokeWidth + ..style = PaintingStyle.fill; // Change to fill style + + // Draw the lines or dots between the two points + Iterable.generate(numLines).forEach((i) { + var startX = source.dx + (i * stepX); + var startY = source.dy + (i * stepY); + if (lineLength == 0.0) { + // Draw a dot with a fixed radius and stroke width + canvas.drawCircle(Offset(startX, startY), circleRadius, circlePaint); + } else { + // Draw a dash + var endX = startX + (stepX * lineLength); + var endY = startY + (stepY * lineLength); + canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint); + } + }); +} + + void _drawSineLine(Canvas canvas, Offset source, Offset destination, Paint paint) { + paint..strokeWidth = 1.5; + + final dx = destination.dx - source.dx; + final dy = destination.dy - source.dy; + final distance = sqrt(dx * dx + dy * dy); + final lineLength = 6; + var phaseOffset = 2; + + // Verify dx and dy to avoid NaN to Offset() + if (dx != 0 || dy != 0) { + var distanceTraveled = 0.0; + var phase = 0.0; + final path = Path()..moveTo(source.dx, source.dy); + + while (distanceTraveled < distance) { + final segmentLength = min(lineLength, distance - distanceTraveled); + final segmentFraction = segmentLength / distance; + final segmentDestination = Offset( + source.dx + dx * segmentFraction, + source.dy + dy * segmentFraction, + ); + + final y = sin(phase + phaseOffset) * segmentLength; + + num x; + if ((dx > 0 && dy < 0) || (dx < 0 && dy > 0)) { + x = sin(phase + phaseOffset) * segmentLength; + } else { // dx < 0 && dy < 0 + x = -sin(phase + phaseOffset) * segmentLength; + } + + path.lineTo(segmentDestination.dx + x, segmentDestination.dy + y); + + distanceTraveled += segmentLength; + source = segmentDestination; + phase += pi * segmentLength / lineLength; + } + canvas.drawPath(path, paint); + } + } } diff --git a/lib/layered/SugiyamaNodeData.dart b/lib/layered/SugiyamaNodeData.dart index 0d68c81..3b37c06 100644 --- a/lib/layered/SugiyamaNodeData.dart +++ b/lib/layered/SugiyamaNodeData.dart @@ -8,11 +8,14 @@ class SugiyamaNodeData { int position = -1; List predecessorNodes = []; List successorNodes = []; + LineType lineType; + + SugiyamaNodeData(this.lineType); bool get isReversed => reversed.isNotEmpty; @override String toString() { - return 'SugiyamaNodeData{reversed: $reversed, isDummy: $isDummy, median: $median, layer: $layer, position: $position'; + return 'SugiyamaNodeData{reversed: $reversed, isDummy: $isDummy, median: $median, layer: $layer, position: $position, lineType: $lineType}'; } }