using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; namespace AIStudio.Wpf.DiagramDesigner { // Note: I couldn't find a useful open source library that does // orthogonal routing so started to write something on my own. // Categorize this as a quick and dirty short term solution. // I will keep on searching. // Helper class to provide an orthogonal connection path public class OrthogonalPathFinder : IPathFinder { private const int const_margin = 20; public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, Point sourceA, Point sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) { List connectionPoints; var isFullConnection = sinkConnectorInfo is FullyCreatedConnectorInfo; var area = new Rect(sourceA, sourceB); var points = new List() { new Point(sourceA.X < sourceB.X ? 0d : area.Width, sourceA.Y < sourceB.Y ? 0d : area.Height ), new Point(sourceA.X > sourceB.X ? 0d : area.Width, sourceA.Y > sourceB.Y ? 0d : area.Height) }; ConnectorInfo sourceInfo = ConnectorInfo(sourceConnectorInfo.Orientation, points[0].X, points[0].Y, sourceConnectorInfo.DataItem.ItemWidth, sourceConnectorInfo.DataItem.ItemHeight, points[0]); //StartPoint = points[0]; if (isFullConnection) { ConnectorInfo sinkInfo = ConnectorInfo(sinkConnectorInfo.Orientation, points[1].X, points[1].Y, ((FullyCreatedConnectorInfo)sinkConnectorInfo).DataItem.ItemWidth, ((FullyCreatedConnectorInfo)sinkConnectorInfo).DataItem.ItemHeight, points[1]); connectionPoints = PointInfoBase.ToList(GetConnectionLine(diagramViewModel, sourceInfo, sinkInfo, false, sourceConnectorInfo.IsInnerPoint)); //EndPoint = ConnectionPoints.Last(); } else { connectionPoints = PointInfoBase.ToList(GetConnectionLine(diagramViewModel, sourceInfo, points[1], sourceConnectorInfo.Orientation, false, sourceConnectorInfo.IsInnerPoint)); //EndPoint = new Point(); } return connectionPoints; } public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, Point position) { return new ConnectorInfo() { Orientation = orientation, DesignerItemSize = new Size(width, height), DesignerItemLeft = left, DesignerItemTop = top, Position = position }; } public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) { List linePoints = new List(); int margin1 = sourceInnerPoint ? 0 : const_margin; int margin2 = const_margin; Rect rectSource = GetRectWithMargin(source, margin1); Rect rectSink = GetRectWithMargin(sink, margin2); Point startPoint = GetOffsetPoint(source, rectSource, sourceInnerPoint); Point endPoint = GetOffsetPoint(sink, rectSink); linePoints.Add(startPoint); Point currentPoint = startPoint; if (!rectSink.Contains(currentPoint) && !rectSource.Contains(endPoint)) { while (true) { #region source node if (IsPointVisible(currentPoint, endPoint, new Rect[] { rectSource, rectSink })) { linePoints.Add(endPoint); currentPoint = endPoint; break; } Point neighbour = GetNearestVisibleNeighborSink(currentPoint, endPoint, sink, rectSource, rectSink); if (!double.IsNaN(neighbour.X)) { linePoints.Add(neighbour); linePoints.Add(endPoint); currentPoint = endPoint; break; } if (currentPoint == startPoint) { bool flag; Point n = GetNearestNeighborSource(source, endPoint, rectSource, rectSink, out flag, sourceInnerPoint); if (linePoints.Contains(n)) { break; } linePoints.Add(n); currentPoint = n; if (!IsRectVisible(currentPoint, rectSink, new Rect[] { rectSource })) { Point n1, n2; GetOppositeCorners(source.Orientation, rectSource, out n1, out n2, sourceInnerPoint); if (flag) { linePoints.Add(n1); currentPoint = n1; } else { linePoints.Add(n2); currentPoint = n2; } if (!IsRectVisible(currentPoint, rectSink, new Rect[] { rectSource })) { if (flag) { linePoints.Add(n2); currentPoint = n2; } else { linePoints.Add(n1); currentPoint = n1; } } } } #endregion #region sink node else // from here on we jump to the sink node { Point n1, n2; // neighbour corner Point s1, s2; // opposite corner GetNeighborCorners(sink.Orientation, rectSink, out s1, out s2); GetOppositeCorners(sink.Orientation, rectSink, out n1, out n2); bool n1Visible = IsPointVisible(currentPoint, n1, new Rect[] { rectSource, rectSink }); bool n2Visible = IsPointVisible(currentPoint, n2, new Rect[] { rectSource, rectSink }); if (n1Visible && n2Visible) { if (rectSource.Contains(n1)) { linePoints.Add(n2); if (rectSource.Contains(s2)) { linePoints.Add(n1); linePoints.Add(s1); } else linePoints.Add(s2); linePoints.Add(endPoint); currentPoint = endPoint; break; } if (rectSource.Contains(n2)) { linePoints.Add(n1); if (rectSource.Contains(s1)) { linePoints.Add(n2); linePoints.Add(s2); } else linePoints.Add(s1); linePoints.Add(endPoint); currentPoint = endPoint; break; } if ((Distance(n1, endPoint) <= Distance(n2, endPoint))) { linePoints.Add(n1); if (rectSource.Contains(s1)) { linePoints.Add(n2); linePoints.Add(s2); } else linePoints.Add(s1); linePoints.Add(endPoint); currentPoint = endPoint; break; } else { linePoints.Add(n2); if (rectSource.Contains(s2)) { linePoints.Add(n1); linePoints.Add(s1); } else linePoints.Add(s2); linePoints.Add(endPoint); currentPoint = endPoint; break; } } else if (n1Visible) { linePoints.Add(n1); if (rectSource.Contains(s1)) { linePoints.Add(n2); linePoints.Add(s2); } else linePoints.Add(s1); linePoints.Add(endPoint); currentPoint = endPoint; break; } else { linePoints.Add(n2); if (rectSource.Contains(s2)) { linePoints.Add(n1); linePoints.Add(s1); } else linePoints.Add(s2); linePoints.Add(endPoint); currentPoint = endPoint; break; } } #endregion } } else { linePoints.Add(endPoint); } linePoints = OptimizeLinePoints(linePoints, new Rect[] { rectSource, rectSink }, source.Orientation, sink.Orientation); CheckPathEnd(source, sink, showLastLine, linePoints, sourceInnerPoint); return linePoints; } public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, Point sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false) { List linePoints = new List(); int margin = isInnerPoint ? 0 : const_margin; Rect rectSource = GetRectWithMargin(source, margin); Point startPoint = GetOffsetPoint(source, rectSource, isInnerPoint); Point endPoint = sinkPoint; linePoints.Add(startPoint); Point currentPoint = startPoint; if (!rectSource.Contains(endPoint)) { while (true) { if (IsPointVisible(currentPoint, endPoint, new Rect[] { rectSource })) { linePoints.Add(endPoint); break; } bool sideFlag; Point n = GetNearestNeighborSource(source, endPoint, rectSource, out sideFlag, isInnerPoint); linePoints.Add(n); currentPoint = n; if (IsPointVisible(currentPoint, endPoint, new Rect[] { rectSource })) { linePoints.Add(endPoint); break; } else { Point n1, n2; GetOppositeCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if (sideFlag) linePoints.Add(n1); else linePoints.Add(n2); linePoints.Add(endPoint); break; } } } else { linePoints.Add(endPoint); } if (preferredOrientation != ConnectorOrientation.None) linePoints = OptimizeLinePoints(linePoints, new Rect[] { rectSource }, source.Orientation, preferredOrientation); else linePoints = OptimizeLinePoints(linePoints, new Rect[] { rectSource }, source.Orientation, GetOpositeOrientation(source.Orientation)); if (!showLastLine) { linePoints.Insert(0, source.Position); } return linePoints; } private static List OptimizeLinePoints(List linePoints, Rect[] rectangles, ConnectorOrientation sourceOrientation, ConnectorOrientation sinkOrientation) { List points = new List(); int cut = 0; for (int i = 0; i < linePoints.Count; i++) { if (i >= cut) { for (int k = linePoints.Count - 1; k > i; k--) { if (IsPointVisible(linePoints[i], linePoints[k], rectangles)) { cut = k; break; } } points.Add(linePoints[i]); } } #region Line for (int j = 0; j < points.Count - 1; j++) { if (points[j].X != points[j + 1].X && points[j].Y != points[j + 1].Y) { ConnectorOrientation orientationFrom; ConnectorOrientation orientationTo; // orientation from point if (j == 0) orientationFrom = sourceOrientation; else orientationFrom = GetOrientation(points[j], points[j - 1]); // orientation to pint if (j == points.Count - 2) orientationTo = sinkOrientation; else orientationTo = GetOrientation(points[j + 1], points[j + 2]); if ((orientationFrom == ConnectorOrientation.Left || orientationFrom == ConnectorOrientation.Right) && (orientationTo == ConnectorOrientation.Left || orientationTo == ConnectorOrientation.Right)) { double centerX = Math.Min(points[j].X, points[j + 1].X) + Math.Abs(points[j].X - points[j + 1].X) / 2; points.Insert(j + 1, new Point(centerX, points[j].Y)); points.Insert(j + 2, new Point(centerX, points[j + 2].Y)); if (points.Count - 1 > j + 3) points.RemoveAt(j + 3); return points; } if ((orientationFrom == ConnectorOrientation.Top || orientationFrom == ConnectorOrientation.Bottom) && (orientationTo == ConnectorOrientation.Top || orientationTo == ConnectorOrientation.Bottom)) { double centerY = Math.Min(points[j].Y, points[j + 1].Y) + Math.Abs(points[j].Y - points[j + 1].Y) / 2; points.Insert(j + 1, new Point(points[j].X, centerY)); points.Insert(j + 2, new Point(points[j + 2].X, centerY)); if (points.Count - 1 > j + 3) points.RemoveAt(j + 3); return points; } if ((orientationFrom == ConnectorOrientation.Left || orientationFrom == ConnectorOrientation.Right) && (orientationTo == ConnectorOrientation.Top || orientationTo == ConnectorOrientation.Bottom)) { points.Insert(j + 1, new Point(points[j + 1].X, points[j].Y)); return points; } if ((orientationFrom == ConnectorOrientation.Top || orientationFrom == ConnectorOrientation.Bottom) && (orientationTo == ConnectorOrientation.Left || orientationTo == ConnectorOrientation.Right)) { points.Insert(j + 1, new Point(points[j].X, points[j + 1].Y)); return points; } } } #endregion return points; } private static ConnectorOrientation GetOrientation(Point p1, Point p2) { if (p1.X == p2.X) { if (p1.Y >= p2.Y) return ConnectorOrientation.Bottom; else return ConnectorOrientation.Top; } else if (p1.Y == p2.Y) { if (p1.X >= p2.X) return ConnectorOrientation.Right; else return ConnectorOrientation.Left; } throw new Exception("Failed to retrieve orientation"); } private static Orientation GetOrientation(ConnectorOrientation sourceOrientation, bool isInnerPoint) { if (isInnerPoint) { return Orientation.Vertical; } switch (sourceOrientation) { case ConnectorOrientation.Left: return Orientation.Horizontal; case ConnectorOrientation.Top: return Orientation.Vertical; case ConnectorOrientation.Right: return Orientation.Horizontal; case ConnectorOrientation.Bottom: return Orientation.Vertical; default: throw new Exception("Unknown ConnectorOrientation"); } } private static Point GetNearestNeighborSource(ConnectorInfo source, Point endPoint, Rect rectSource, Rect rectSink, out bool flag, bool isInnerPoint) { Point n1, n2; // neighbors GetNeighborCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if (rectSink.Contains(n1)) { flag = false; return n2; } if (rectSink.Contains(n2)) { flag = true; return n1; } if ((Distance(n1, endPoint) <= Distance(n2, endPoint))) { flag = true; return n1; } else { flag = false; return n2; } } private static Point GetNearestNeighborSource(ConnectorInfo source, Point endPoint, Rect rectSource, out bool flag, bool isInnerPoint) { Point n1, n2; // neighbors GetNeighborCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if ((Distance(n1, endPoint) <= Distance(n2, endPoint))) { flag = true; return n1; } else { flag = false; return n2; } } private static Point GetNearestVisibleNeighborSink(Point currentPoint, Point endPoint, ConnectorInfo sink, Rect rectSource, Rect rectSink) { Point s1, s2; // neighbors on sink side GetNeighborCorners(sink.Orientation, rectSink, out s1, out s2); bool flag1 = IsPointVisible(currentPoint, s1, new Rect[] { rectSource, rectSink }); bool flag2 = IsPointVisible(currentPoint, s2, new Rect[] { rectSource, rectSink }); if (flag1) // s1 visible { if (flag2) // s1 and s2 visible { if (rectSink.Contains(s1)) return s2; if (rectSink.Contains(s2)) return s1; if ((Distance(s1, endPoint) <= Distance(s2, endPoint))) return s1; else return s2; } else { return s1; } } else // s1 not visible { if (flag2) // only s2 visible { return s2; } else // s1 and s2 not visible { return new Point(double.NaN, double.NaN); } } } private static bool IsPointVisible(Point fromPoint, Point targetPoint, Rect[] rectangles) { foreach (Rect rect in rectangles) { if (RectangleIntersectsLine(rect, fromPoint, targetPoint)) return false; } return true; } private static bool IsRectVisible(Point fromPoint, Rect targetRect, Rect[] rectangles) { if (IsPointVisible(fromPoint, targetRect.TopLeft, rectangles)) return true; if (IsPointVisible(fromPoint, targetRect.TopRight, rectangles)) return true; if (IsPointVisible(fromPoint, targetRect.BottomLeft, rectangles)) return true; if (IsPointVisible(fromPoint, targetRect.BottomRight, rectangles)) return true; return false; } private static bool RectangleIntersectsLine(Rect rect, Point startPoint, Point endPoint) { rect.Inflate(-1, -1); return rect.IntersectsWith(new Rect(startPoint, endPoint)); } private static void GetOppositeCorners(ConnectorOrientation orientation, Rect rect, out Point n1, out Point n2, bool isInnerPoint = false) { if (isInnerPoint) { n1 = rect.Location; n2 = rect.Location; return; } switch (orientation) { case ConnectorOrientation.Left: n1 = rect.TopRight; n2 = rect.BottomRight; break; case ConnectorOrientation.Top: n1 = rect.BottomLeft; n2 = rect.BottomRight; break; case ConnectorOrientation.Right: n1 = rect.TopLeft; n2 = rect.BottomLeft; break; case ConnectorOrientation.Bottom: n1 = rect.TopLeft; n2 = rect.TopRight; break; default: throw new Exception("No opposite corners found!"); } } private static void GetNeighborCorners(ConnectorOrientation orientation, Rect rect, out Point n1, out Point n2, bool isInnerPoint = false) { if (isInnerPoint) { n1 = rect.Location; n2 = rect.Location; return; } switch (orientation) { case ConnectorOrientation.Left: n1 = rect.TopLeft; n2 = rect.BottomLeft; break; case ConnectorOrientation.Top: n1 = rect.TopLeft; n2 = rect.TopRight; break; case ConnectorOrientation.Right: n1 = rect.TopRight; n2 = rect.BottomRight; break; case ConnectorOrientation.Bottom: n1 = rect.BottomLeft; n2 = rect.BottomRight; break; default: throw new Exception("No neighour corners found!"); } } private static double Distance(Point p1, Point p2) { return Point.Subtract(p1, p2).Length; } private static Rect GetRectWithMargin(ConnectorInfo connectorThumb, double margin) { Rect rect = new Rect(connectorThumb.DesignerItemLeft, connectorThumb.DesignerItemTop, 0, 0); rect.Inflate(margin, margin); return rect; } private static Point GetOffsetPoint(ConnectorInfo connector, Rect rect, bool isInnerPoint = false) { Point offsetPoint = new Point(); if (isInnerPoint) { offsetPoint = new Point(connector.Position.X, connector.Position.Y); return offsetPoint; } switch (connector.Orientation) { case ConnectorOrientation.Left: offsetPoint = new Point(rect.Left, connector.Position.Y); break; case ConnectorOrientation.Top: offsetPoint = new Point(connector.Position.X, rect.Top); break; case ConnectorOrientation.Right: offsetPoint = new Point(rect.Right, connector.Position.Y); break; case ConnectorOrientation.Bottom: offsetPoint = new Point(connector.Position.X, rect.Bottom); break; default: break; } return offsetPoint; } private static void CheckPathEnd(ConnectorInfo source, ConnectorInfo sink, bool showLastLine, List linePoints, bool sourceInnerPoint) { if (showLastLine) { Point startPoint = new Point(0, 0); Point endPoint = new Point(0, 0); double marginPath = 15; switch (source.Orientation) { case ConnectorOrientation.Left: startPoint = new Point(source.Position.X - marginPath, source.Position.Y); break; case ConnectorOrientation.Top: startPoint = new Point(source.Position.X, source.Position.Y - marginPath); break; case ConnectorOrientation.Right: startPoint = new Point(source.Position.X + marginPath, source.Position.Y); break; case ConnectorOrientation.Bottom: startPoint = new Point(source.Position.X, source.Position.Y + marginPath); break; default: break; } if (sourceInnerPoint) { startPoint = new Point(source.Position.X, source.Position.Y); } switch (sink.Orientation) { case ConnectorOrientation.Left: endPoint = new Point(sink.Position.X - marginPath, sink.Position.Y); break; case ConnectorOrientation.Top: endPoint = new Point(sink.Position.X, sink.Position.Y - marginPath); break; case ConnectorOrientation.Right: endPoint = new Point(sink.Position.X + marginPath, sink.Position.Y); break; case ConnectorOrientation.Bottom: endPoint = new Point(sink.Position.X, sink.Position.Y + marginPath); break; default: break; } linePoints.Insert(0, startPoint); linePoints.Add(endPoint); } else { linePoints.Insert(0, source.Position); linePoints.Add(sink.Position); } } private static ConnectorOrientation GetOpositeOrientation(ConnectorOrientation connectorOrientation) { switch (connectorOrientation) { case ConnectorOrientation.Left: return ConnectorOrientation.Right; case ConnectorOrientation.Top: return ConnectorOrientation.Bottom; case ConnectorOrientation.Right: return ConnectorOrientation.Left; case ConnectorOrientation.Bottom: return ConnectorOrientation.Top; default: return ConnectorOrientation.Top; } } } }