diff --git a/AIStudio.Wpf.DiagramApp/ViewModels/DiagramsViewModel.cs b/AIStudio.Wpf.DiagramApp/ViewModels/DiagramsViewModel.cs index 3e1ee42..739d223 100644 --- a/AIStudio.Wpf.DiagramApp/ViewModels/DiagramsViewModel.cs +++ b/AIStudio.Wpf.DiagramApp/ViewModels/DiagramsViewModel.cs @@ -335,8 +335,8 @@ namespace AIStudio.Wpf.DiagramApp.ViewModels foreach (var connectionVM in DiagramViewModel.Items.OfType()) { - FullyCreatedConnectorInfo sinkConnector = connectionVM.SinkConnectorInfo as FullyCreatedConnectorInfo; - + FullyCreatedConnectorInfo sinkConnector = connectionVM.SinkConnectorInfoFully; + ConnectionItem connection = new ConnectionItem( connectionVM.SourceConnectorInfo.DataItem.Id, connectionVM.SourceConnectorInfo.Orientation, @@ -523,7 +523,9 @@ namespace AIStudio.Wpf.DiagramApp.ViewModels foreach (var connectionVM in DiagramViewModel.Items.OfType()) { - FullyCreatedConnectorInfo sinkConnector = connectionVM.SinkConnectorInfo as FullyCreatedConnectorInfo; + if (connectionVM.IsFullConnection == false) continue; + + FullyCreatedConnectorInfo sinkConnector = connectionVM.SinkConnectorInfoFully; ConnectionItem connection = new ConnectionItem( connectionVM.SourceConnectorInfo.DataItem.Id, diff --git a/AIStudio.Wpf.DiagramApp/ViewModels/LogicalViewModel.cs b/AIStudio.Wpf.DiagramApp/ViewModels/LogicalViewModel.cs index 19a61b6..5e34e3e 100644 --- a/AIStudio.Wpf.DiagramApp/ViewModels/LogicalViewModel.cs +++ b/AIStudio.Wpf.DiagramApp/ViewModels/LogicalViewModel.cs @@ -126,9 +126,9 @@ namespace AIStudio.Wpf.Logical if (arg is ConnectorViewModel connector) { - if (connector.SinkConnectorInfo is FullyCreatedConnectorInfo fully) + if (connector.IsFullConnection) { - if (DiagramViewModel.Items.OfType().Any(p => p.SinkConnectorInfo == fully)) + if (DiagramViewModel.Items.OfType().Any(p => p.SinkConnectorInfo == connector.SinkConnectorInfoFully)) { return false; } diff --git a/AIStudio.Wpf.DiagramDesigner/AIStudio.Wpf.DiagramDesigner.csproj b/AIStudio.Wpf.DiagramDesigner/AIStudio.Wpf.DiagramDesigner.csproj index 3dbba4a..0014bbf 100644 --- a/AIStudio.Wpf.DiagramDesigner/AIStudio.Wpf.DiagramDesigner.csproj +++ b/AIStudio.Wpf.DiagramDesigner/AIStudio.Wpf.DiagramDesigner.csproj @@ -58,9 +58,5 @@ Designer - - - - \ No newline at end of file diff --git a/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs b/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs index ec73f17..7d46bb3 100644 --- a/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs +++ b/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs @@ -203,7 +203,7 @@ namespace AIStudio.Wpf.DiagramDesigner Rect rectangleBounds = sourceConnector.TransformToVisual(this).TransformBounds(new Rect(sourceConnector.RenderSize)); Point point = new Point(rectangleBounds.Left + (rectangleBounds.Width / 2), rectangleBounds.Bottom + (rectangleBounds.Height / 2)); - partialConnection = new ConnectorViewModel(_viewModel, sourceDataItem, new PartCreatedConnectionInfo(point), VectorLineDrawMode); + partialConnection = new ConnectorViewModel(_viewModel, sourceDataItem, new PartCreatedConnectionInfo(point.X, point.Y), VectorLineDrawMode); _viewModel.DirectAddItemCommand.Execute(partialConnection); } @@ -225,7 +225,7 @@ namespace AIStudio.Wpf.DiagramDesigner Rect rectangleBounds = new Rect(sourceConnectorInfo.DataItem.Left, sourceConnectorInfo.DataItem.Top, 3, 3); Point point = new Point(rectangleBounds.Left + (rectangleBounds.Width / 2), rectangleBounds.Bottom + (rectangleBounds.Height / 2)); - partialConnection = new ConnectorViewModel(_viewModel, sourceConnectorInfo, new PartCreatedConnectionInfo(point), VectorLineDrawMode); + partialConnection = new ConnectorViewModel(_viewModel, sourceConnectorInfo, new PartCreatedConnectionInfo(point.X, point.Y), VectorLineDrawMode); _viewModel.DirectAddItemCommand.Execute(partialConnection); } } @@ -309,7 +309,7 @@ namespace AIStudio.Wpf.DiagramDesigner { if (e.LeftButton == MouseButtonState.Pressed) { - partialConnection.SinkConnectorInfo = new PartCreatedConnectionInfo(currentPoint); + partialConnection.SinkConnectorInfo = new PartCreatedConnectionInfo(currentPoint.X, currentPoint.Y); HitTesting(currentPoint); } } diff --git a/AIStudio.Wpf.DiagramDesigner/Controls/DragThumb.cs b/AIStudio.Wpf.DiagramDesigner/Controls/DragThumb.cs index f7bbbf1..866808a 100644 --- a/AIStudio.Wpf.DiagramDesigner/Controls/DragThumb.cs +++ b/AIStudio.Wpf.DiagramDesigner/Controls/DragThumb.cs @@ -31,9 +31,9 @@ namespace AIStudio.Wpf.DiagramDesigner.Controls if (designerItem is ConnectorViewModel connector) { designerItems.Add(connector.SourceConnectorInfo.DataItem); - if (connector.SinkConnectorInfo is FullyCreatedConnectorInfo) + if (connector.IsFullConnection) { - designerItems.Add((connector.SinkConnectorInfo as FullyCreatedConnectorInfo).DataItem); + designerItems.Add(connector.SinkConnectorInfoFully.DataItem); } if (designerItem.OutTextItem != null) { diff --git a/AIStudio.Wpf.DiagramDesigner/Controls/PointDragThumb.cs b/AIStudio.Wpf.DiagramDesigner/Controls/PointDragThumb.cs index 57b0fff..c169d14 100644 --- a/AIStudio.Wpf.DiagramDesigner/Controls/PointDragThumb.cs +++ b/AIStudio.Wpf.DiagramDesigner/Controls/PointDragThumb.cs @@ -31,7 +31,7 @@ namespace AIStudio.Wpf.DiagramDesigner.Controls void DragThumb_DragDelta(object sender, DragDeltaEventArgs e) { - if (this.DataContext is PointInfoBase point) + if (this.DataContext is ConnectorPoint point) { double minLeft = double.MaxValue; double minTop = double.MaxValue; diff --git a/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionDataConverter.cs b/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionDataConverter.cs index 0053700..2dc526e 100644 --- a/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionDataConverter.cs +++ b/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionDataConverter.cs @@ -26,45 +26,53 @@ namespace AIStudio.Wpf.DiagramDesigner public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { PathGeometry pathGeometry = new PathGeometry(); - if (values[0] != null) - { - List points = (List)values[0]; + //if (values[0] != null) + //{ + // List points = (List)values[0]; - PathFigure figure = new PathFigure(); - figure.StartPoint = points[0]; - if (values[1]?.ToString() == DrawMode.RadiusConnectingLine.ToString()) - { - for (var i = 0; i < points.Count - 1; i++) - { - int current = i, last = i - 1, next = i + 1, next2 = i + 2; - if (last == -1) - { - last = 0; - } - if (next == points.Count) - { - next = points.Count - 1; - } - if (next2 == points.Count) - { - next2 = points.Count - 1; - } - var bzs = SegmentHelper.GetBezierSegment(points[current], points[last], points[next], points[next2]); - figure.Segments.Add(bzs); - } - } - else - { - for (int i = 0; i < points.Count; i++) - { + // PathFigure figure = new PathFigure(); + // figure.StartPoint = points[0]; + // if (values[1]?.ToString() == DrawMode.RadiusConnectingLine.ToString()) + // { + // for (var i = 0; i < points.Count - 1; i++) + // { + // int current = i, last = i - 1, next = i + 1, next2 = i + 2; + // if (last == -1) + // { + // last = 0; + // } + // if (next == points.Count) + // { + // next = points.Count - 1; + // } + // if (next2 == points.Count) + // { + // next2 = points.Count - 1; + // } + // var bzs = SegmentHelper.GetBezierSegment(points[current], points[last], points[next], points[next2]); + // figure.Segments.Add(bzs); + // } - LineSegment arc = new LineSegment(points[i], true); - figure.Segments.Add(arc); - } - } + // //贝塞尔曲线的简单连接 + // //var start = points[0]; + // //var end = points.Last(); + // //var width = end.X - start.X; + // //var height = end.Y - start.Y; + // //var ctrlPos = 0.382 * width + 0.309 * height; + // //figure.Segments.Add(new BezierSegment(new Point(ctrlPos - 1, 1), new Point(width - ctrlPos + 1, height - 1), new Point(width - 1, height - 1), true)); + // } + // else + // { + // for (int i = 0; i < points.Count; i++) + // { - pathGeometry.Figures.Add(figure); - } + // LineSegment arc = new LineSegment(points[i], true); + // figure.Segments.Add(arc); + // } + // } + + // pathGeometry.Figures.Add(figure); + //} return pathGeometry; } diff --git a/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionPathConverter.cs b/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionPathConverter.cs index ba2f686..ac5ab57 100644 --- a/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionPathConverter.cs +++ b/AIStudio.Wpf.DiagramDesigner/Converters/ConnectionPathConverter.cs @@ -25,13 +25,13 @@ namespace AIStudio.Wpf.DiagramDesigner public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { - List points = (List)value; + List points = (List)value; PointCollection pointCollection = new PointCollection(); if (points != null) { foreach (var point in points) { - pointCollection.Add(point); + pointCollection.Add(new Point(point.X, point.Y)); } } return pointCollection; diff --git a/AIStudio.Wpf.DiagramDesigner/Enums/ConnectorOrientation.cs b/AIStudio.Wpf.DiagramDesigner/Enums/ConnectorOrientation.cs index de516d8..1daf519 100644 --- a/AIStudio.Wpf.DiagramDesigner/Enums/ConnectorOrientation.cs +++ b/AIStudio.Wpf.DiagramDesigner/Enums/ConnectorOrientation.cs @@ -7,9 +7,13 @@ namespace AIStudio.Wpf.DiagramDesigner public enum ConnectorOrientation { None = 0, - Left = 1, - Top = 2, - Right = 3, - Bottom = 4 + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left, + TopLeft } } diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/BezierSpline.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/BezierSpline.cs new file mode 100644 index 0000000..4645ef5 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/BezierSpline.cs @@ -0,0 +1,206 @@ +using System; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + /// + /// Bezier Spline methods + /// + /// + /// Modified: Peter Lee (peterlee.com.cn < at > gmail.com) + /// Update: 2009-03-16 + /// + /// see also: + /// Draw a smooth curve through a set of 2D points with Bezier primitives + /// http://www.codeproject.com/KB/graphics/BezierSpline.aspx + /// By Oleg V. Polikarpotchkin + /// + /// Algorithm Descripition: + /// + /// To make a sequence of individual Bezier curves to be a spline, we + /// should calculate Bezier control points so that the spline curve + /// has two continuous derivatives at knot points. + /// + /// Note: `[]` denotes subscript + /// `^` denotes supscript + /// `'` denotes first derivative + /// `''` denotes second derivative + /// + /// A Bezier curve on a single interval can be expressed as: + /// + /// B(t) = (1-t)^3 P0 + 3(1-t)^2 t P1 + 3(1-t)t^2 P2 + t^3 P3 (*) + /// + /// where t is in [0,1], and + /// 1. P0 - first knot point + /// 2. P1 - first control point (close to P0) + /// 3. P2 - second control point (close to P3) + /// 4. P3 - second knot point + /// + /// The first derivative of (*) is: + /// + /// B'(t) = -3(1-t)^2 P0 + 3(3t^2–4t+1) P1 + 3(2–3t)t P2 + 3t^2 P3 + /// + /// The second derivative of (*) is: + /// + /// B''(t) = 6(1-t) P0 + 6(3t-2) P1 + 6(1–3t) P2 + 6t P3 + /// + /// Considering a set of piecewise Bezier curves with n+1 points + /// (Q[0..n]) and n subintervals, the (i-1)-th curve should connect + /// to the i-th one: + /// + /// Q[0] = P0[1], + /// Q[1] = P0[2] = P3[1], ... , Q[i-1] = P0[i] = P3[i-1] (i = 1..n) (@) + /// + /// At the i-th subinterval, the Bezier curve is: + /// + /// B[i](t) = (1-t)^3 P0[i] + 3(1-t)^2 t P1[i] + + /// 3(1-t)t^2 P2[i] + t^3 P3[i] (i = 1..n) + /// + /// applying (@): + /// + /// B[i](t) = (1-t)^3 Q[i-1] + 3(1-t)^2 t P1[i] + + /// 3(1-t)t^2 P2[i] + t^3 Q[i] (i = 1..n) (i) + /// + /// From (i), the first derivative at the i-th subinterval is: + /// + /// B'[i](t) = -3(1-t)^2 Q[i-1] + 3(3t^2–4t+1) P1[i] + + /// 3(2–3t)t P2[i] + 3t^2 Q[i] (i = 1..n) + /// + /// Using the first derivative continuity condition: + /// + /// B'[i-1](1) = B'[i](0) + /// + /// we get: + /// + /// P1[i] + P2[i-1] = 2Q[i-1] (i = 2..n) (1) + /// + /// From (i), the second derivative at the i-th subinterval is: + /// + /// B''[i](t) = 6(1-t) Q[i-1] + 6(3t-2) P1[i] + + /// 6(1-3t) P2[i] + 6t Q[i] (i = 1..n) + /// + /// Using the second derivative continuity condition: + /// + /// B''[i-1](1) = B''[i](0) + /// + /// we get: + /// + /// P1[i-1] + 2P1[i] = P2[i] + 2P2[i-1] (i = 2..n) (2) + /// + /// Then, using the so-called "natural conditions": + /// + /// B''[1](0) = 0 + /// + /// B''[n](1) = 0 + /// + /// to the second derivative equations, and we get: + /// + /// 2P1[1] - P2[1] = Q[0] (3) + /// + /// 2P2[n] - P1[n] = Q[n] (4) + /// + /// From (1)(2)(3)(4), we have 2n conditions for n first control points + /// P1[1..n], and n second control points P2[1..n]. + /// + /// Eliminating P2[1..n], we get (be patient to get :-) a set of n + /// equations for solving P1[1..n]: + /// + /// 2P1[1] + P1[2] + = Q[0] + 2Q[1] + /// P1[1] + 4P1[2] + P1[3] = 4Q[1] + 2Q[2] + /// ... + /// P1[i-1] + 4P1[i] + P1[i+1] = 4Q[i-1] + 2Q[i] + /// ... + /// P1[n-2] + 4P1[n-1] + P1[n] = 4Q[n-2] + 2Q[n-1] + /// P1[n-1] + 3.5P1[n] = (8Q[n-1] + Q[n]) / 2 + /// + /// From this set of equations, P1[1..n] are easy but tedious to solve. + /// + public static class BezierSpline + { + /// + /// Get open-ended Bezier Spline Control Points. + /// + /// Input Knot Bezier spline points. + /// Output First Control points array of knots.Length - 1 length. + /// Output Second Control points array of knots.Length - 1 length. + /// parameter must be not null. + /// array must containg at least two points. + public static void GetCurveControlPoints(PointBase[] knots, out PointBase[] firstControlPoints, out PointBase[] secondControlPoints) + { + if (knots == null) + throw new ArgumentNullException("knots"); + int n = knots.Length - 1; + if (n < 1) + throw new ArgumentException("At least two knot points required", "knots"); + if (n == 1) + { // Special case: Bezier curve should be a straight line. + firstControlPoints = new PointBase[1]; + // 3P1 = 2P0 + P3 + firstControlPoints[0] = new PointBase((2 * knots[0].X + knots[1].X) / 3, (2 * knots[0].Y + knots[1].Y) / 3); + + secondControlPoints = new PointBase[1]; + // P2 = 2P1 – P0 + secondControlPoints[0] = new PointBase(2 * firstControlPoints[0].X - knots[0].X, 2 * firstControlPoints[0].Y - knots[0].Y); + return; + } + + // Calculate first Bezier control points + // Right hand side vector + double[] rhs = new double[n]; + + // Set right hand side X values + for (int i = 1; i < n - 1; ++i) + rhs[i] = 4 * knots[i].X + 2 * knots[i + 1].X; + rhs[0] = knots[0].X + 2 * knots[1].X; + rhs[n - 1] = (8 * knots[n - 1].X + knots[n].X) / 2.0; + // Get first control points X-values + double[] x = GetFirstControlPoints(rhs); + + // Set right hand side Y values + for (int i = 1; i < n - 1; ++i) + rhs[i] = 4 * knots[i].Y + 2 * knots[i + 1].Y; + rhs[0] = knots[0].Y + 2 * knots[1].Y; + rhs[n - 1] = (8 * knots[n - 1].Y + knots[n].Y) / 2.0; + // Get first control points Y-values + double[] y = GetFirstControlPoints(rhs); + + // Fill output arrays. + firstControlPoints = new PointBase[n]; + secondControlPoints = new PointBase[n]; + for (int i = 0; i < n; ++i) + { + // First control point + firstControlPoints[i] = new PointBase(x[i], y[i]); + // Second control point + if (i < n - 1) + secondControlPoints[i] = new PointBase(2 * knots[i + 1].X - x[i + 1], 2 * knots[i + 1].Y - y[i + 1]); + else + secondControlPoints[i] = new PointBase((knots[n].X + x[n - 1]) / 2, (knots[n].Y + y[n - 1]) / 2); + } + } + + /// + /// Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + /// + /// Right hand side vector. + /// Solution vector. + private static double[] GetFirstControlPoints(double[] rhs) + { + int n = rhs.Length; + double[] x = new double[n]; // Solution vector. + double[] tmp = new double[n]; // Temp workspace. + + double b = 2.0; + x[0] = rhs[0] / b; + for (int i = 1; i < n; i++) // Decomposition and forward substitution. + { + tmp[i] = 1 / b; + b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; + x[i] = (rhs[i] - x[i - 1]) / b; + } + for (int i = 1; i < n; i++) + x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution. + + return x; + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/EllipseBase.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/EllipseBase.cs new file mode 100644 index 0000000..9b92587 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/EllipseBase.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + public class EllipseBase : IShape + { + public EllipseBase(double cx, double cy, double rx, double ry) + { + Cx = cx; + Cy = cy; + Rx = rx; + Ry = ry; + } + + public double Cx { get; } + public double Cy { get; } + public double Rx { get; } + public double Ry { get; } + + public IEnumerable GetIntersectionsWithLine(LineBase line) + { + var a1 = line.Start; + var a2 = line.End; + var dir = new PointBase(line.End.X - line.Start.X, line.End.Y - line.Start.Y); + var diff = a1.Substract(Cx, Cy); + var mDir = new PointBase(dir.X / (Rx * Rx), dir.Y / (Ry * Ry)); + var mDiff = new PointBase(diff.X / (Rx * Rx), diff.Y / (Ry * Ry)); + + var a = dir.Dot(mDir); + var b = dir.Dot(mDiff); + var c = diff.Dot(mDiff) - 1.0; + var d = b * b - a * c; + + if (d > 0) + { + var root = Math.Sqrt(d); + var ta = (-b - root) / a; + var tb = (-b + root) / a; + + if (ta >= 0 && 1 >= ta || tb >= 0 && 1 >= tb) + { + if (0 <= ta && ta <= 1) + yield return a1.Lerp(a2, ta); + + if (0 <= tb && tb <= 1) + yield return a1.Lerp(a2, tb); + } + } + else + { + var t = -b / a; + if (0 <= t && t <= 1) + { + yield return a1.Lerp(a2, t); + } + } + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/IShape.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/IShape.cs new file mode 100644 index 0000000..ee6b4dc --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/IShape.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + public interface IShape + { + IEnumerable GetIntersectionsWithLine(LineBase line); + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/LineBase.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/LineBase.cs new file mode 100644 index 0000000..d135fae --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/LineBase.cs @@ -0,0 +1,43 @@ +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + public struct LineBase + { + public LineBase(PointBase start, PointBase end) + { + Start = start; + End = end; + } + + public PointBase Start { get; } + public PointBase End { get; } + + public PointBase GetIntersection(LineBase line) + { + var pt1Dir = new PointBase(End.X - Start.X, End.Y - Start.Y); + var pt2Dir = new PointBase(line.End.X - line.Start.X, line.End.Y - line.Start.Y); + var det = (pt1Dir.X * pt2Dir.Y) - (pt1Dir.Y * pt2Dir.X); + var deltaPt = new PointBase(line.Start.X - Start.X, line.Start.Y - Start.Y); + var alpha = (deltaPt.X * pt2Dir.Y) - (deltaPt.Y * pt2Dir.X); + var beta = (deltaPt.X * pt1Dir.Y) - (deltaPt.Y * pt1Dir.X); + + if (det == 0 || alpha * det < 0 || beta * det < 0) + return PointBase.Empty; + + if (det > 0) + { + if (alpha > det || beta > det) + return PointBase.Empty; + + } + else + { + if (alpha < det || beta < det) + return PointBase.Empty; + } + + return new PointBase(Start.X + (alpha * pt1Dir.X / det), Start.Y + (alpha * pt1Dir.Y / det)); + } + + public override string ToString() => $"Line from {Start} to {End}"; + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/PointBase.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/PointBase.cs new file mode 100644 index 0000000..b985070 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/PointBase.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + [Serializable] + public struct PointBase : IFormattable + { + public static PointBase Zero { get; } = new PointBase(0, 0); + + public PointBase(double x, double y) + { + _x = x; + _y = y; + } + + /// + /// 中间X + /// + internal double _x; + public double X + { + get + { + return _x; + } + + set + { + _x = value; + } + } + + /// + /// 中间Y + /// + internal double _y; + public double Y + { + get + { + return _y; + } + + set + { + _y = value; + } + } + + public double Dot(PointBase other) => X * other.X + Y * other.Y; + + public PointBase Lerp(PointBase other, double t) + => new PointBase(X * (1.0 - t) + other.X * t, Y * (1.0 - t) + other.Y * t); + + // Maybe just make Points mutable? + public PointBase Add(double value) => new PointBase(X + value, Y + value); + public PointBase Add(double x, double y) => new PointBase(X + x, Y + y); + + public PointBase Substract(double value) => new PointBase(X - value, Y - value); + public PointBase Substract(double x, double y) => new PointBase(X - x, Y - y); + + public double DistanceTo(PointBase other) + => Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2)); + + public void Deconstruct(out double x, out double y) + { + x = X; + y = Y; + } + + //public static PointBase operator -(PointBase a, PointBase b) + //{ + // return new PointBase(a.X - b.X, a.Y - b.Y); + //} + //public static PointBase operator +(PointBase a, PointBase b) + //{ + // return new PointBase(a.X + b.X, a.Y + b.Y); + //} + + public static implicit operator PointBase(Point point) + { + return new PointBase(point.X, point.Y); + } + + public static implicit operator Point(PointBase pointInfoBase) + { + return new Point(pointInfoBase.X, pointInfoBase.Y); + } + + //public override string ToString() => $"PointBase(x={X}, y={Y})"; + + #region Statics + + /// + /// Empty - a static property which provides an Empty rectangle. X and Y are positive-infinity + /// and Width and Height are negative infinity. This is the only situation where Width or + /// Height can be negative. + /// + public static PointBase Empty + { + get + { + return s_empty; + } + } + + #endregion Statics + + #region Private Methods + + static private PointBase CreateEmptyRect() + { + PointBase point = new PointBase(); + // We can't set these via the property setters because negatives widths + // are rejected in those APIs. + point._x = Double.PositiveInfinity; + point._y = Double.PositiveInfinity; + return point; + } + + #endregion Private Methods + + + #region Private Fields + + private readonly static PointBase s_empty = CreateEmptyRect(); + + #endregion Private Fields + + #region Public Methods + + + + + /// + /// Compares two PointBase instances for exact equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two PointBase instances are exactly equal, false otherwise + /// + /// The first PointBase to compare + /// The second PointBase to compare + public static bool operator ==(PointBase point1, PointBase point2) + { + return point1.X == point2.X && + point1.Y == point2.Y; + } + + /// + /// Compares two PointBase instances for exact inequality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two PointBase instances are exactly unequal, false otherwise + /// + /// The first PointBase to compare + /// The second PointBase to compare + public static bool operator !=(PointBase point1, PointBase point2) + { + return !(point1 == point2); + } + /// + /// Compares two PointBase instances for object equality. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the two PointBase instances are exactly equal, false otherwise + /// + /// The first PointBase to compare + /// The second PointBase to compare + public static bool Equals(PointBase point1, PointBase point2) + { + return point1.X.Equals(point2.X) && + point1.Y.Equals(point2.Y); + } + + /// + /// Equals - compares this PointBase with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the object is an instance of PointBase and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is PointBase)) + { + return false; + } + + PointBase value = (PointBase)o; + return PointBase.Equals(this, value); + } + + /// + /// Equals - compares this PointBase with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if "value" is equal to "this". + /// + /// The PointBase to compare to "this" + public bool Equals(PointBase value) + { + return PointBase.Equals(this, value); + } + /// + /// Returns the HashCode for this PointBase + /// + /// + /// int - the HashCode for this PointBase + /// + public override int GetHashCode() + { + // Perform field-by-field XOR of HashCodes + return X.GetHashCode() ^ + Y.GetHashCode(); + } + + ///// + ///// Parse - returns an instance converted from the provided string using + ///// the culture "en-US" + ///// string with PointBase data + ///// + //public static PointBase Parse(string source) + //{ + // IFormatProvider formatProvider = System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS; + + // TokenizerHelper th = new TokenizerHelper(source, formatProvider); + + // PointBase value; + + // String firstToken = th.NextTokenRequired(); + + // value = new PointBase( + // Convert.ToDouble(firstToken, formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider)); + + // // There should be no more tokens in this string. + // th.LastTokenRequired(); + + // return value; + //} + + #endregion Public Methods + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + string IFormattable.ToString(string format, IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(format, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + // Helper to get the numeric list separator for a given culture. + char separator = ','; + return String.Format(provider, + "{1:" + format + "}{0}{2:" + format + "}", + separator, + _x, + _y); + } + + + + #endregion Internal Properties + + #region Public Methods + + /// + /// Offset - update the location by adding offsetX to X and offsetY to Y + /// + /// The offset in the x dimension + /// The offset in the y dimension + public void Offset(double offsetX, double offsetY) + { + _x += offsetX; + _y += offsetY; + } + + /// + /// Operator PointBase + Vector + /// + /// + /// PointBase - The result of the addition + /// + /// The PointBase to be added to the Vector + /// The Vectr to be added to the PointBase + public static PointBase operator +(PointBase point, VectorBase vector) + { + return new PointBase(point._x + vector._x, point._y + vector._y); + } + + /// + /// Add: PointBase + Vector + /// + /// + /// PointBase - The result of the addition + /// + /// The PointBase to be added to the Vector + /// The Vector to be added to the PointBase + public static PointBase Add(PointBase point, VectorBase vector) + { + return new PointBase(point._x + vector._x, point._y + vector._y); + } + + /// + /// Operator PointBase - Vector + /// + /// + /// PointBase - The result of the subtraction + /// + /// The PointBase from which the Vector is subtracted + /// The Vector which is subtracted from the PointBase + public static PointBase operator -(PointBase point, VectorBase vector) + { + return new PointBase(point._x - vector._x, point._y - vector._y); + } + + /// + /// Subtract: PointBase - Vector + /// + /// + /// PointBase - The result of the subtraction + /// + /// The PointBase from which the Vector is subtracted + /// The Vector which is subtracted from the PointBase + public static PointBase Subtract(PointBase point, VectorBase vector) + { + return new PointBase(point._x - vector._x, point._y - vector._y); + } + + /// + /// Operator PointBase - PointBase + /// + /// + /// Vector - The result of the subtraction + /// + /// The PointBase from which point2 is subtracted + /// The PointBase subtracted from point1 + public static VectorBase operator -(PointBase point1, PointBase point2) + { + return new VectorBase(point1._x - point2._x, point1._y - point2._y); + } + + /// + /// Subtract: PointBase - PointBase + /// + /// + /// Vector - The result of the subtraction + /// + /// The PointBase from which point2 is subtracted + /// The PointBase subtracted from point1 + public static VectorBase Subtract(PointBase point1, PointBase point2) + { + return new VectorBase(point1._x - point2._x, point1._y - point2._y); + } + + /// + /// Operator PointBase * Matrix + /// + //public static PointBase operator *(PointBase point, Matrix matrix) + //{ + // return matrix.Transform(point); + //} + + /// + /// Multiply: PointBase * Matrix + /// + //public static PointBase Multiply(PointBase point, Matrix matrix) + //{ + // return matrix.Transform(point); + //} + + /// + /// Explicit conversion to Size. Note that since Size cannot contain negative values, + /// the resulting size will contains the absolute values of X and Y + /// + /// + /// Size - A Size equal to this PointBase + /// + /// PointBase - the PointBase to convert to a Size + public static explicit operator SizeBase(PointBase point) + { + return new SizeBase(Math.Abs(point._x), Math.Abs(point._y)); + } + + /// + /// Explicit conversion to Vector + /// + /// + /// Vector - A Vector equal to this PointBase + /// + /// PointBase - the PointBase to convert to a Vector + public static explicit operator VectorBase(PointBase point) + { + return new VectorBase(point._x, point._y); + } + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/RectangleBase.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/RectangleBase.cs new file mode 100644 index 0000000..4b6acec --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/RectangleBase.cs @@ -0,0 +1,1075 @@ +using System; +using System.Collections.Generic; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + [Serializable] + public struct RectangleBase : IShape, IFormattable + { + public static RectangleBase Zero { get; } = new RectangleBase(0, 0, 0, 0); + + public RectangleBase(double left, double top, double right, double bottom, bool lefttoprightbottom) + { + _x = left; + _y = top; + _width = Math.Abs(left - right); + _height = Math.Abs(top - bottom); + } + + public RectangleBase(double x,double y,double width, double height) + { + if (width < 0 || height < 0) + { + throw new System.ArgumentException("Size_WidthAndHeightCannotBeNegative"); + } + + _x = x; + _y = y; + _width = width; + _height = height; + } + + public RectangleBase(PointBase location, SizeBase size) + { + if (size.IsEmpty) + { + this = s_empty; + } + else + { + _x = location._x; + _y = location._y; + _width = size._width; + _height = size._height; + } + } + + public RectangleBase(PointBase point1, PointBase point2) + { + _x = Math.Min(point1._x, point2._x); + _y = Math.Min(point1._y, point2._y); + + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + _width = Math.Max(Math.Max(point1._x, point2._x) - _x, 0); + _height = Math.Max(Math.Max(point1._y, point2._y) - _y, 0); + } + + public RectangleBase(SizeBase size) + { + if (size.IsEmpty) + { + this = s_empty; + } + else + { + _x = _y = 0; + _width = size.Width; + _height = size.Height; + } + } + + public bool Overlap(RectangleBase r) + => Left < r.Right && Right > r.Left && Top < r.Bottom && Bottom > r.Top; + + public bool Intersects(RectangleBase r) + { + var thisX = Left; + var thisY = Top; + var thisW = Width; + var thisH = Height; + var rectX = r.Left; + var rectY = r.Top; + var rectW = r.Width; + var rectH = r.Height; + return rectX < thisX + thisW && thisX < rectX + rectW && rectY < thisY + thisH && thisY < rectY + rectH; + } + + public RectangleBase InflateRectangle(double horizontal, double vertical) + => new RectangleBase(Left - horizontal, Top - vertical, Right + horizontal, Bottom + vertical); + + public RectangleBase UnionRectangle(RectangleBase r) + { + var x1 = Math.Min(Left, r.Left); + var x2 = Math.Max(Left + Width, r.Left + r.Width); + var y1 = Math.Min(Top, r.Top); + var y2 = Math.Max(Top + Height, r.Top + r.Height); + return new RectangleBase(x1, y1, x2, y2); + } + + public bool ContainsPoint(PointBase point) => ContainsPoint(point.X, point.Y); + + public bool ContainsPoint(double x, double y) + => x >= Left && x <= Right && y >= Top && y <= Bottom; + + public IEnumerable GetIntersectionsWithLine(LineBase line) + { + var borders = new[] { + new LineBase(NorthWest, NorthEast), + new LineBase(NorthEast, SouthEast), + new LineBase(SouthWest, SouthEast), + new LineBase(NorthWest, SouthWest) + }; + + for (var i = 0; i < borders.Length; i++) + { + var intersectionPt = borders[i].GetIntersection(line); + if (intersectionPt != null) + yield return intersectionPt; + } + } + + public PointBase Center => new PointBase(Left + Width / 2, Top + Height / 2); + public PointBase NorthEast => new PointBase(Right, Top); + public PointBase SouthEast => new PointBase(Right, Bottom); + public PointBase SouthWest => new PointBase(Left, Bottom); + public PointBase NorthWest => new PointBase(Left, Top); + public PointBase East => new PointBase(Right, Top + Height / 2); + public PointBase North => new PointBase(Left + Width / 2, Top); + public PointBase South => new PointBase(Left + Width / 2, Bottom); + public PointBase West => new PointBase(Left, Top + Height / 2); + + //public bool Equals(Rectangle other) + //{ + // return other != null && Left == other.Left && Right == other.Right && Top == other.Top && + // Bottom == other.Bottom && Width == other.Width && Height == other.Height; + //} + + //public override string ToString() + // => $"Rectangle(width={Width}, height={Height}, top={Top}, right={Right}, bottom={Bottom}, left={Left})"; + + #region Statics + + /// + /// Empty - a static property which provides an Empty rectangle. X and Y are positive-infinity + /// and Width and Height are negative infinity. This is the only situation where Width or + /// Height can be negative. + /// + public static RectangleBase Empty + { + get + { + return s_empty; + } + } + + #endregion Statics + + #region Public Properties + + /// + /// IsEmpty - this returns true if this rect is the Empty rectangle. + /// Note: If width or height are 0 this Rectangle still contains a 0 or 1 dimensional set + /// of points, so this method should not be used to check for 0 area. + /// + public bool IsEmpty + { + get + { + // The funny width and height tests are to handle NaNs + //Debug.Assert((!(_width < 0) && !(_height < 0)) || (this == Empty)); + + return _width < 0; + } + } + + /// + /// Location - The PointBase representing the origin of the Rectangle + /// + public PointBase Location + { + get + { + return new PointBase(_x, _y); + } + set + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotModifyEmptyRect"); + } + + _x = value._x; + _y = value._y; + } + } + + /// + /// Size - The Size representing the area of the Rectangle + /// + public SizeBase Size + { + get + { + if (IsEmpty) + return SizeBase.Empty; + return new SizeBase(_width, _height); + } + set + { + if (value.IsEmpty) + { + this = s_empty; + } + else + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotModifyEmptyRect"); + } + + _width = value._width; + _height = value._height; + } + } + } + + internal double _x; + /// + /// X - The X coordinate of the Location. + /// If this is the empty rectangle, the value will be positive infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double X + { + get + { + return _x; + } + set + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotModifyEmptyRect"); + } + + _x = value; + } + } + + internal double _y; + /// + /// Y - The Y coordinate of the Location + /// If this is the empty rectangle, the value will be positive infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double Y + { + get + { + return _y; + } + set + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotModifyEmptyRect"); + } + + _y = value; + } + } + + internal double _width; + /// + /// Width - The Width component of the Size. This cannot be set to negative, and will only + /// be negative if this is the empty rectangle, in which case it will be negative infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double Width + { + get + { + return _width; + } + set + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotModifyEmptyRect"); + } + + if (value < 0) + { + throw new System.ArgumentException("Size_WidthCannotBeNegative"); + } + + _width = value; + } + } + + internal double _height; + /// + /// Height - The Height component of the Size. This cannot be set to negative, and will only + /// be negative if this is the empty rectangle, in which case it will be negative infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double Height + { + get + { + return _height; + } + set + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotModifyEmptyRect"); + } + + if (value < 0) + { + throw new System.ArgumentException("Size_HeightCannotBeNegative"); + } + + _height = value; + } + } + + /// + /// Left Property - This is a read-only alias for X + /// If this is the empty rectangle, the value will be positive infinity. + /// + public double Left + { + get + { + return _x; + } + } + + /// + /// Top Property - This is a read-only alias for Y + /// If this is the empty rectangle, the value will be positive infinity. + /// + public double Top + { + get + { + return _y; + } + } + + /// + /// Right Property - This is a read-only alias for X + Width + /// If this is the empty rectangle, the value will be negative infinity. + /// + public double Right + { + get + { + if (IsEmpty) + { + return Double.NegativeInfinity; + } + + return _x + _width; + } + } + + /// + /// Bottom Property - This is a read-only alias for Y + Height + /// If this is the empty rectangle, the value will be negative infinity. + /// + public double Bottom + { + get + { + if (IsEmpty) + { + return Double.NegativeInfinity; + } + + return _y + _height; + } + } + + /// + /// TopLeft Property - This is a read-only alias for the PointBase which is at X, Y + /// If this is the empty rectangle, the value will be positive infinity, positive infinity. + /// + public PointBase TopLeft + { + get + { + return new PointBase(Left, Top); + } + } + + /// + /// TopRight Property - This is a read-only alias for the PointBase which is at X + Width, Y + /// If this is the empty rectangle, the value will be negative infinity, positive infinity. + /// + public PointBase TopRight + { + get + { + return new PointBase(Right, Top); + } + } + + /// + /// BottomLeft Property - This is a read-only alias for the PointBase which is at X, Y + Height + /// If this is the empty rectangle, the value will be positive infinity, negative infinity. + /// + public PointBase BottomLeft + { + get + { + return new PointBase(Left, Bottom); + } + } + + /// + /// BottomRight Property - This is a read-only alias for the PointBase which is at X + Width, Y + Height + /// If this is the empty rectangle, the value will be negative infinity, negative infinity. + /// + public PointBase BottomRight + { + get + { + return new PointBase(Right, Bottom); + } + } + #endregion Public Properties + + #region Public Methods + + /// + /// Contains - Returns true if the PointBase is within the rectangle, inclusive of the edges. + /// Returns false otherwise. + /// + /// The point which is being tested + /// + /// Returns true if the PointBase is within the rectangle. + /// Returns false otherwise + /// + public bool Contains(PointBase point) + { + return Contains(point._x, point._y); + } + + /// + /// Contains - Returns true if the PointBase represented by x,y is within the rectangle inclusive of the edges. + /// Returns false otherwise. + /// + /// X coordinate of the point which is being tested + /// Y coordinate of the point which is being tested + /// + /// Returns true if the PointBase represented by x,y is within the rectangle. + /// Returns false otherwise. + /// + public bool Contains(double x, double y) + { + if (IsEmpty) + { + return false; + } + + return ContainsInternal(x, y); + } + + /// + /// Contains - Returns true if the Rectangle non-Empty and is entirely contained within the + /// rectangle, inclusive of the edges. + /// Returns false otherwise + /// + public bool Contains(RectangleBase rect) + { + if (IsEmpty || rect.IsEmpty) + { + return false; + } + + return (_x <= rect._x && + _y <= rect._y && + _x + _width >= rect._x + rect._width && + _y + _height >= rect._y + rect._height); + } + + /// + /// IntersectsWith - Returns true if the Rectangle intersects with this rectangle + /// Returns false otherwise. + /// Note that if one edge is coincident, this is considered an intersection. + /// + /// + /// Returns true if the Rectangle intersects with this rectangle + /// Returns false otherwise. + /// or Height + /// + /// Rectangle + public bool IntersectsWith(RectangleBase rect) + { + if (IsEmpty || rect.IsEmpty) + { + return false; + } + + return (rect.Left <= Right) && + (rect.Right >= Left) && + (rect.Top <= Bottom) && + (rect.Bottom >= Top); + } + + /// + /// Intersect - Update this rectangle to be the intersection of this and rect + /// If either this or rect are Empty, the result is Empty as well. + /// + /// The rect to intersect with this + public void Intersect(RectangleBase rect) + { + if (!this.IntersectsWith(rect)) + { + //this = Empty; + } + else + { + double left = Math.Max(Left, rect.Left); + double top = Math.Max(Top, rect.Top); + + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + _width = Math.Max(Math.Min(Right, rect.Right) - left, 0); + _height = Math.Max(Math.Min(Bottom, rect.Bottom) - top, 0); + + _x = left; + _y = top; + } + } + + /// + /// Intersect - Return the result of the intersection of rect1 and rect2. + /// If either this or rect are Empty, the result is Empty as well. + /// + public static RectangleBase Intersect(RectangleBase rect1, RectangleBase rect2) + { + rect1.Intersect(rect2); + return rect1; + } + + /// + /// Union - Update this rectangle to be the union of this and rect. + /// + public void Union(RectangleBase rect) + { + if (IsEmpty) + { + //this = rect; + } + else if (!rect.IsEmpty) + { + double left = Math.Min(Left, rect.Left); + double top = Math.Min(Top, rect.Top); + + + // We need this check so that the math does not result in NaN + if ((rect.Width == Double.PositiveInfinity) || (Width == Double.PositiveInfinity)) + { + _width = Double.PositiveInfinity; + } + else + { + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + double maxRight = Math.Max(Right, rect.Right); + _width = Math.Max(maxRight - left, 0); + } + + // We need this check so that the math does not result in NaN + if ((rect.Height == Double.PositiveInfinity) || (Height == Double.PositiveInfinity)) + { + _height = Double.PositiveInfinity; + } + else + { + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + double maxBottom = Math.Max(Bottom, rect.Bottom); + _height = Math.Max(maxBottom - top, 0); + } + + _x = left; + _y = top; + } + } + + /// + /// Union - Return the result of the union of rect1 and rect2. + /// + public static RectangleBase Union(RectangleBase rect1, RectangleBase rect2) + { + rect1.Union(rect2); + return rect1; + } + + /// + /// Union - Update this rectangle to be the union of this and point. + /// + public void Union(PointBase point) + { + Union(new RectangleBase(point, point)); + } + + /// + /// Union - Return the result of the union of rect and point. + /// + public static RectangleBase Union(RectangleBase rect, PointBase point) + { + rect.Union(new RectangleBase(point, point)); + return rect; + } + + /// + /// Offset - translate the Location by the offset provided. + /// If this is Empty, this method is illegal. + /// + public void Offset(VectorBase offsetVector) + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotCallMethod"); + } + + _x += offsetVector._x; + _y += offsetVector._y; + } + + /// + /// Offset - translate the Location by the offset provided + /// If this is Empty, this method is illegal. + /// + public void Offset(double offsetX, double offsetY) + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotCallMethod"); + } + + _x += offsetX; + _y += offsetY; + } + + /// + /// Offset - return the result of offsetting rect by the offset provided + /// If this is Empty, this method is illegal. + /// + public static RectangleBase Offset(RectangleBase rect, VectorBase offsetVector) + { + rect.Offset(offsetVector.X, offsetVector.Y); + return rect; + } + + /// + /// Offset - return the result of offsetting rect by the offset provided + /// If this is Empty, this method is illegal. + /// + public static RectangleBase Offset(RectangleBase rect, double offsetX, double offsetY) + { + rect.Offset(offsetX, offsetY); + return rect; + } + + /// + /// Inflate - inflate the bounds by the size provided, in all directions + /// If this is Empty, this method is illegal. + /// + public void Inflate(SizeBase size) + { + Inflate(size._width, size._height); + } + + /// + /// Inflate - inflate the bounds by the size provided, in all directions. + /// If -width is > Width / 2 or -height is > Height / 2, this Rectangle becomes Empty + /// If this is Empty, this method is illegal. + /// + public void Inflate(double width, double height) + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Rect_CannotCallMethod"); + } + + _x -= width; + _y -= height; + + // Do two additions rather than multiplication by 2 to avoid spurious overflow + // That is: (A + 2 * B) != ((A + B) + B) if 2*B overflows. + // Note that multiplication by 2 might work in this case because A should start + // positive & be "clamped" to positive after, but consider A = Inf & B = -MAX. + _width += width; + _width += width; + _height += height; + _height += height; + + // We catch the case of inflation by less than -width/2 or -height/2 here. This also + // maintains the invariant that either the Rectangle is Empty or _width and _height are + // non-negative, even if the user parameters were NaN, though this isn't strictly maintained + // by other methods. + if (!(_width >= 0 && _height >= 0)) + { + this = s_empty; + } + } + + /// + /// Inflate - return the result of inflating rect by the size provided, in all directions + /// If this is Empty, this method is illegal. + /// + public static RectangleBase Inflate(RectangleBase rect, SizeBase size) + { + rect.Inflate(size._width, size._height); + return rect; + } + + /// + /// Inflate - return the result of inflating rect by the size provided, in all directions + /// If this is Empty, this method is illegal. + /// + public static RectangleBase Inflate(RectangleBase rect, double width, double height) + { + rect.Inflate(width, height); + return rect; + } + + /// + /// Returns the bounds of the transformed rectangle. + /// The Empty Rectangle is not affected by this call. + /// + /// + /// The rect which results from the transformation. + /// + /// The Rectangle to transform. + /// The Matrix by which to transform. + //public static Rectangle Transform(Rectangle rect, Matrix matrix) + //{ + // MatrixUtil.TransformRect(ref rect, ref matrix); + // return rect; + //} + + /// + /// Updates rectangle to be the bounds of the original value transformed + /// by the matrix. + /// The Empty Rectangle is not affected by this call. + /// + /// Matrix + //public void Transform(Matrix matrix) + //{ + // MatrixUtil.TransformRect(ref this, ref matrix); + //} + + /// + /// Scale the rectangle in the X and Y directions + /// + /// The scale in X + /// The scale in Y + public void Scale(double scaleX, double scaleY) + { + if (IsEmpty) + { + return; + } + + _x *= scaleX; + _y *= scaleY; + _width *= scaleX; + _height *= scaleY; + + // If the scale in the X dimension is negative, we need to normalize X and Width + if (scaleX < 0) + { + // Make X the left-most edge again + _x += _width; + + // and make Width positive + _width *= -1; + } + + // Do the same for the Y dimension + if (scaleY < 0) + { + // Make Y the top-most edge again + _y += _height; + + // and make Height positive + _height *= -1; + } + } + + #endregion Public Methods + + #region Private Methods + + /// + /// ContainsInternal - Performs just the "point inside" logic + /// + /// + /// bool - true if the point is inside the rect + /// + /// The x-coord of the point to test + /// The y-coord of the point to test + private bool ContainsInternal(double x, double y) + { + // We include points on the edge as "contained". + // We do "x - _width <= _x" instead of "x <= _x + _width" + // so that this check works when _width is PositiveInfinity + // and _x is NegativeInfinity. + return ((x >= _x) && (x - _width <= _x) && + (y >= _y) && (y - _height <= _y)); + } + + static private RectangleBase CreateEmptyRect() + { + RectangleBase rect = new RectangleBase(); + // We can't set these via the property setters because negatives widths + // are rejected in those APIs. + rect._x = Double.PositiveInfinity; + rect._y = Double.PositiveInfinity; + rect._width = Double.NegativeInfinity; + rect._height = Double.NegativeInfinity; + return rect; + } + + #endregion Private Methods + + #region Private Fields + + private readonly static RectangleBase s_empty = CreateEmptyRect(); + + #endregion Private Fields + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + string IFormattable.ToString(string format, IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(format, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + if (IsEmpty) + { + return "Empty"; + } + + // Helper to get the numeric list separator for a given culture. + char separator = ','; + return String.Format(provider, + "{1:" + format + "}{0}{2:" + format + "}{0}{3:" + format + "}{0}{4:" + format + "}", + separator, + _x, + _y, + _width, + _height); + } + + + + #endregion Internal Properties + + #region Public Methods + + + + + /// + /// Compares two Rectangle instances for exact equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Rectangle instances are exactly equal, false otherwise + /// + /// The first Rectangle to compare + /// The second Rectangle to compare + public static bool operator ==(RectangleBase rect1, RectangleBase rect2) + { + return rect1.X == rect2.X && + rect1.Y == rect2.Y && + rect1.Width == rect2.Width && + rect1.Height == rect2.Height; + } + + /// + /// Compares two Rectangle instances for exact inequality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Rectangle instances are exactly unequal, false otherwise + /// + /// The first Rectangle to compare + /// The second Rectangle to compare + public static bool operator !=(RectangleBase rect1, RectangleBase rect2) + { + return !(rect1 == rect2); + } + /// + /// Compares two Rectangle instances for object equality. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the two Rectangle instances are exactly equal, false otherwise + /// + /// The first Rectangle to compare + /// The second Rectangle to compare + public static bool Equals(RectangleBase rect1, RectangleBase rect2) + { + if (rect1.IsEmpty) + { + return rect2.IsEmpty; + } + else + { + return rect1.X.Equals(rect2.X) && + rect1.Y.Equals(rect2.Y) && + rect1.Width.Equals(rect2.Width) && + rect1.Height.Equals(rect2.Height); + } + } + + /// + /// Equals - compares this Rectangle with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the object is an instance of Rectangle and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is RectangleBase)) + { + return false; + } + + RectangleBase value = (RectangleBase)o; + return RectangleBase.Equals(this, value); + } + + /// + /// Equals - compares this Rectangle with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if "value" is equal to "this". + /// + /// The Rectangle to compare to "this" + public bool Equals(RectangleBase value) + { + return RectangleBase.Equals(this, value); + } + /// + /// Returns the HashCode for this Rectangle + /// + /// + /// int - the HashCode for this Rectangle + /// + public override int GetHashCode() + { + if (IsEmpty) + { + return 0; + } + else + { + // Perform field-by-field XOR of HashCodes + return X.GetHashCode() ^ + Y.GetHashCode() ^ + Width.GetHashCode() ^ + Height.GetHashCode(); + } + } + + /// + /// Parse - returns an instance converted from the provided string using + /// the culture "en-US" + /// string with Rectangle data + /// + //public static Rectangle Parse(string source) + //{ + // IFormatProvider formatProvider = System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS; + + // TokenizerHelper th = new TokenizerHelper(source, formatProvider); + + // Rectangle value; + + // String firstToken = th.NextTokenRequired(); + + // // The token will already have had whitespace trimmed so we can do a + // // simple string compare. + // if (firstToken == "Empty") + // { + // value = Empty; + // } + // else + // { + // value = new Rectangle( + // Convert.ToDouble(firstToken, formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider)); + // } + + // // There should be no more tokens in this string. + // th.LastTokenRequired(); + + // return value; + //} + + #endregion Public Methods + + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/Shapes.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/Shapes.cs new file mode 100644 index 0000000..86aa7d0 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/Shapes.cs @@ -0,0 +1,28 @@ +using AIStudio.Wpf.DiagramDesigner.Models; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + public delegate IShape ShapeDefiner(DesignerItemViewModelBase node); + + public static class Shapes + { + public static IShape Rectangle(DesignerItemViewModelBase node) => new RectangleBase(node.Position, node.Size); + + public static IShape Circle(DesignerItemViewModelBase node) + { + var halfWidth = node.Size.Width / 2; + var centerX = node.Position.X + halfWidth; + var centerY = node.Position.Y + node.Size.Height / 2; + return new EllipseBase(centerX, centerY, halfWidth, halfWidth); + } + + public static IShape Ellipse(DesignerItemViewModelBase node) + { + var halfWidth = node.Size.Width / 2; + var halfHeight = node.Size.Height / 2; + var centerX = node.Position.X + halfWidth; + var centerY = node.Position.Y + halfHeight; + return new EllipseBase(centerX, centerY, halfWidth, halfHeight); + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/SizeBase.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/SizeBase.cs new file mode 100644 index 0000000..646e65c --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/SizeBase.cs @@ -0,0 +1,385 @@ +using System; +using System.Windows; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + [Serializable] + public struct SizeBase : IFormattable + { + public static SizeBase Zero { get; } = new SizeBase(0, 0); + + public SizeBase(double width, double height) + { + if (width < 0 || height < 0) + { + throw new System.ArgumentException("Size_WidthAndHeightCannotBeNegative"); + } + + _width = width; + _height = height; + } + + public bool IsEmpty + { + get + { + return _width < 0; + } + } + + internal double _width; + + public double Width + { + get + { + return _width; + } + set + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Size_CannotModifyEmptySize"); + } + + if (value < 0) + { + throw new System.ArgumentException("Size_WidthCannotBeNegative"); + } + + _width = value; + } + } + + internal double _height; + public double Height + { + get + { + return _height; + } + set + { + if (IsEmpty) + { + throw new System.InvalidOperationException("Size_CannotModifyEmptySize"); + } + + if (value < 0) + { + throw new System.ArgumentException("Size_HeightCannotBeNegative"); + } + + _height = value; + } + } + + public SizeBase Add(double value) => new SizeBase(Width + value, Height + value); + + //public bool Equals(Size size) => size != null && Width == size.Width && Height == size.Height; + + //public override string ToString() => $"Size(width={Width}, height={Height})"; + + public static implicit operator SizeBase(Size size) + { + return new SizeBase(size.Width, size.Height); + } + + public static implicit operator Size(SizeBase sizebase) + { + return new Size(sizebase.Width, sizebase.Height); + } + + #region Statics + + /// + /// Empty - a static property which provides an Empty size. Width and Height are + /// negative-infinity. This is the only situation + /// where size can be negative. + /// + public static SizeBase Empty + { + get + { + return s_empty; + } + } + + #endregion Statics + + #region Public Methods + + + + + /// + /// Compares two Size instances for exact equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Size instances are exactly equal, false otherwise + /// + /// The first Size to compare + /// The second Size to compare + public static bool operator ==(SizeBase size1, SizeBase size2) + { + return size1.Width == size2.Width && + size1.Height == size2.Height; + } + + /// + /// Compares two Size instances for exact inequality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Size instances are exactly unequal, false otherwise + /// + /// The first Size to compare + /// The second Size to compare + public static bool operator !=(SizeBase size1, SizeBase size2) + { + return !(size1 == size2); + } + /// + /// Compares two Size instances for object equality. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the two Size instances are exactly equal, false otherwise + /// + /// The first Size to compare + /// The second Size to compare + public static bool Equals(SizeBase size1, SizeBase size2) + { + if (size1.IsEmpty) + { + return size2.IsEmpty; + } + else + { + return size1.Width.Equals(size2.Width) && + size1.Height.Equals(size2.Height); + } + } + + /// + /// Equals - compares this Size with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the object is an instance of Size and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is SizeBase)) + { + return false; + } + + SizeBase value = (SizeBase)o; + return SizeBase.Equals(this, value); + } + + /// + /// Equals - compares this Size with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if "value" is equal to "this". + /// + /// The Size to compare to "this" + public bool Equals(SizeBase value) + { + return SizeBase.Equals(this, value); + } + /// + /// Returns the HashCode for this Size + /// + /// + /// int - the HashCode for this Size + /// + public override int GetHashCode() + { + if (IsEmpty) + { + return 0; + } + else + { + // Perform field-by-field XOR of HashCodes + return Width.GetHashCode() ^ + Height.GetHashCode(); + } + } + + /// + /// Parse - returns an instance converted from the provided string using + /// the culture "en-US" + /// string with Size data + /// + //public static Size Parse(string source) + //{ + // IFormatProvider formatProvider = System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS; + + // TokenizerHelper th = new TokenizerHelper(source, formatProvider); + + // Size value; + + // String firstToken = th.NextTokenRequired(); + + // // The token will already have had whitespace trimmed so we can do a + // // simple string compare. + // if (firstToken == "Empty") + // { + // value = Empty; + // } + // else + // { + // value = new Size( + // Convert.ToDouble(firstToken, formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider)); + // } + + // // There should be no more tokens in this string. + // th.LastTokenRequired(); + + // return value; + //} + + #endregion Public Methods + + #region Public Operators + + /// + /// Explicit conversion to Vector. + /// + /// + /// Vector - A Vector equal to this Size + /// + /// Size - the Size to convert to a Vector + public static explicit operator VectorBase(SizeBase size) + { + return new VectorBase(size._width, size._height); + } + + /// + /// Explicit conversion to PointBase + /// + /// + /// PointBase - A PointBase equal to this Size + /// + /// Size - the Size to convert to a PointBase + public static explicit operator PointBase(SizeBase size) + { + return new PointBase(size._width, size._height); + } + + #endregion Public Operators + + #region Private Methods + + static private SizeBase CreateEmptySize() + { + SizeBase size = new SizeBase(); + // We can't set these via the property setters because negatives widths + // are rejected in those APIs. + size._width = Double.NegativeInfinity; + size._height = Double.NegativeInfinity; + return size; + } + + #endregion Private Methods + + #region Private Fields + + private readonly static SizeBase s_empty = CreateEmptySize(); + + #endregion Private Fields + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + string IFormattable.ToString(string format, IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(format, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + if (IsEmpty) + { + return "Empty"; + } + + // Helper to get the numeric list separator for a given culture. + char separator = ','; + return String.Format(provider, + "{1:" + format + "}{0}{2:" + format + "}", + separator, + _width, + _height); + } + + + + #endregion Internal Properties + } +} \ No newline at end of file diff --git a/AIStudio.Wpf.DiagramDesigner/Geometry/VectorBase.cs b/AIStudio.Wpf.DiagramDesigner/Geometry/VectorBase.cs new file mode 100644 index 0000000..423b628 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Geometry/VectorBase.cs @@ -0,0 +1,618 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AIStudio.Wpf.DiagramDesigner.Geometry +{ + [Serializable] + public struct VectorBase : IFormattable + { + #region Constructors + + /// + /// Constructor which sets the vector's initial values + /// + /// double - The initial X + /// double - THe initial Y + public VectorBase(double x, double y) + { + _x = x; + _y = y; + } + + #endregion Constructors + + #region Public Methods + + /// + /// Length Property - the length of this Vector + /// + public double Length + { + get + { + return Math.Sqrt(_x * _x + _y * _y); + } + } + + /// + /// LengthSquared Property - the squared length of this Vector + /// + public double LengthSquared + { + get + { + return _x * _x + _y * _y; + } + } + + /// + /// Normalize - Updates this Vector to maintain its direction, but to have a length + /// of 1. This is equivalent to dividing this Vector by Length + /// + public void Normalize() + { + // Avoid overflow + this /= Math.Max(Math.Abs(_x), Math.Abs(_y)); + this /= Length; + } + + /// + /// CrossProduct - Returns the cross product: vector1.X*vector2.Y - vector1.Y*vector2.X + /// + /// + /// Returns the cross product: vector1.X*vector2.Y - vector1.Y*vector2.X + /// + /// The first Vector + /// The second Vector + public static double CrossProduct(VectorBase vector1, VectorBase vector2) + { + return vector1._x * vector2._y - vector1._y * vector2._x; + } + + /// + /// AngleBetween - the angle between 2 vectors + /// + /// + /// Returns the the angle in degrees between vector1 and vector2 + /// + /// The first Vector + /// The second Vector + public static double AngleBetween(VectorBase vector1, VectorBase vector2) + { + double sin = vector1._x * vector2._y - vector2._x * vector1._y; + double cos = vector1._x * vector2._x + vector1._y * vector2._y; + + return Math.Atan2(sin, cos) * (180 / Math.PI); + } + + #endregion Public Methods + + #region Public Operators + /// + /// Operator -Vector (unary negation) + /// + public static VectorBase operator -(VectorBase vector) + { + return new VectorBase(-vector._x, -vector._y); + } + + /// + /// Negates the values of X and Y on this Vector + /// + public void Negate() + { + _x = -_x; + _y = -_y; + } + + /// + /// Operator Vector + Vector + /// + public static VectorBase operator +(VectorBase vector1, VectorBase vector2) + { + return new VectorBase(vector1._x + vector2._x, + vector1._y + vector2._y); + } + + /// + /// Add: Vector + Vector + /// + public static VectorBase Add(VectorBase vector1, VectorBase vector2) + { + return new VectorBase(vector1._x + vector2._x, + vector1._y + vector2._y); + } + + /// + /// Operator Vector - Vector + /// + public static VectorBase operator -(VectorBase vector1, VectorBase vector2) + { + return new VectorBase(vector1._x - vector2._x, + vector1._y - vector2._y); + } + + /// + /// Subtract: Vector - Vector + /// + public static VectorBase Subtract(VectorBase vector1, VectorBase vector2) + { + return new VectorBase(vector1._x - vector2._x, + vector1._y - vector2._y); + } + + /// + /// Operator Vector + PointBase + /// + public static PointBase operator +(VectorBase vector, PointBase point) + { + return new PointBase(point._x + vector._x, point._y + vector._y); + } + + /// + /// Add: Vector + PointBase + /// + public static PointBase Add(VectorBase vector, PointBase point) + { + return new PointBase(point._x + vector._x, point._y + vector._y); + } + + /// + /// Operator Vector * double + /// + public static VectorBase operator *(VectorBase vector, double scalar) + { + return new VectorBase(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Multiply: Vector * double + /// + public static VectorBase Multiply(VectorBase vector, double scalar) + { + return new VectorBase(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Operator double * Vector + /// + public static VectorBase operator *(double scalar, VectorBase vector) + { + return new VectorBase(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Multiply: double * Vector + /// + public static VectorBase Multiply(double scalar, VectorBase vector) + { + return new VectorBase(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Operator Vector / double + /// + public static VectorBase operator /(VectorBase vector, double scalar) + { + return vector * (1.0 / scalar); + } + + /// + /// Multiply: Vector / double + /// + public static VectorBase Divide(VectorBase vector, double scalar) + { + return vector * (1.0 / scalar); + } + + /// + /// Operator Vector * Matrix + /// + //public static Vector operator *(Vector vector, Matrix matrix) + //{ + // return matrix.Transform(vector); + //} + + /// + /// Multiply: Vector * Matrix + /// + //public static Vector Multiply(Vector vector, Matrix matrix) + //{ + // return matrix.Transform(vector); + //} + + /// + /// Operator Vector * Vector, interpreted as their dot product + /// + public static double operator *(VectorBase vector1, VectorBase vector2) + { + return vector1._x * vector2._x + vector1._y * vector2._y; + } + + /// + /// Multiply - Returns the dot product: vector1.X*vector2.X + vector1.Y*vector2.Y + /// + /// + /// Returns the dot product: vector1.X*vector2.X + vector1.Y*vector2.Y + /// + /// The first Vector + /// The second Vector + public static double Multiply(VectorBase vector1, VectorBase vector2) + { + return vector1._x * vector2._x + vector1._y * vector2._y; + } + + /// + /// Determinant - Returns the determinant det(vector1, vector2) + /// + /// + /// Returns the determinant: vector1.X*vector2.Y - vector1.Y*vector2.X + /// + /// The first Vector + /// The second Vector + public static double Determinant(VectorBase vector1, VectorBase vector2) + { + return vector1._x * vector2._y - vector1._y * vector2._x; + } + + /// + /// Explicit conversion to Size. Note that since Size cannot contain negative values, + /// the resulting size will contains the absolute values of X and Y + /// + /// + /// Size - A Size equal to this Vector + /// + /// Vector - the Vector to convert to a Size + public static explicit operator SizeBase(VectorBase vector) + { + return new SizeBase(Math.Abs(vector._x), Math.Abs(vector._y)); + } + + /// + /// Explicit conversion to PointBase + /// + /// + /// PointBase - A PointBase equal to this Vector + /// + /// Vector - the Vector to convert to a PointBase + public static explicit operator PointBase(VectorBase vector) + { + return new PointBase(vector._x, vector._y); + } + #endregion Public Operators + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + + + + /// + /// Compares two Vector instances for exact equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Vector instances are exactly equal, false otherwise + /// + /// The first Vector to compare + /// The second Vector to compare + public static bool operator ==(VectorBase vector1, VectorBase vector2) + { + return vector1.X == vector2.X && + vector1.Y == vector2.Y; + } + + /// + /// Compares two Vector instances for exact inequality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Vector instances are exactly unequal, false otherwise + /// + /// The first Vector to compare + /// The second Vector to compare + public static bool operator !=(VectorBase vector1, VectorBase vector2) + { + return !(vector1 == vector2); + } + /// + /// Compares two Vector instances for object equality. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the two Vector instances are exactly equal, false otherwise + /// + /// The first Vector to compare + /// The second Vector to compare + public static bool Equals(VectorBase vector1, VectorBase vector2) + { + return vector1.X.Equals(vector2.X) && + vector1.Y.Equals(vector2.Y); + } + + /// + /// Equals - compares this Vector with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the object is an instance of Vector and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is VectorBase)) + { + return false; + } + + VectorBase value = (VectorBase)o; + return VectorBase.Equals(this, value); + } + + /// + /// Equals - compares this Vector with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if "value" is equal to "this". + /// + /// The Vector to compare to "this" + public bool Equals(VectorBase value) + { + return VectorBase.Equals(this, value); + } + /// + /// Returns the HashCode for this Vector + /// + /// + /// int - the HashCode for this Vector + /// + public override int GetHashCode() + { + // Perform field-by-field XOR of HashCodes + return X.GetHashCode() ^ + Y.GetHashCode(); + } + + /// + /// Parse - returns an instance converted from the provided string using + /// the culture "en-US" + /// string with Vector data + /// + //public static Vector Parse(string source) + //{ + // IFormatProvider formatProvider = System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS; + + // TokenizerHelper th = new TokenizerHelper(source, formatProvider); + + // Vector value; + + // String firstToken = th.NextTokenRequired(); + + // value = new Vector( + // Convert.ToDouble(firstToken, formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider)); + + // // There should be no more tokens in this string. + // th.LastTokenRequired(); + + // return value; + //} + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + + + + #region Public Properties + + /// + /// X - double. Default value is 0. + /// + public double X + { + get + { + return _x; + } + + set + { + _x = value; + } + } + + /// + /// Y - double. Default value is 0. + /// + public double Y + { + get + { + return _y; + } + + set + { + _y = value; + } + } + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + #region Protected Methods + + + + + + #endregion ProtectedMethods + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + + + + + + + + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + string IFormattable.ToString(string format, IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(format, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + // Helper to get the numeric list separator for a given culture. + char separator = ','; + return String.Format(provider, + "{1:" + format + "}{0}{2:" + format + "}", + separator, + _x, + _y); + } + + + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Dependency Properties + // + //------------------------------------------------------ + + #region Dependency Properties + + + + #endregion Dependency Properties + + //------------------------------------------------------ + // + // Internal Fields + // + //------------------------------------------------------ + + #region Internal Fields + + + internal double _x; + internal double _y; + + #endregion Internal Fields + + + + #region Constructors + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + + + + #endregion Constructors + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Helpers/DoubleExtensions.cs b/AIStudio.Wpf.DiagramDesigner/Helpers/DoubleExtensions.cs new file mode 100644 index 0000000..c795dca --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Helpers/DoubleExtensions.cs @@ -0,0 +1,10 @@ +using System; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public static class DoubleExtensions + { + public static bool AlmostEqualTo(this double double1, double double2, double tolerance = 0.0001) + => Math.Abs(double1 - double2) < tolerance; + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Helpers/NumberExtensions.cs b/AIStudio.Wpf.DiagramDesigner/Helpers/NumberExtensions.cs new file mode 100644 index 0000000..ecd119d --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Helpers/NumberExtensions.cs @@ -0,0 +1,9 @@ +using System.Globalization; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public static class NumberExtensions + { + public static string ToInvariantString(this double n) => n.ToString(CultureInfo.InvariantCulture); + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Helpers/PointHelper.cs b/AIStudio.Wpf.DiagramDesigner/Helpers/PointHelper.cs index 40de5ba..f6f1e5d 100644 --- a/AIStudio.Wpf.DiagramDesigner/Helpers/PointHelper.cs +++ b/AIStudio.Wpf.DiagramDesigner/Helpers/PointHelper.cs @@ -1,23 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows; -using System.Windows.Data; -using System.Windows.Media; -using System.Windows.Media.Imaging; - +using AIStudio.Wpf.DiagramDesigner.Geometry; namespace AIStudio.Wpf.DiagramDesigner { public class PointHelper { - public static Point GetPointForConnector(FullyCreatedConnectorInfo connector) + public static PointBase GetPointForConnector(FullyCreatedConnectorInfo connector) { - Point point = new Point(); + PointBase point = new PointBase(); + if (connector == null) + { + return point; + } + if (connector.IsInnerPoint) { - point = new Point(connector.DataItem.Left + connector.DataItem.ItemWidth * connector.XRatio, + point = new PointBase(connector.DataItem.Left + connector.DataItem.ItemWidth * connector.XRatio, connector.DataItem.Top + connector.DataItem.ItemHeight * connector.YRatio); } else @@ -26,16 +23,16 @@ namespace AIStudio.Wpf.DiagramDesigner switch (connector.Orientation) { case ConnectorOrientation.Top: - point = new Point(connector.DataItem.Left + (connector.DataItem.ItemWidth / 2), connector.DataItem.Top); + point = new PointBase(connector.DataItem.Left + (connector.DataItem.ItemWidth / 2), connector.DataItem.Top); break; case ConnectorOrientation.Bottom: - point = new Point(connector.DataItem.Left + (connector.DataItem.ItemWidth / 2), (connector.DataItem.Top + connector.DataItem.ItemHeight)); + point = new PointBase(connector.DataItem.Left + (connector.DataItem.ItemWidth / 2), (connector.DataItem.Top + connector.DataItem.ItemHeight)); break; case ConnectorOrientation.Right: - point = new Point(connector.DataItem.Left + connector.DataItem.ItemWidth, connector.DataItem.Top + (connector.DataItem.ItemHeight / 2)); + point = new PointBase(connector.DataItem.Left + connector.DataItem.ItemWidth, connector.DataItem.Top + (connector.DataItem.ItemHeight / 2)); break; case ConnectorOrientation.Left: - point = new Point(connector.DataItem.Left, connector.DataItem.Top + (connector.DataItem.ItemHeight / 2)); + point = new PointBase(connector.DataItem.Left, connector.DataItem.Top + (connector.DataItem.ItemHeight / 2)); break; } } diff --git a/AIStudio.Wpf.DiagramDesigner/Helpers/ConnectorInfo.cs b/AIStudio.Wpf.DiagramDesigner/Models/ConnectorInfo.cs similarity index 69% rename from AIStudio.Wpf.DiagramDesigner/Helpers/ConnectorInfo.cs rename to AIStudio.Wpf.DiagramDesigner/Models/ConnectorInfo.cs index 5d48c0b..61a93ad 100644 --- a/AIStudio.Wpf.DiagramDesigner/Helpers/ConnectorInfo.cs +++ b/AIStudio.Wpf.DiagramDesigner/Models/ConnectorInfo.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Windows; +using AIStudio.Wpf.DiagramDesigner.Geometry; namespace AIStudio.Wpf.DiagramDesigner { @@ -10,8 +10,8 @@ namespace AIStudio.Wpf.DiagramDesigner { public double DesignerItemLeft { get; set; } public double DesignerItemTop { get; set; } - public Size DesignerItemSize { get; set; } - public Point Position { get; set; } + public SizeBase DesignerItemSize { get; set; } + public PointBase Position { get; set; } public ConnectorOrientation Orientation { get; set; } } } diff --git a/AIStudio.Wpf.DiagramDesigner/Helpers/DragObject.cs b/AIStudio.Wpf.DiagramDesigner/Models/DragObject.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Helpers/DragObject.cs rename to AIStudio.Wpf.DiagramDesigner/Models/DragObject.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Helpers/EventToCommandArgs.cs b/AIStudio.Wpf.DiagramDesigner/Models/EventToCommandArgs.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Helpers/EventToCommandArgs.cs rename to AIStudio.Wpf.DiagramDesigner/Models/EventToCommandArgs.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/ConnectionItem.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/ConnectionItem.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/ConnectionItem.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/ConnectionItem.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/ConnectorItem.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/ConnectorItem.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/ConnectorItem.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/ConnectorItem.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/DesignerItemBase.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/DesignerItemBase.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/DesignerItemBase.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/DesignerItemBase.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/ImageDesignerItem.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/ImageDesignerItem.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/ImageDesignerItem.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/ImageDesignerItem.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/LogicalGateDesignerItemBase.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/LogicalGateDesignerItemBase.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/LogicalGateDesignerItemBase.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/LogicalGateDesignerItemBase.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/MediaDesignerItem.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/MediaDesignerItem.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/MediaDesignerItem.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/MediaDesignerItem.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/SelectableDesignerItemBase.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/SelectableDesignerItemBase.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/SelectableDesignerItemBase.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/SelectableDesignerItemBase.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/SerializableObject.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/SerializableObject.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/SerializableObject.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/SerializableObject.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Models/TextDesignerItem.cs b/AIStudio.Wpf.DiagramDesigner/Models/Serializables/TextDesignerItem.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Models/TextDesignerItem.cs rename to AIStudio.Wpf.DiagramDesigner/Models/Serializables/TextDesignerItem.cs diff --git a/AIStudio.Wpf.DiagramDesigner/Helpers/ToolBoxData.cs b/AIStudio.Wpf.DiagramDesigner/Models/ToolBoxData.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/Helpers/ToolBoxData.cs rename to AIStudio.Wpf.DiagramDesigner/Models/ToolBoxData.cs diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/BoundaryPathFinder.cs b/AIStudio.Wpf.DiagramDesigner/PathFinder/BoundaryPathFinder.cs similarity index 61% rename from AIStudio.Wpf.DiagramDesigner/PathGenerators/BoundaryPathFinder.cs rename to AIStudio.Wpf.DiagramDesigner/PathFinder/BoundaryPathFinder.cs index 365b428..06a54e4 100644 --- a/AIStudio.Wpf.DiagramDesigner/PathGenerators/BoundaryPathFinder.cs +++ b/AIStudio.Wpf.DiagramDesigner/PathFinder/BoundaryPathFinder.cs @@ -1,19 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Windows; +using AIStudio.Wpf.DiagramDesigner.Geometry; namespace AIStudio.Wpf.DiagramDesigner { public class BoundaryPathFinder : IPathFinder { - public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, Point sourceA, Point sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) + public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, PointBase sourceA, PointBase sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) { - List connectionPoints; + List connectionPoints; var isFullConnection = sinkConnectorInfo is FullyCreatedConnectorInfo; - var points = new List() + var points = new List() { sourceA, sourceB @@ -38,34 +37,34 @@ namespace AIStudio.Wpf.DiagramDesigner ((FullyCreatedConnectorInfo)sinkConnectorInfo).DataItem.ItemHeight, points[1]); - connectionPoints = PointInfoBase.ToList(GetConnectionLine(diagramViewModel, sourceInfo, sinkInfo, false, sourceConnectorInfo.IsInnerPoint)); + connectionPoints = ConnectorPoint.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(); + connectionPoints = ConnectorPoint.ToList(GetConnectionLine(diagramViewModel, sourceInfo, points[1], sourceConnectorInfo.Orientation, false, sourceConnectorInfo.IsInnerPoint)); + //EndPoint = new PointBase(); } return connectionPoints; } - public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, Point position) + public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, PointBase position) { return new ConnectorInfo() { Orientation = orientation, - DesignerItemSize = new Size(width, height), + DesignerItemSize = new SizeBase(width, height), DesignerItemLeft = left, DesignerItemTop = top, Position = position }; } - public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) + public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) { - var points = new List(); - var ends = new List { source.Position, sink.Position }; + var points = new List(); + var ends = new List { source.Position, sink.Position }; points.Add(ends[0]); points.AddRange(GetMiddlePoints(source, sink, diagramViewModel.GridCellSize, diagramViewModel.GridMargin, true)); @@ -75,10 +74,10 @@ namespace AIStudio.Wpf.DiagramDesigner return res.ToList(); } - public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, Point sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false) + public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, PointBase sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false) { - var points = new List(); - var ends = new List { source.Position, sinkPoint }; + var points = new List(); + var ends = new List { source.Position, sinkPoint }; points.Add(ends[0]); points.AddRange(GetMiddlePoints(source, new ConnectorInfo() { Orientation = ConnectorOrientation.Top, Position = sinkPoint }, diagramViewModel.GridCellSize, diagramViewModel.GridMargin, false)); @@ -88,9 +87,9 @@ namespace AIStudio.Wpf.DiagramDesigner return res.ToList(); } - private IEnumerable GetMiddlePoints(ConnectorInfo source, ConnectorInfo sink, Size gridCellSize, double gridMargin, bool isFullConnection) + private IEnumerable GetMiddlePoints(ConnectorInfo source, ConnectorInfo sink, SizeBase gridCellSize, double gridMargin, bool isFullConnection) { - var points = new List(); + var points = new List(); if (isFullConnection) { var p0 = GetFirstSegment(source.Orientation, source.Position, gridCellSize, gridMargin); @@ -100,8 +99,8 @@ namespace AIStudio.Wpf.DiagramDesigner return points; - var p2 = new Point(GetNearestCross(p0.X, p1.X), GetNearestCross(p0.Y, p1.Y)); - var p3 = new Point(GetNearestCross(p1.X, p0.X), GetNearestCross(p1.Y, p0.Y)); + var p2 = new PointBase(GetNearestCross(p0.X, p1.X), GetNearestCross(p0.Y, p1.Y)); + var p3 = new PointBase(GetNearestCross(p1.X, p0.X), GetNearestCross(p1.Y, p0.Y)); if (p2 == p3) { points.Add(p0); @@ -113,7 +112,7 @@ namespace AIStudio.Wpf.DiagramDesigner points.Add(p0); points.Add(p2); if (!(Math.Abs(p2.X - p3.X) < 0.0001) && !(Math.Abs(p2.Y - p3.Y) < 0.0001)) - points.Add(new Point(p2.X, p3.Y)); + points.Add(new PointBase(p2.X, p3.Y)); points.Add(p3); points.Add(p1); } @@ -122,18 +121,18 @@ namespace AIStudio.Wpf.DiagramDesigner return points; } - private Point GetFirstSegment(ConnectorOrientation orientation, Point point, Size cellSize, double margin) + private PointBase GetFirstSegment(ConnectorOrientation orientation, PointBase point, SizeBase cellSize, double margin) { double x = (int)((point.X - margin) / cellSize.Width) + 0.5; double y = (int)((point.Y - margin) / cellSize.Height) + 0.5; if (orientation == ConnectorOrientation.Top) - return new Point(x, y - 0.5); + return new PointBase(x, y - 0.5); else if (orientation == ConnectorOrientation.Bottom) - return new Point(x, y + 0.5); + return new PointBase(x, y + 0.5); else if (orientation == ConnectorOrientation.Left) - return new Point(x - 0.5, y); + return new PointBase(x - 0.5, y); else - return new Point(x + 0.5, y); + return new PointBase(x + 0.5, y); } public static double GetNearestCross(double a, double b) @@ -146,25 +145,25 @@ namespace AIStudio.Wpf.DiagramDesigner return Math.Floor(a); } - public static Point SegmentMiddlePoint(Point p1, Point p2) + public static PointBase SegmentMiddlePoint(PointBase p1, PointBase p2) { - return new Point((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2); + return new PointBase((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2); } - private void DoScale(List points, Size cellSize, double margin) + private void DoScale(List points, SizeBase cellSize, double margin) { for (int i = 0; i < points.Count; i++) { - points[i] = new Point(points[i].X * cellSize.Width + margin, + points[i] = new PointBase(points[i].X * cellSize.Width + margin, points[i].Y * cellSize.Height + margin); } } - private void DoShift(Point[] points) + private void DoShift(PointBase[] points) { - double left = new Point[] { points.FirstOrDefault(), points.LastOrDefault() }.Min(p => p.X); - double top = new Point[] { points.FirstOrDefault(), points.LastOrDefault() }.Min(p => p.Y); + double left = new PointBase[] { points.FirstOrDefault(), points.LastOrDefault() }.Min(p => p.X); + double top = new PointBase[] { points.FirstOrDefault(), points.LastOrDefault() }.Min(p => p.Y); for (int i = 0; i < points.Length; i++) { diff --git a/AIStudio.Wpf.DiagramDesigner/PathFinder/IPathFinder.cs b/AIStudio.Wpf.DiagramDesigner/PathFinder/IPathFinder.cs new file mode 100644 index 0000000..c223cb4 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/PathFinder/IPathFinder.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public interface IPathFinder + { + List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, PointBase sourceA, PointBase sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo); + List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false); + List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, PointBase sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false); + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/OrthogonalPathFinder.cs b/AIStudio.Wpf.DiagramDesigner/PathFinder/OrthogonalPathFinder.cs similarity index 75% rename from AIStudio.Wpf.DiagramDesigner/PathGenerators/OrthogonalPathFinder.cs rename to AIStudio.Wpf.DiagramDesigner/PathFinder/OrthogonalPathFinder.cs index bf248a9..4bffd4d 100644 --- a/AIStudio.Wpf.DiagramDesigner/PathGenerators/OrthogonalPathFinder.cs +++ b/AIStudio.Wpf.DiagramDesigner/PathFinder/OrthogonalPathFinder.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Windows; using System.Windows.Controls; +using AIStudio.Wpf.DiagramDesigner.Geometry; namespace AIStudio.Wpf.DiagramDesigner { @@ -15,16 +15,16 @@ namespace AIStudio.Wpf.DiagramDesigner { private const int const_margin = 20; - public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, Point sourceA, Point sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) + public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, PointBase sourceA, PointBase sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) { - List connectionPoints; + List connectionPoints; var isFullConnection = sinkConnectorInfo is FullyCreatedConnectorInfo; - var area = new Rect(sourceA, sourceB); + var area = new RectangleBase(sourceA, sourceB); - var points = new List() + 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) + new PointBase(sourceA.X < sourceB.X ? 0d : area.Width, sourceA.Y < sourceB.Y ? 0d : area.Height ), + new PointBase(sourceA.X > sourceB.X ? 0d : area.Width, sourceA.Y > sourceB.Y ? 0d : area.Height) }; ConnectorInfo sourceInfo = ConnectorInfo(sourceConnectorInfo.Orientation, @@ -45,44 +45,44 @@ namespace AIStudio.Wpf.DiagramDesigner ((FullyCreatedConnectorInfo)sinkConnectorInfo).DataItem.ItemHeight, points[1]); - connectionPoints = PointInfoBase.ToList(GetConnectionLine(diagramViewModel, sourceInfo, sinkInfo, false, sourceConnectorInfo.IsInnerPoint)); + connectionPoints = ConnectorPoint.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(); + connectionPoints = ConnectorPoint.ToList(GetConnectionLine(diagramViewModel, sourceInfo, points[1], sourceConnectorInfo.Orientation, false, sourceConnectorInfo.IsInnerPoint)); + //EndPoint = new PointBase(); } return connectionPoints; } - public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, Point position) + public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, PointBase position) { return new ConnectorInfo() { Orientation = orientation, - DesignerItemSize = new Size(width, height), + DesignerItemSize = new SizeBase(width, height), DesignerItemLeft = left, DesignerItemTop = top, Position = position }; } - public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) + public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) { - List linePoints = new List(); + List linePoints = new List(); int margin1 = sourceInnerPoint ? 0 : const_margin; int margin2 = const_margin; - Rect rectSource = GetRectWithMargin(source, margin1); - Rect rectSink = GetRectWithMargin(sink, margin2); + RectangleBase rectSource = GetRectWithMargin(source, margin1); + RectangleBase rectSink = GetRectWithMargin(sink, margin2); - Point startPoint = GetOffsetPoint(source, rectSource, sourceInnerPoint); - Point endPoint = GetOffsetPoint(sink, rectSink); + PointBase startPoint = GetOffsetPoint(source, rectSource, sourceInnerPoint); + PointBase endPoint = GetOffsetPoint(sink, rectSink); linePoints.Add(startPoint); - Point currentPoint = startPoint; + PointBase currentPoint = startPoint; if (!rectSink.Contains(currentPoint) && !rectSource.Contains(endPoint)) { @@ -90,14 +90,14 @@ namespace AIStudio.Wpf.DiagramDesigner { #region source node - if (IsPointVisible(currentPoint, endPoint, new Rect[] { rectSource, rectSink })) + if (IsPointVisible(currentPoint, endPoint, new RectangleBase[] { rectSource, rectSink })) { linePoints.Add(endPoint); currentPoint = endPoint; break; } - Point neighbour = GetNearestVisibleNeighborSink(currentPoint, endPoint, sink, rectSource, rectSink); + PointBase neighbour = GetNearestVisibleNeighborSink(currentPoint, endPoint, sink, rectSource, rectSink); if (!double.IsNaN(neighbour.X)) { linePoints.Add(neighbour); @@ -109,7 +109,7 @@ namespace AIStudio.Wpf.DiagramDesigner if (currentPoint == startPoint) { bool flag; - Point n = GetNearestNeighborSource(source, endPoint, rectSource, rectSink, out flag, sourceInnerPoint); + PointBase n = GetNearestNeighborSource(source, endPoint, rectSource, rectSink, out flag, sourceInnerPoint); if (linePoints.Contains(n)) { break; @@ -117,9 +117,9 @@ namespace AIStudio.Wpf.DiagramDesigner linePoints.Add(n); currentPoint = n; - if (!IsRectVisible(currentPoint, rectSink, new Rect[] { rectSource })) + if (!IsRectVisible(currentPoint, rectSink, new RectangleBase[] { rectSource })) { - Point n1, n2; + PointBase n1, n2; GetOppositeCorners(source.Orientation, rectSource, out n1, out n2, sourceInnerPoint); if (flag) { @@ -131,7 +131,7 @@ namespace AIStudio.Wpf.DiagramDesigner linePoints.Add(n2); currentPoint = n2; } - if (!IsRectVisible(currentPoint, rectSink, new Rect[] { rectSource })) + if (!IsRectVisible(currentPoint, rectSink, new RectangleBase[] { rectSource })) { if (flag) { @@ -152,13 +152,13 @@ namespace AIStudio.Wpf.DiagramDesigner else // from here on we jump to the sink node { - Point n1, n2; // neighbour corner - Point s1, s2; // opposite corner + PointBase n1, n2; // neighbour corner + PointBase 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 }); + bool n1Visible = IsPointVisible(currentPoint, n1, new RectangleBase[] { rectSource, rectSink }); + bool n2Visible = IsPointVisible(currentPoint, n2, new RectangleBase[] { rectSource, rectSink }); if (n1Visible && n2Visible) { @@ -260,48 +260,48 @@ namespace AIStudio.Wpf.DiagramDesigner linePoints.Add(endPoint); } - linePoints = OptimizeLinePoints(linePoints, new Rect[] { rectSource, rectSink }, source.Orientation, sink.Orientation); + linePoints = OptimizeLinePoints(linePoints, new RectangleBase[] { 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) + public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, PointBase sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false) { - List linePoints = new List(); + List linePoints = new List(); int margin = isInnerPoint ? 0 : const_margin; - Rect rectSource = GetRectWithMargin(source, margin); - Point startPoint = GetOffsetPoint(source, rectSource, isInnerPoint); - Point endPoint = sinkPoint; + RectangleBase rectSource = GetRectWithMargin(source, margin); + PointBase startPoint = GetOffsetPoint(source, rectSource, isInnerPoint); + PointBase endPoint = sinkPoint; linePoints.Add(startPoint); - Point currentPoint = startPoint; + PointBase currentPoint = startPoint; if (!rectSource.Contains(endPoint)) { while (true) { - if (IsPointVisible(currentPoint, endPoint, new Rect[] { rectSource })) + if (IsPointVisible(currentPoint, endPoint, new RectangleBase[] { rectSource })) { linePoints.Add(endPoint); break; } bool sideFlag; - Point n = GetNearestNeighborSource(source, endPoint, rectSource, out sideFlag, isInnerPoint); + PointBase n = GetNearestNeighborSource(source, endPoint, rectSource, out sideFlag, isInnerPoint); linePoints.Add(n); currentPoint = n; - if (IsPointVisible(currentPoint, endPoint, new Rect[] { rectSource })) + if (IsPointVisible(currentPoint, endPoint, new RectangleBase[] { rectSource })) { linePoints.Add(endPoint); break; } else { - Point n1, n2; + PointBase n1, n2; GetOppositeCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if (sideFlag) linePoints.Add(n1); @@ -319,9 +319,9 @@ namespace AIStudio.Wpf.DiagramDesigner } if (preferredOrientation != ConnectorOrientation.None) - linePoints = OptimizeLinePoints(linePoints, new Rect[] { rectSource }, source.Orientation, preferredOrientation); + linePoints = OptimizeLinePoints(linePoints, new RectangleBase[] { rectSource }, source.Orientation, preferredOrientation); else - linePoints = OptimizeLinePoints(linePoints, new Rect[] { rectSource }, source.Orientation, GetOpositeOrientation(source.Orientation)); + linePoints = OptimizeLinePoints(linePoints, new RectangleBase[] { rectSource }, source.Orientation, GetOpositeOrientation(source.Orientation)); if (!showLastLine) { @@ -331,9 +331,9 @@ namespace AIStudio.Wpf.DiagramDesigner return linePoints; } - private static List OptimizeLinePoints(List linePoints, Rect[] rectangles, ConnectorOrientation sourceOrientation, ConnectorOrientation sinkOrientation) + private static List OptimizeLinePoints(List linePoints, RectangleBase[] rectangles, ConnectorOrientation sourceOrientation, ConnectorOrientation sinkOrientation) { - List points = new List(); + List points = new List(); int cut = 0; for (int i = 0; i < linePoints.Count; i++) @@ -377,8 +377,8 @@ namespace AIStudio.Wpf.DiagramDesigner (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)); + points.Insert(j + 1, new PointBase(centerX, points[j].Y)); + points.Insert(j + 2, new PointBase(centerX, points[j + 2].Y)); if (points.Count - 1 > j + 3) points.RemoveAt(j + 3); return points; @@ -388,8 +388,8 @@ namespace AIStudio.Wpf.DiagramDesigner (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)); + points.Insert(j + 1, new PointBase(points[j].X, centerY)); + points.Insert(j + 2, new PointBase(points[j + 2].X, centerY)); if (points.Count - 1 > j + 3) points.RemoveAt(j + 3); return points; @@ -398,14 +398,14 @@ namespace AIStudio.Wpf.DiagramDesigner 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)); + points.Insert(j + 1, new PointBase(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)); + points.Insert(j + 1, new PointBase(points[j].X, points[j + 1].Y)); return points; } } @@ -415,7 +415,7 @@ namespace AIStudio.Wpf.DiagramDesigner return points; } - private static ConnectorOrientation GetOrientation(Point p1, Point p2) + private static ConnectorOrientation GetOrientation(PointBase p1, PointBase p2) { if (p1.X == p2.X) { @@ -455,9 +455,9 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private static Point GetNearestNeighborSource(ConnectorInfo source, Point endPoint, Rect rectSource, Rect rectSink, out bool flag, bool isInnerPoint) + private static PointBase GetNearestNeighborSource(ConnectorInfo source, PointBase endPoint, RectangleBase rectSource, RectangleBase rectSink, out bool flag, bool isInnerPoint) { - Point n1, n2; // neighbors + PointBase n1, n2; // neighbors GetNeighborCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if (rectSink.Contains(n1)) @@ -484,9 +484,9 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private static Point GetNearestNeighborSource(ConnectorInfo source, Point endPoint, Rect rectSource, out bool flag, bool isInnerPoint) + private static PointBase GetNearestNeighborSource(ConnectorInfo source, PointBase endPoint, RectangleBase rectSource, out bool flag, bool isInnerPoint) { - Point n1, n2; // neighbors + PointBase n1, n2; // neighbors GetNeighborCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if ((Distance(n1, endPoint) <= Distance(n2, endPoint))) @@ -501,13 +501,13 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private static Point GetNearestVisibleNeighborSink(Point currentPoint, Point endPoint, ConnectorInfo sink, Rect rectSource, Rect rectSink) + private static PointBase GetNearestVisibleNeighborSink(PointBase currentPoint, PointBase endPoint, ConnectorInfo sink, RectangleBase rectSource, RectangleBase rectSink) { - Point s1, s2; // neighbors on sink side + PointBase 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 }); + bool flag1 = IsPointVisible(currentPoint, s1, new RectangleBase[] { rectSource, rectSink }); + bool flag2 = IsPointVisible(currentPoint, s2, new RectangleBase[] { rectSource, rectSink }); if (flag1) // s1 visible { @@ -538,14 +538,14 @@ namespace AIStudio.Wpf.DiagramDesigner } else // s1 and s2 not visible { - return new Point(double.NaN, double.NaN); + return new PointBase(double.NaN, double.NaN); } } } - private static bool IsPointVisible(Point fromPoint, Point targetPoint, Rect[] rectangles) + private static bool IsPointVisible(PointBase fromPoint, PointBase targetPoint, RectangleBase[] rectangles) { - foreach (Rect rect in rectangles) + foreach (RectangleBase rect in rectangles) { if (RectangleIntersectsLine(rect, fromPoint, targetPoint)) return false; @@ -553,7 +553,7 @@ namespace AIStudio.Wpf.DiagramDesigner return true; } - private static bool IsRectVisible(Point fromPoint, Rect targetRect, Rect[] rectangles) + private static bool IsRectVisible(PointBase fromPoint, RectangleBase targetRect, RectangleBase[] rectangles) { if (IsPointVisible(fromPoint, targetRect.TopLeft, rectangles)) return true; @@ -570,13 +570,13 @@ namespace AIStudio.Wpf.DiagramDesigner return false; } - private static bool RectangleIntersectsLine(Rect rect, Point startPoint, Point endPoint) + private static bool RectangleIntersectsLine(RectangleBase rect, PointBase startPoint, PointBase endPoint) { rect.Inflate(-1, -1); - return rect.IntersectsWith(new Rect(startPoint, endPoint)); + return rect.IntersectsWith(new RectangleBase(startPoint, endPoint)); } - private static void GetOppositeCorners(ConnectorOrientation orientation, Rect rect, out Point n1, out Point n2, bool isInnerPoint = false) + private static void GetOppositeCorners(ConnectorOrientation orientation, RectangleBase rect, out PointBase n1, out PointBase n2, bool isInnerPoint = false) { if (isInnerPoint) { @@ -602,7 +602,7 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private static void GetNeighborCorners(ConnectorOrientation orientation, Rect rect, out Point n1, out Point n2, bool isInnerPoint = false) + private static void GetNeighborCorners(ConnectorOrientation orientation, RectangleBase rect, out PointBase n1, out PointBase n2, bool isInnerPoint = false) { if (isInnerPoint) { @@ -628,14 +628,14 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private static double Distance(Point p1, Point p2) + private static double Distance(PointBase p1, PointBase p2) { - return Point.Subtract(p1, p2).Length; + return PointBase.Subtract(p1, p2).Length; } - private static Rect GetRectWithMargin(ConnectorInfo connectorThumb, double margin) + private static RectangleBase GetRectWithMargin(ConnectorInfo connectorThumb, double margin) { - Rect rect = new Rect(connectorThumb.DesignerItemLeft, + RectangleBase rect = new RectangleBase(connectorThumb.DesignerItemLeft, connectorThumb.DesignerItemTop, 0, 0); @@ -645,28 +645,28 @@ namespace AIStudio.Wpf.DiagramDesigner return rect; } - private static Point GetOffsetPoint(ConnectorInfo connector, Rect rect, bool isInnerPoint = false) + private static PointBase GetOffsetPoint(ConnectorInfo connector, RectangleBase rect, bool isInnerPoint = false) { - Point offsetPoint = new Point(); + PointBase offsetPoint = new PointBase(); if (isInnerPoint) { - offsetPoint = new Point(connector.Position.X, connector.Position.Y); + offsetPoint = new PointBase(connector.Position.X, connector.Position.Y); return offsetPoint; } switch (connector.Orientation) { case ConnectorOrientation.Left: - offsetPoint = new Point(rect.Left, connector.Position.Y); + offsetPoint = new PointBase(rect.Left, connector.Position.Y); break; case ConnectorOrientation.Top: - offsetPoint = new Point(connector.Position.X, rect.Top); + offsetPoint = new PointBase(connector.Position.X, rect.Top); break; case ConnectorOrientation.Right: - offsetPoint = new Point(rect.Right, connector.Position.Y); + offsetPoint = new PointBase(rect.Right, connector.Position.Y); break; case ConnectorOrientation.Bottom: - offsetPoint = new Point(connector.Position.X, rect.Bottom); + offsetPoint = new PointBase(connector.Position.X, rect.Bottom); break; default: break; @@ -675,48 +675,48 @@ namespace AIStudio.Wpf.DiagramDesigner return offsetPoint; } - private static void CheckPathEnd(ConnectorInfo source, ConnectorInfo sink, bool showLastLine, List linePoints, bool sourceInnerPoint) + 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); + PointBase startPoint = new PointBase(0, 0); + PointBase endPoint = new PointBase(0, 0); double marginPath = 15; switch (source.Orientation) { case ConnectorOrientation.Left: - startPoint = new Point(source.Position.X - marginPath, source.Position.Y); + startPoint = new PointBase(source.Position.X - marginPath, source.Position.Y); break; case ConnectorOrientation.Top: - startPoint = new Point(source.Position.X, source.Position.Y - marginPath); + startPoint = new PointBase(source.Position.X, source.Position.Y - marginPath); break; case ConnectorOrientation.Right: - startPoint = new Point(source.Position.X + marginPath, source.Position.Y); + startPoint = new PointBase(source.Position.X + marginPath, source.Position.Y); break; case ConnectorOrientation.Bottom: - startPoint = new Point(source.Position.X, source.Position.Y + marginPath); + startPoint = new PointBase(source.Position.X, source.Position.Y + marginPath); break; default: break; } if (sourceInnerPoint) { - startPoint = new Point(source.Position.X, source.Position.Y); + startPoint = new PointBase(source.Position.X, source.Position.Y); } switch (sink.Orientation) { case ConnectorOrientation.Left: - endPoint = new Point(sink.Position.X - marginPath, sink.Position.Y); + endPoint = new PointBase(sink.Position.X - marginPath, sink.Position.Y); break; case ConnectorOrientation.Top: - endPoint = new Point(sink.Position.X, sink.Position.Y - marginPath); + endPoint = new PointBase(sink.Position.X, sink.Position.Y - marginPath); break; case ConnectorOrientation.Right: - endPoint = new Point(sink.Position.X + marginPath, sink.Position.Y); + endPoint = new PointBase(sink.Position.X + marginPath, sink.Position.Y); break; case ConnectorOrientation.Bottom: - endPoint = new Point(sink.Position.X, sink.Position.Y + marginPath); + endPoint = new PointBase(sink.Position.X, sink.Position.Y + marginPath); break; default: break; diff --git a/AIStudio.Wpf.DiagramDesigner/PathFinder/StraightLinePathFinder.cs b/AIStudio.Wpf.DiagramDesigner/PathFinder/StraightLinePathFinder.cs new file mode 100644 index 0000000..8fb4f77 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/PathFinder/StraightLinePathFinder.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public class StraightLinePathFinder : IPathFinder + { + public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, PointBase sourceA, PointBase sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) + { + var area = new RectangleBase(sourceA, sourceB); + var connectionPoints = ConnectorPoint.ToList(new List() + { + + new PointBase(sourceA.X < sourceB.X ? 0d : area.Width, sourceA.Y < sourceB.Y ? 0d : area.Height ), + new PointBase(sourceA.X > sourceB.X ? 0d : area.Width, sourceA.Y > sourceB.Y ? 0d : area.Height) + }); + + return connectionPoints; + } + + public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) + { + throw new NotImplementedException(); + } + + public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, PointBase sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false) + { + throw new NotImplementedException(); + } + + + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/IPathFinder.cs b/AIStudio.Wpf.DiagramDesigner/PathGenerators/IPathFinder.cs deleted file mode 100644 index 3635d4b..0000000 --- a/AIStudio.Wpf.DiagramDesigner/PathGenerators/IPathFinder.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows; - -namespace AIStudio.Wpf.DiagramDesigner -{ - public interface IPathFinder - { - List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, Point sourceA, Point sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo); - List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false); - List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, Point sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false); - } -} diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGeneratorResult.cs b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGeneratorResult.cs new file mode 100644 index 0000000..c110bca --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGeneratorResult.cs @@ -0,0 +1,23 @@ +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public class PathGeneratorResult + { + public PathGeneratorResult(string[] paths, double? sourceMarkerAngle = null, PointBase? sourceMarkerPosition = null, + double? targetMarkerAngle = null, PointBase? targetMarkerPosition = null) + { + Paths = paths; + SourceMarkerAngle = sourceMarkerAngle; + SourceMarkerPosition = sourceMarkerPosition; + TargetMarkerAngle = targetMarkerAngle; + TargetMarkerPosition = targetMarkerPosition; + } + + public string[] Paths { get; } + public double? SourceMarkerAngle { get; } + public PointBase? SourceMarkerPosition { get; } + public double? TargetMarkerAngle { get; } + public PointBase? TargetMarkerPosition { get; } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Smooth.cs b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Smooth.cs new file mode 100644 index 0000000..ea214a2 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Smooth.cs @@ -0,0 +1,92 @@ +using System; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public static partial class PathGenerators + { + private const double _margin = 125; + + public static PathGeneratorResult Smooth(IDiagramViewModel _, ConnectorViewModel link, PointBase[] route, PointBase source, PointBase target) + { + route = ConcatRouteAndSourceAndTarget(route, source, target); + + if (route.Length > 2) + return CurveThroughPoints(route, link); + + route = GetRouteWithCurvePoints(link, route); + double? sourceAngle = null; + double? targetAngle = null; + + sourceAngle = SourceMarkerAdjustement(route, (double)link.ColorViewModel.LeftArrowSizeStyle); + targetAngle = TargetMarkerAdjustement(route, (double)link.ColorViewModel.RightArrowPathStyle); + + var path = FormattableString.Invariant($"M {route[0].X} {route[0].Y} C {route[1].X} {route[1].Y}, {route[2].X} {route[2].Y}, {route[3].X} {route[3].Y}"); + return new PathGeneratorResult(new[] { path }, sourceAngle, route[0], targetAngle, route[route.Length - 1]); + } + + private static PathGeneratorResult CurveThroughPoints(PointBase[] route, ConnectorViewModel link) + { + double? sourceAngle = null; + double? targetAngle = null; + + sourceAngle = SourceMarkerAdjustement(route, (double)link.ColorViewModel.LeftArrowSizeStyle); + targetAngle = TargetMarkerAdjustement(route, (double)link.ColorViewModel.RightArrowPathStyle); + + Geometry.BezierSpline.GetCurveControlPoints(route, out var firstControlPoints, out var secondControlPoints); + var paths = new string[firstControlPoints.Length]; + + for (var i = 0; i < firstControlPoints.Length; i++) + { + var cp1 = firstControlPoints[i]; + var cp2 = secondControlPoints[i]; + paths[i] = FormattableString.Invariant($"M {route[i].X} {route[i].Y} C {cp1.X} {cp1.Y}, {cp2.X} {cp2.Y}, {route[i + 1].X} {route[i + 1].Y}"); + } + + // Todo: adjust marker positions based on closest control points + return new PathGeneratorResult(paths, sourceAngle, route[0], targetAngle, route[route.Length - 1]); + } + + private static PointBase[] GetRouteWithCurvePoints(ConnectorViewModel link, PointBase[] route) + { + if (link.IsPortless) + { + if (Math.Abs(route[0].X - route[1].X) >= Math.Abs(route[0].Y - route[1].Y)) + { + var cX = (route[0].X + route[1].X) / 2; + return new[] { route[0], new PointBase(cX, route[0].Y), new PointBase(cX, route[1].Y), route[1] }; + } + else + { + var cY = (route[0].Y + route[1].Y) / 2; + return new[] { route[0], new PointBase(route[0].X, cY), new PointBase(route[1].X, cY), route[1] }; + } + } + else + { + var cX = (route[0].X + route[1].X) / 2; + var cY = (route[0].Y + route[1].Y) / 2; + var curvePointA = GetCurvePoint(route[0].X, route[0].Y, cX, cY, link.SourceConnectorInfo?.Orientation); + var curvePointB = GetCurvePoint(route[1].X, route[1].Y, cX, cY, link.SinkConnectorInfo?.Orientation); + return new[] { route[0], curvePointA, curvePointB, route[1] }; + } + } + + private static PointBase GetCurvePoint(double pX, double pY, double cX, double cY, ConnectorOrientation? alignment) + { + var margin = Math.Min(_margin, Math.Pow(Math.Pow(pX - cX, 2) + Math.Pow(pY - cY, 2), .5)); + switch (alignment) + { + case ConnectorOrientation.Top: return new PointBase(pX, Math.Min(pY - margin, cY)); + case ConnectorOrientation.Bottom: return new PointBase(pX, Math.Max(pY + margin, cY)); + case ConnectorOrientation.TopRight: return new PointBase(Math.Max(pX + margin, cX), Math.Min(pY - margin, cY)); + case ConnectorOrientation.BottomRight: return new PointBase(Math.Max(pX + margin, cX), Math.Max(pY + margin, cY)); + case ConnectorOrientation.Right: return new PointBase(Math.Max(pX + margin, cX), pY); + case ConnectorOrientation.Left: return new PointBase(Math.Min(pX - margin, cX), pY); + case ConnectorOrientation.BottomLeft: return new PointBase(Math.Min(pX - margin, cX), Math.Max(pY + margin, cY)); + case ConnectorOrientation.TopLeft: return new PointBase(Math.Min(pX - margin, cX), Math.Min(pY - margin, cY)); + default: return new PointBase(cX, cY); + }; + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Straight.cs b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Straight.cs new file mode 100644 index 0000000..bba45d4 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Straight.cs @@ -0,0 +1,26 @@ +using System; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public static partial class PathGenerators + { + public static PathGeneratorResult Straight(IDiagramViewModel _, ConnectorViewModel link, PointBase[] route, PointBase source, PointBase target) + { + route = ConcatRouteAndSourceAndTarget(route, source, target); + double? sourceAngle = null; + double? targetAngle = null; + + sourceAngle = SourceMarkerAdjustement(route, (double)link.ColorViewModel.LeftArrowSizeStyle); + targetAngle = TargetMarkerAdjustement(route, (double)link.ColorViewModel.RightArrowPathStyle); + + var paths = new string[route.Length - 1]; + for (var i = 0; i < route.Length - 1; i++) + { + paths[i] = FormattableString.Invariant($"M {route[i].X} {route[i].Y} L {route[i + 1].X} {route[i + 1].Y}"); + } + + return new PathGeneratorResult(paths, sourceAngle, route[0], targetAngle, route[route.Length - 1]); + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Utils.cs b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Utils.cs new file mode 100644 index 0000000..84425f1 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/PathGenerators/PathGenerators.Utils.cs @@ -0,0 +1,35 @@ +using System; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public static partial class PathGenerators + { + public static double SourceMarkerAdjustement(PointBase[] route, double markerWidth) + { + var angleInRadians = Math.Atan2(route[1].Y - route[0].Y, route[1].X - route[0].X) + Math.PI; + var xChange = markerWidth * Math.Cos(angleInRadians); + var yChange = markerWidth * Math.Sin(angleInRadians); + route[0] = new PointBase(route[0].X - xChange, route[0].Y - yChange); + return angleInRadians * 180 / Math.PI; + } + + public static double TargetMarkerAdjustement(PointBase[] route, double markerWidth) + { + var angleInRadians = Math.Atan2(route[route.Length - 1].Y - route[route.Length - 2].Y, route[route.Length - 1].X - route[route.Length - 2].X); + var xChange = markerWidth * Math.Cos(angleInRadians); + var yChange = markerWidth * Math.Sin(angleInRadians); + route[route.Length - 1] = new PointBase(route[route.Length - 1].X - xChange, route[route.Length - 1].Y - yChange); + return angleInRadians * 180 / Math.PI; + } + + public static PointBase[] ConcatRouteAndSourceAndTarget(PointBase[] route, PointBase source, PointBase target) + { + var result = new PointBase[route.Length + 2]; + result[0] = source; + route.CopyTo(result, 1); + result[route.Length - 1] = target; + return result; + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/PathGenerators/StraightLinePathFinder.cs b/AIStudio.Wpf.DiagramDesigner/PathGenerators/StraightLinePathFinder.cs deleted file mode 100644 index 4476c06..0000000 --- a/AIStudio.Wpf.DiagramDesigner/PathGenerators/StraightLinePathFinder.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Windows; - -namespace AIStudio.Wpf.DiagramDesigner -{ - public class StraightLinePathFinder : IPathFinder - { - - - public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, Point sourceA, Point sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) - { - var area = new Rect(sourceA, sourceB); - var connectionPoints = PointInfoBase.ToList(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) - }); - - return connectionPoints; - } - - public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) - { - throw new NotImplementedException(); - } - - public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, Point sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false) - { - throw new NotImplementedException(); - } - - - } -} diff --git a/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Normal.cs b/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Normal.cs new file mode 100644 index 0000000..d229f9d --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Normal.cs @@ -0,0 +1,13 @@ +using System.Linq; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public static partial class Routers + { + public static PointBase[] Normal(IDiagramViewModel _, ConnectorViewModel link) + { + return link.Vertices.Select(v => (PointBase)v).ToArray(); + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Orthogonal.cs b/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Orthogonal.cs new file mode 100644 index 0000000..de531c5 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Orthogonal.cs @@ -0,0 +1,586 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +// Implementation taken from the JS version: https://gist.github.com/menendezpoo/4a8894c152383b9d7a870c24a04447e4 +// Todo: Make it more c#, Benchmark A* vs Dijkstra, Add more options +namespace AIStudio.Wpf.DiagramDesigner +{ + public static partial class Routers + { + public static PointBase[] Orthogonal(IDiagramViewModel _, ConnectorViewModel link) + { + if (link.IsPortless) + throw new Exception("Orthogonal router doesn't work with portless links yet"); + + if (link.IsFullConnection == false) + return Normal(_, link); + + var shapeMargin = 10; + var globalBoundsMargin = 50; + var spots = new List(); + var verticals = new List(); + var horizontals = new List(); + var sideA = link.SourceConnectorInfo.Orientation; + var sideAVertical = IsVerticalSide(sideA); + var sideB = link.SinkConnectorInfo.Orientation; + var sideBVertical = IsVerticalSide(sideB); + var originA = GetPortPositionBasedOnAlignment(link.SourceConnectorInfo); + var originB = GetPortPositionBasedOnAlignment(link.SinkConnectorInfo); + var shapeA = link.SourceConnectorInfo.DataItem.GetBounds(includePorts: true); + var shapeB = link.SinkConnectorInfoFully.DataItem.GetBounds(includePorts: true); + var inflatedA = shapeA.InflateRectangle(shapeMargin, shapeMargin); + var inflatedB = shapeB.InflateRectangle(shapeMargin, shapeMargin); + + if (inflatedA.Intersects(inflatedB)) + { + shapeMargin = 0; + inflatedA = shapeA; + inflatedB = shapeB; + } + + // Curated bounds to stick to + var bounds = inflatedA.UnionRectangle(inflatedB).InflateRectangle(globalBoundsMargin, globalBoundsMargin); + + // Add edges to rulers + verticals.Add(inflatedA.Left); + verticals.Add(inflatedA.Right); + horizontals.Add(inflatedA.Top); + horizontals.Add(inflatedA.Bottom); + verticals.Add(inflatedB.Left); + verticals.Add(inflatedB.Right); + horizontals.Add(inflatedB.Top); + horizontals.Add(inflatedB.Bottom); + + // Rulers at origins of shapes + (sideAVertical ? verticals : horizontals).Add(sideAVertical ? originA.X : originA.Y); + (sideBVertical ? verticals : horizontals).Add(sideBVertical ? originB.X : originB.Y); + + // Points of shape antennas + spots.Add(GetOriginSpot(originA, sideA, shapeMargin)); + spots.Add(GetOriginSpot(originB, sideB, shapeMargin)); + + // Sort rulers + verticals.Sort(); + horizontals.Sort(); + + // Create grid + var grid = RulersToGrid(verticals, horizontals, bounds); + var gridPoints = GridToSpots(grid, new[] { inflatedA, inflatedB }); + + // Add to spots + spots.AddRange(gridPoints); + + // Create graph + var graph = CreateGraph(spots); + + // Origin and destination by extruding antennas + var origin = ExtrudeCp(originA, shapeMargin, sideA); + var destination = ExtrudeCp(originB, shapeMargin, sideB); + + var path = ShortestPath(graph, origin, destination); + if (path.Length > 0) + { + return SimplifyPath(path); + } + else + { + return Normal(_, link); + } + } + + private static PointBase GetOriginSpot(PointBase p, ConnectorOrientation alignment, double shapeMargin) + { + switch (alignment) + { + case ConnectorOrientation.Top: return p.Add(0, -shapeMargin); + case ConnectorOrientation.Right: return p.Add(shapeMargin, 0); + case ConnectorOrientation.Bottom: return p.Add(0, shapeMargin); + case ConnectorOrientation.Left: return p.Add(-shapeMargin, 0); + default: + throw new NotImplementedException(); + } + } + + private static bool IsVerticalSide(ConnectorOrientation alignment) + => alignment == ConnectorOrientation.Top || alignment == ConnectorOrientation.Bottom; // Add others + + private static Grid RulersToGrid(List verticals, List horizontals, RectangleBase bounds) + { + var result = new Grid(); + verticals.Sort(); + horizontals.Sort(); + + var lastX = bounds.Left; + var lastY = bounds.Top; + var column = 0; + var row = 0; + + foreach (var y in horizontals) + { + foreach (var x in verticals) + { + result.Set(row, column++, new RectangleBase(lastX, lastY, x, y)); + lastX = x; + } + + // Last cell of the row + result.Set(row, column, new RectangleBase(lastX, lastY, bounds.Right, y)); + lastX = bounds.Left; + lastY = y; + column = 0; + row++; + } + + lastX = bounds.Left; + + // Last fow of cells + foreach (var x in verticals) + { + result.Set(row, column++, new RectangleBase(lastX, lastY, x, bounds.Bottom)); + lastX = x; + } + + // Last cell of last row + result.Set(row, column, new RectangleBase(lastX, lastY, bounds.Right, bounds.Bottom)); + return result; + } + + private static List GridToSpots(Grid grid, RectangleBase[] obstacles) + { + bool obstacleCollision(PointBase p) => obstacles.Where(o => o.ContainsPoint(p)).Any(); + + var gridPoints = new List(); + foreach (var keyValuePair in grid.Data) + { + var row = keyValuePair.Key; + var data = keyValuePair.Value; + + var firstRow = row == 0; + var lastRow = row == grid.Rows - 1; + + foreach (var keyValuePair2 in data) + { + var col = keyValuePair2.Key; + var r = keyValuePair2.Value; + + var firstCol = col == 0; + var lastCol = col == grid.Columns - 1; + var nw = firstCol && firstRow; + var ne = firstRow && lastCol; + var se = lastRow && lastCol; + var sw = lastRow && firstCol; + + if (nw || ne || se || sw) + { + gridPoints.Add(r.NorthWest); + gridPoints.Add(r.NorthEast); + gridPoints.Add(r.SouthWest); + gridPoints.Add(r.SouthEast); + } + else if (firstRow) + { + gridPoints.Add(r.NorthWest); + gridPoints.Add(r.North); + gridPoints.Add(r.NorthEast); + } + else if (lastRow) + { + gridPoints.Add(r.SouthEast); + gridPoints.Add(r.South); + gridPoints.Add(r.SouthWest); + } + else if (firstCol) + { + gridPoints.Add(r.NorthWest); + gridPoints.Add(r.West); + gridPoints.Add(r.SouthWest); + } + else if (lastCol) + { + gridPoints.Add(r.NorthEast); + gridPoints.Add(r.East); + gridPoints.Add(r.SouthEast); + } + else + { + gridPoints.Add(r.NorthWest); + gridPoints.Add(r.North); + gridPoints.Add(r.NorthEast); + gridPoints.Add(r.East); + gridPoints.Add(r.SouthEast); + gridPoints.Add(r.South); + gridPoints.Add(r.SouthWest); + gridPoints.Add(r.West); + gridPoints.Add(r.Center); + } + } + } + + // Reduce repeated points and filter out those who touch shapes + return ReducePoints(gridPoints).Where(p => !obstacleCollision(p)).ToList(); + } + + private static IEnumerable ReducePoints(List points) + { + var map = new Dictionary>(); + foreach (var p in points) + { + (var x, var y) = p; + if (!map.ContainsKey(y)) map.Add(y, new List()); + var arr = map[y]; + + if (!arr.Contains(x)) arr.Add(x); + } + + foreach (var keyValuePair in map) + { + var y = keyValuePair.Key; + var xs = keyValuePair.Value; + + foreach (var x in xs) + { + yield return new PointBase(x, y); + } + } + } + + private static PointGraph CreateGraph(List spots) + { + var hotXs = new List(); + var hotYs = new List(); + var graph = new PointGraph(); + + spots.ForEach(p => { + (var x, var y) = p; + if (!hotXs.Contains(x)) hotXs.Add(x); + if (!hotYs.Contains(y)) hotYs.Add(y); + graph.Add(p); + }); + + hotXs.Sort(); + hotYs.Sort(); + + for (var i = 0; i < hotYs.Count; i++) + { + for (var j = 0; j < hotXs.Count; j++) + { + var b = new PointBase(hotXs[j], hotYs[i]); + if (!graph.Has(b)) continue; + + if (j > 0) + { + var a = new PointBase(hotXs[j - 1], hotYs[i]); + + if (graph.Has(a)) + { + graph.Connect(a, b); + graph.Connect(b, a); + } + } + + if (i > 0) + { + var a = new PointBase(hotXs[j], hotYs[i - 1]); + + if (graph.Has(a)) + { + graph.Connect(a, b); + graph.Connect(b, a); + } + } + } + } + + return graph; + } + + private static PointBase ExtrudeCp(PointBase p, double margin, ConnectorOrientation alignment) + { + switch (alignment) + { + case ConnectorOrientation.Top: return p.Add(0, -margin); + case ConnectorOrientation.Right: return p.Add(margin, 0); + case ConnectorOrientation.Bottom: return p.Add(0, margin); + case ConnectorOrientation.Left: return p.Add(-margin, 0); + default: throw new NotImplementedException(); + } + } + + private static PointBase[] ShortestPath(PointGraph graph, PointBase origin, PointBase destination) + { + var originNode = graph.Get(origin); + var destinationNode = graph.Get(destination); + + if (originNode == null || destinationNode == null) + throw new Exception("Origin node or Destination node not found"); + + graph.CalculateShortestPathFromSource(graph, originNode); + return destinationNode.ShortestPath.Select(n => n.Data).ToArray(); + } + + private static PointBase[] SimplifyPath(PointBase[] points) + { + if (points.Length <= 2) + { + return points; + } + + var r = new List() { points[0] }; + for (var i = 1; i < points.Length; i++) + { + var cur = points[i]; + if (i == (points.Length - 1)) + { + r.Add(cur); + break; + } + + var prev = points[i - 1]; + var next = points[i + 1]; + var bend = GetBend(prev, cur, next); + + if (bend != "none") + { + r.Add(cur); + } + } + + return r.ToArray(); + } + + private static string GetBend(PointBase a, PointBase b, PointBase c) + { + var equalX = a.X == b.X && b.X == c.X; + var equalY = a.Y == b.Y && b.Y == c.Y; + var segment1Horizontal = a.Y == b.Y; + var segment1Vertical = a.X == b.X; + var segment2Horizontal = b.Y == c.Y; + var segment2Vertical = b.X == c.X; + + if (equalX || equalY) + { + return "none"; + } + + if ( + !(segment1Vertical || segment1Horizontal) || + !(segment2Vertical || segment2Horizontal) + ) + { + return "unknown"; + } + + if (segment1Horizontal && segment2Vertical) + { + return c.Y > b.Y ? "s" : "n"; + + } + else if (segment1Vertical && segment2Horizontal) + { + return c.X > b.X ? "e" : "w"; + } + + throw new Exception("Nope"); + } + + class Grid + { + public Grid() + { + Data = new Dictionary>(); + } + + public Dictionary> Data + { + get; + } + public double Rows + { + get; private set; + } + public double Columns + { + get; private set; + } + + public void Set(double row, double column, RectangleBase rectangle) + { + Rows = Math.Max(Rows, row + 1); + Columns = Math.Max(Columns, column + 1); + + if (!Data.ContainsKey(row)) + { + Data.Add(row, new Dictionary()); + } + + Data[row].Add(column, rectangle); + } + + public RectangleBase Get(double row, double column) + { + if (!Data.ContainsKey(row)) + return RectangleBase.Empty; + + if (!Data[row].ContainsKey(column)) + return RectangleBase.Empty; + + return Data[row][column]; + } + + public RectangleBase[] Rectangles() => Data.SelectMany(r => r.Value.Values).ToArray(); + } + + class PointGraph + { + public readonly Dictionary> _index = new Dictionary>(); + + public void Add(PointBase p) + { + (var x, var y) = p; + var xs = x.ToInvariantString(); + var ys = y.ToInvariantString(); + + if (!_index.ContainsKey(xs)) + _index.Add(xs, new Dictionary()); + + if (!_index[xs].ContainsKey(ys)) + _index[xs].Add(ys, new PointNode(p)); + } + + private PointNode GetLowestDistanceNode(HashSet unsettledNodes) + { + PointNode lowestDistanceNode = null; + var lowestDistance = double.MaxValue; + foreach (var node in unsettledNodes) + { + var nodeDistance = node.Distance; + if (nodeDistance < lowestDistance) + { + lowestDistance = nodeDistance; + lowestDistanceNode = node; + } + } + + return lowestDistanceNode; + } + + public PointGraph CalculateShortestPathFromSource(PointGraph graph, PointNode source) + { + source.Distance = 0; + var settledNodes = new HashSet(); + var unsettledNodes = new HashSet + { + source + }; + + while (unsettledNodes.Count != 0) + { + var currentNode = GetLowestDistanceNode(unsettledNodes); + unsettledNodes.Remove(currentNode); + + foreach (var keyValuePair in currentNode.AdjacentNodes) + { + var adjacentNode = keyValuePair.Key; + var edgeWeight = keyValuePair.Value; + if (!settledNodes.Contains(adjacentNode)) + { + CalculateMinimumDistance(adjacentNode, edgeWeight, currentNode); + unsettledNodes.Add(adjacentNode); + } + + } + settledNodes.Add(currentNode); + } + + return graph; + } + + private void CalculateMinimumDistance(PointNode evaluationNode, double edgeWeight, PointNode sourceNode) + { + var sourceDistance = sourceNode.Distance; + var comingDirection = InferPathDirection(sourceNode); + var goingDirection = DirectionOfNodes(sourceNode, evaluationNode); + var changingDirection = comingDirection != null && goingDirection != null && comingDirection != goingDirection; + var extraWeigh = changingDirection ? Math.Pow(edgeWeight + 1, 2) : 0; + + if (sourceDistance + edgeWeight + extraWeigh < evaluationNode.Distance) + { + evaluationNode.Distance = sourceDistance + edgeWeight + extraWeigh; + var shortestPath = new List(); + shortestPath.AddRange(sourceNode.ShortestPath); + shortestPath.Add(sourceNode); + evaluationNode.ShortestPath = shortestPath; + } + } + + private char? DirectionOf(PointBase a, PointBase b) + { + if (a.X == b.X) return 'h'; + else if (a.Y == b.Y) return 'v'; + return null; + } + + private char? DirectionOfNodes(PointNode a, PointNode b) => DirectionOf(a.Data, b.Data); + + private char? InferPathDirection(PointNode node) + { + if (node.ShortestPath.Count == 0) + return null; + + return DirectionOfNodes(node.ShortestPath[node.ShortestPath.Count - 1], node); + } + + public void Connect(PointBase a, PointBase b) + { + var nodeA = Get(a); + var nodeB = Get(b); + + if (nodeA == null || nodeB == null) + return; + + nodeA.AdjacentNodes.Add(nodeB, a.DistanceTo(b)); + } + + public bool Has(PointBase p) + { + (var x, var y) = p; + var xs = x.ToInvariantString(); + var ys = y.ToInvariantString(); + return _index.ContainsKey(xs) && _index[xs].ContainsKey(ys); + } + + public PointNode Get(PointBase p) + { + (var x, var y) = p; + var xs = x.ToInvariantString(); + var ys = y.ToInvariantString(); + + if (_index.ContainsKey(xs) && _index[xs].ContainsKey(ys)) + return _index[xs][ys]; + + return null; + } + } + + class PointNode + { + public PointNode(PointBase data) + { + Data = data; + } + + public PointBase Data + { + get; + } + public double Distance { get; set; } = double.MaxValue; + public List ShortestPath { get; set; } = new List(); + public Dictionary AdjacentNodes { get; set; } = new Dictionary(); + } + } + + +} diff --git a/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Utils.cs b/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Utils.cs new file mode 100644 index 0000000..ca0acae --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Routers/Routers.Utils.cs @@ -0,0 +1,32 @@ +using AIStudio.Wpf.DiagramDesigner.Geometry; +using AIStudio.Wpf.DiagramDesigner.Models; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public static partial class Routers + { + public static PointBase GetPortPositionBasedOnAlignment(ConnectorInfoBase port) + { + var pt = port.Position; + switch (port.Orientation) + { + case ConnectorOrientation.Top: + return new PointBase(pt.X + port.ConnectorWidth / 2, pt.Y); + case ConnectorOrientation.TopRight: + return new PointBase(pt.X + port.ConnectorWidth, pt.Y); + case ConnectorOrientation.Right: + return new PointBase(pt.X + port.ConnectorWidth, pt.Y + port.ConnectorHeight / 2); + case ConnectorOrientation.BottomRight: + return new PointBase(pt.X + port.ConnectorWidth, pt.Y + port.ConnectorHeight); + case ConnectorOrientation.Bottom: + return new PointBase(pt.X + port.ConnectorWidth / 2, pt.Y + port.ConnectorHeight); + case ConnectorOrientation.BottomLeft: + return new PointBase(pt.X, pt.Y + port.ConnectorHeight); + case ConnectorOrientation.Left: + return new PointBase(pt.X, pt.Y + port.ConnectorHeight / 2); + default: + return pt; + } + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml b/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml index 593166f..b3a9b3d 100644 --- a/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml +++ b/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml @@ -540,12 +540,12 @@ - - + + /// 中间X + /// private double _x; public double X { @@ -48,6 +50,9 @@ namespace AIStudio.Wpf.DiagramDesigner } } + /// + /// 中间Y + /// private double _y; public double Y { @@ -64,6 +69,9 @@ namespace AIStudio.Wpf.DiagramDesigner } } + /// + /// 边界Left + /// public double Left { get @@ -72,6 +80,9 @@ namespace AIStudio.Wpf.DiagramDesigner } } + /// + /// 边界Top + /// public double Top { get @@ -80,6 +91,15 @@ namespace AIStudio.Wpf.DiagramDesigner } } + public PointBase Position + { + get + { + return new PointBase(Left, Top); + } + } + public PointBase MiddlePosition => new PointBase(Left + ConnectorWidth / 2, Top + ConnectorHeight / 2); + private double connectorWidth = 8; public double ConnectorWidth { @@ -105,53 +125,32 @@ namespace AIStudio.Wpf.DiagramDesigner { SetProperty(ref _colorViewModel, value); } + } + + public static ConnectorPoint operator -(ConnectorPoint a, ConnectorPoint b) + { + return new ConnectorPoint(a.X - b.X, a.Y - b.Y); + } + public static ConnectorPoint operator +(ConnectorPoint a, ConnectorPoint b) + { + return new ConnectorPoint(a.X + b.X, a.Y + b.Y); } - public double Dot(PointInfoBase other) => X * other.X + Y * other.Y; - - public PointInfoBase Lerp(PointInfoBase other, double t) - => new PointInfoBase(X * (1.0 - t) + other.X * t, Y * (1.0 - t) + other.Y * t); - - // Maybe just make Points mutable? - public PointInfoBase Add(double value) => new PointInfoBase(X + value, Y + value); - public PointInfoBase Add(double x, double y) => new PointInfoBase(X + x, Y + y); - - public PointInfoBase Substract(double value) => new PointInfoBase(X - value, Y - value); - public PointInfoBase Substract(double x, double y) => new PointInfoBase(X - x, Y - y); - - public double DistanceTo(PointInfoBase other) - => Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2)); - - public void Deconstruct(out double x, out double y) + public static implicit operator ConnectorPoint(PointBase point) { - x = X; - y = Y; - } - - public static PointInfoBase operator -(PointInfoBase a, PointInfoBase b) - { - return new Point(a.X - b.X, a.Y - b.Y); - } - public static PointInfoBase operator +(PointInfoBase a, PointInfoBase b) - { - return new Point(a.X + b.X, a.Y + b.Y); + return new ConnectorPoint(point); } - public static implicit operator PointInfoBase(Point point) + public static implicit operator PointBase(ConnectorPoint pointInfoBase) { - return new PointInfoBase(point); + return new PointBase(pointInfoBase.X, pointInfoBase.Y); } - public static implicit operator Point(PointInfoBase pointInfoBase) + public static List ToList(List lst) { - return new Point(pointInfoBase.X, pointInfoBase.Y); + return lst.Select(p => (ConnectorPoint)p).ToList(); } - public static List ToList(List lst) - { - return lst.Select(p => (PointInfoBase)p).ToList(); - } - - public override string ToString() => $"PointInfoBase(x={X}, y={Y})"; + public override string ToString() => $"ConnectorPoint(x={X}, y={Y})"; } } diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/ConnectorViewModel.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/ConnectorViewModel.cs similarity index 73% rename from AIStudio.Wpf.DiagramDesigner/ViewModels/ConnectorViewModel.cs rename to AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/ConnectorViewModel.cs index 59da4c6..963fde4 100644 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/ConnectorViewModel.cs +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/ConnectorViewModel.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Windows; +//using System.Windows; using System.Windows.Media; +using AIStudio.Wpf.DiagramDesigner.Geometry; using AIStudio.Wpf.DiagramDesigner.Helpers; namespace AIStudio.Wpf.DiagramDesigner @@ -32,7 +33,7 @@ namespace AIStudio.Wpf.DiagramDesigner public override SelectableDesignerItemBase ToXmlObject() { - if (SinkConnectorInfo is FullyCreatedConnectorInfo sinkConnector) + if (IsFullConnection) { ConnectionItem connection = new ConnectionItem( SourceConnectorInfo.DataItem.Id, @@ -41,12 +42,12 @@ namespace AIStudio.Wpf.DiagramDesigner GetXRatioFromConnector(SourceConnectorInfo), GetYRatioFromConnector(SourceConnectorInfo), SourceConnectorInfo.IsInnerPoint, - sinkConnector.DataItem.Id, - sinkConnector.Orientation, - sinkConnector.DataItem.GetType(), - GetXRatioFromConnector(sinkConnector), - GetYRatioFromConnector(sinkConnector), - sinkConnector.IsInnerPoint, + SinkConnectorInfoFully.DataItem.Id, + SinkConnectorInfoFully.Orientation, + SinkConnectorInfoFully.DataItem.GetType(), + GetXRatioFromConnector(SinkConnectorInfoFully), + GetYRatioFromConnector(SinkConnectorInfoFully), + SinkConnectorInfoFully.IsInnerPoint, this); return connection; @@ -67,16 +68,8 @@ namespace AIStudio.Wpf.DiagramDesigner get; set; } - public bool IsFullConnection - { - get - { - return _sinkConnectorInfo is FullyCreatedConnectorInfo; - } - } - - private Point _sourceA; - public Point SourceA + private PointBase _sourceA; + public PointBase SourceA { get { @@ -91,8 +84,8 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private Point _sourceB; - public Point SourceB + private PointBase _sourceB; + public PointBase SourceB { get { @@ -107,8 +100,8 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private List _connectionPoints; - public List ConnectionPoints + private List _connectionPoints; + public List ConnectionPoints { get { @@ -128,8 +121,8 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private Point _startPoint; - public Point StartPoint + private PointBase _startPoint; + public PointBase StartPoint { get { @@ -141,8 +134,8 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private Point _endPoint; - public Point EndPoint + private PointBase _endPoint; + public PointBase EndPoint { get { @@ -154,8 +147,8 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private Rect _area; - public Rect Area + private RectangleBase _area; + public RectangleBase Area { get { @@ -163,7 +156,7 @@ namespace AIStudio.Wpf.DiagramDesigner } private set { - Rect oldarea = _area; + RectangleBase oldarea = _area; if (SetProperty(ref _area, value)) { UpdateConnectionPoints(); @@ -177,6 +170,10 @@ namespace AIStudio.Wpf.DiagramDesigner get; set; } + //待完善这两处 + public List Vertices { get; } = new List(); + public List Labels { get; set; } = new List(); + public virtual Dictionary PropertiesSetting { get @@ -188,13 +185,13 @@ namespace AIStudio.Wpf.DiagramDesigner } } - public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, Point position) + public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, PointBase position) { return new ConnectorInfo() { Orientation = orientation, - DesignerItemSize = new Size(width, height), + DesignerItemSize = new SizeBase(width, height), DesignerItemLeft = left, DesignerItemTop = top, Position = position @@ -237,12 +234,46 @@ namespace AIStudio.Wpf.DiagramDesigner } else { - SourceB = ((PartCreatedConnectionInfo)SinkConnectorInfo).CurrentLocation; + SourceB = SinkConnectorInfoPart.Position; } } } } + public FullyCreatedConnectorInfo SinkConnectorInfoFully + { + get + { + return SinkConnectorInfo as FullyCreatedConnectorInfo; + } + } + + public PartCreatedConnectionInfo SinkConnectorInfoPart + { + get + { + return SinkConnectorInfo as PartCreatedConnectionInfo; + } + } + + public ConnectorPoint OnGoingPosition + { + get + { + return SinkConnectorInfoPart?.Position; + } + } + + public bool IsFullConnection + { + get + { + return SinkConnectorInfoFully != null; + } + } + + public bool IsPortless => SourceConnectorInfo?.DataItem?.Connectors?.Count() == 0; + public double GetXRatioFromConnector(FullyCreatedConnectorInfo info) { if (info.IsInnerPoint) @@ -291,7 +322,7 @@ namespace AIStudio.Wpf.DiagramDesigner private void UpdateArea() { - Area = new Rect(SourceA, SourceB); + Area = new RectangleBase(SourceA, SourceB); } private void UpdateConnectionPoints() @@ -300,6 +331,9 @@ namespace AIStudio.Wpf.DiagramDesigner StartPoint = ConnectionPoints.First(); EndPoint = ConnectionPoints.Last(); + //var router = Routers.Normal(Parent, this); + //var pathGenerator = PathGenerators.Smooth(Parent, this, router, SourceA, SourceB); + } private void ConnectorViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) @@ -311,9 +345,9 @@ namespace AIStudio.Wpf.DiagramDesigner case "Left": case "Top": SourceA = PointHelper.GetPointForConnector(this.SourceConnectorInfo); - if (this.SinkConnectorInfo is FullyCreatedConnectorInfo) + if (IsFullConnection) { - SourceB = PointHelper.GetPointForConnector((FullyCreatedConnectorInfo)this.SinkConnectorInfo); + SourceB = PointHelper.GetPointForConnector(this.SinkConnectorInfoFully); } break; @@ -365,6 +399,7 @@ namespace AIStudio.Wpf.DiagramDesigner { get; set; } + private void DeleteConnection(object args) { if (this.Parent is IDiagramViewModel) @@ -414,17 +449,46 @@ namespace AIStudio.Wpf.DiagramDesigner } } - public void OutTextItemLocation(Rect oldArea, Rect newArea) + public void OutTextItemLocation(RectangleBase oldArea, RectangleBase newArea) { if (this.OutTextItem is TextDesignerItemViewModel text) { - var oldpoint = new Point(oldArea.Left + oldArea.Width / 2, oldArea.Top + oldArea.Height / 2); - var newpoint = new Point(newArea.Left + newArea.Width / 2, newArea.Top + newArea.Height / 2); + var oldpoint = new PointBase(oldArea.Left + oldArea.Width / 2, oldArea.Top + oldArea.Height / 2); + var newpoint = new PointBase(newArea.Left + newArea.Width / 2, newArea.Top + newArea.Height / 2); text.Left = text.Left + newpoint.X - oldpoint.X; text.Top = text.Top + newpoint.Y - oldpoint.Y; } } + //private (PointInfoBase source, PointInfoBase target) FindConnectionPoints(PointInfoBase[] route) + //{ + // if (IsPortless) // Portless + // { + // if (SourceConnectorInfo.DataItem == null || (IsFullConnection && SinkConnectorInfoFully.DataItem == null)) + // return (null, null); + + // var sourceCenter = SourceConnectorInfo.DataItem.GetBounds().Center; + // var targetCenter = SinkConnectorInfoFully?.DataItem?.GetBounds().Center ?? SinkConnectorInfoFully.Position; + // var firstPt = route.Length > 0 ? route[0] : targetCenter; + // var secondPt = route.Length > 0 ? route[0] : sourceCenter; + // var sourceLine = new Line(firstPt, sourceCenter); + // var targetLine = new Line(secondPt, targetCenter); + // var sourceIntersections = Link.SourceNode.GetShape().GetIntersectionsWithLine(sourceLine); + // var targetIntersections = Link.TargetNode.GetShape().GetIntersectionsWithLine(targetLine); + // var sourceIntersection = GetClosestPointTo(sourceIntersections, firstPt); + // var targetIntersection = GetClosestPointTo(targetIntersections, secondPt); + // return (sourceIntersection ?? sourceCenter, targetIntersection ?? targetCenter); + // } + // else + // { + // if (!Link.SourcePort.Initialized || Link.TargetPort?.Initialized == false) + // return (null, null); + + // var source = GetPortPositionBasedOnAlignment(Link.SourcePort, Link.SourceMarker); + // var target = GetPortPositionBasedOnAlignment(Link.TargetPort, Link.TargetMarker); + // return (source, target ?? Link.OnGoingPosition); + // } + //} } } diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs index b047dde..8ed0c05 100644 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DesignerItemViewModelBase.cs @@ -7,6 +7,7 @@ using System.Windows; using System.Linq; using System.Reactive.Linq; using AIStudio.Wpf.DiagramDesigner.Models; +using AIStudio.Wpf.DiagramDesigner.Geometry; namespace AIStudio.Wpf.DiagramDesigner { @@ -85,6 +86,10 @@ namespace AIStudio.Wpf.DiagramDesigner get { return (connectors != null && connectors.Count >= 4) ? connectors[3] : null; } } + public ShapeDefiner ShapeDefiner + { + get; + } private string _icon; [CanDo] @@ -133,16 +138,16 @@ namespace AIStudio.Wpf.DiagramDesigner } [CanDo] - public Point ItemWidthHeight + public SizeBase Size { get { - return new Point(ItemWidth, ItemHeight); + return new SizeBase(ItemWidth, ItemHeight); } set { - ItemWidth = value.X; - ItemHeight = value.Y; + ItemWidth = value.Width; + ItemHeight = value.Height; } } @@ -211,14 +216,22 @@ namespace AIStudio.Wpf.DiagramDesigner { SetProperty(ref _top, value); } - } + } - [CanDo] - public Point TopLeft + public PointBase Position { get { - return new Point(Left, Top); + return new PointBase(Left, Top); + } + } + + [CanDo] + public PointBase TopLeft + { + get + { + return new PointBase(Left, Top); } set { @@ -386,17 +399,37 @@ namespace AIStudio.Wpf.DiagramDesigner public void RaiseTopLeft() { - this.RaisePropertyChanged(nameof(TopLeft), new Point(GetOldValue(nameof(Left)), GetOldValue(nameof(Top))), TopLeft); + this.RaisePropertyChanged(nameof(TopLeft), new PointBase(GetOldValue(nameof(Left)), GetOldValue(nameof(Top))), TopLeft); } public void RaiseItemWidthHeight() { - this.RaisePropertyChanged(nameof(ItemWidthHeight), new Point(GetOldValue(nameof(ItemWidth)), GetOldValue(nameof(ItemHeight))), ItemWidthHeight); + this.RaisePropertyChanged(nameof(Size), new SizeBase(GetOldValue(nameof(ItemWidth)), GetOldValue(nameof(ItemHeight))), Size); } public void RaiseAngle() { this.RaisePropertyChanged(nameof(Angle), GetOldValue(nameof(Angle)), Angle); } + + public RectangleBase GetBounds(bool includePorts = false) + { + if (!includePorts) + return new RectangleBase(Left, Top, ItemWidth, ItemHeight); + + var leftPort = LeftConnector; + var topPort = TopConnector; + var rightPort = RightConnector; + var bottomPort = BottomConnector; + + var left = leftPort == null ? Left: Math.Min(Left, leftPort.Position.X); + var top = topPort == null ? Top : Math.Min(Left, topPort.Position.Y); + var right = rightPort == null ? Left + ItemWidth : + Math.Max(rightPort.Position.X + rightPort.ConnectorWidth, Left + ItemWidth); + var bottom = bottomPort == null ? Top + ItemHeight : + Math.Max(bottomPort.Position.Y + bottomPort.ConnectorHeight, Top + ItemHeight); + + return new RectangleBase(left, top, right, bottom); + } } } diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/DiagramViewModel.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DiagramViewModel.cs similarity index 98% rename from AIStudio.Wpf.DiagramDesigner/ViewModels/DiagramViewModel.cs rename to AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DiagramViewModel.cs index e691b77..c15e13f 100644 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/DiagramViewModel.cs +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DiagramViewModel.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Windows; using System.Windows.Input; using System.Windows.Media; +using AIStudio.Wpf.DiagramDesigner.Geometry; using AIStudio.Wpf.DiagramDesigner.Helpers; using AIStudio.Wpf.DiagramDesigner.Models; using Newtonsoft.Json; @@ -324,9 +325,9 @@ namespace AIStudio.Wpf.DiagramDesigner } } - private Point _currentPoint; + private System.Windows.Point _currentPoint; [Browsable(false)] - public Point CurrentPoint + public System.Windows.Point CurrentPoint { get { @@ -1393,7 +1394,7 @@ namespace AIStudio.Wpf.DiagramDesigner select item).FirstOrDefault(); DesignerItemViewModelBase sinkItem = (from item in selectedDesignerItems - where item.Id == ((connection.SinkConnectorInfo as FullyCreatedConnectorInfo).DataItem).Id + where item.Id == connection.SinkConnectorInfoFully?.DataItem?.Id select item).FirstOrDefault(); if (sourceItem != null && @@ -1423,15 +1424,15 @@ namespace AIStudio.Wpf.DiagramDesigner OffsetX = 10; OffsetY = 10; - Clipboard.Clear(); - Clipboard.SetData(DataFormats.Serializable, json); + System.Windows.Clipboard.Clear(); + System.Windows.Clipboard.SetData(System.Windows.DataFormats.Serializable, json); } private void ExecutePasteCommand(object parameter) { - if (Clipboard.ContainsData(DataFormats.Serializable)) + if (System.Windows.Clipboard.ContainsData(System.Windows.DataFormats.Serializable)) { - String clipboardData = Clipboard.GetData(DataFormats.Serializable) as String; + String clipboardData = System.Windows.Clipboard.GetData(System.Windows.DataFormats.Serializable) as String; if (String.IsNullOrEmpty(clipboardData)) return; @@ -1512,7 +1513,7 @@ namespace AIStudio.Wpf.DiagramDesigner } catch (Exception e) { - MessageBox.Show(e.StackTrace, e.Message, MessageBoxButton.OK, MessageBoxImage.Error); + System.Windows.MessageBox.Show(e.StackTrace, e.Message, System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); } } } @@ -1683,7 +1684,7 @@ namespace AIStudio.Wpf.DiagramDesigner where item.ParentId == Guid.Empty select item; - Rect rect = GetBoundingRectangle(items); + RectangleBase rect = GetBoundingRectangle(items); GroupDesignerItemViewModel groupItem = new GroupDesignerItemViewModel(); groupItem.IsGroup = true; @@ -1752,7 +1753,7 @@ namespace AIStudio.Wpf.DiagramDesigner } } - public Rect GetBoundingRectangle(IEnumerable items) + public RectangleBase GetBoundingRectangle(IEnumerable items) { double x1 = Double.MaxValue; double y1 = Double.MaxValue; @@ -1768,7 +1769,7 @@ namespace AIStudio.Wpf.DiagramDesigner y2 = Math.Max(item.Top + item.ItemHeight, y2); } - return new Rect(new Point(x1, y1), new Point(x2, y2)); + return new RectangleBase(new PointBase(x1, y1), new PointBase(x2, y2)); } #region 用于wpf大小与物理像素之间转换 diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/FullyCreatedConnectorInfo.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/FullyCreatedConnectorInfo.cs similarity index 94% rename from AIStudio.Wpf.DiagramDesigner/ViewModels/FullyCreatedConnectorInfo.cs rename to AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/FullyCreatedConnectorInfo.cs index 95e3233..93ec2af 100644 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/FullyCreatedConnectorInfo.cs +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/FullyCreatedConnectorInfo.cs @@ -3,11 +3,19 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; +using AIStudio.Wpf.DiagramDesigner.Geometry; namespace AIStudio.Wpf.DiagramDesigner { public class FullyCreatedConnectorInfo : ConnectorInfoBase { + public override PointBase Position + { + get + { + return PointHelper.GetPointForConnector(this); + } + } private List menuOptions; diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LinkLabelModel.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LinkLabelModel.cs new file mode 100644 index 0000000..a8c6563 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LinkLabelModel.cs @@ -0,0 +1,32 @@ +namespace AIStudio.Wpf.DiagramDesigner +{ + public class LinkLabelModel : ConnectorPoint + { + public LinkLabelModel(ConnectorViewModel parent, string id, string content, double? distance = null, ConnectorPoint offset = null) + { + Parent = parent; + Content = content; + Distance = distance; + Offset = offset; + } + + public LinkLabelModel(ConnectorViewModel parent, string content, double? distance = null, ConnectorPoint offset = null) + { + Parent = parent; + Content = content; + Distance = distance; + Offset = offset; + } + + public ConnectorViewModel Parent { get; } + public string Content { get; set; } + /// + /// 3 types of values are possible: + /// - A number between 0 and 1: Position relative to the link's length + /// - A positive number, greater than 1: Position away from the start + /// - A negative number, less than 0: Position away from the end + /// + public double? Distance { get; set; } + public ConnectorPoint Offset { get; set; } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LinkVertexModel.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LinkVertexModel.cs new file mode 100644 index 0000000..0b6f7d9 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LinkVertexModel.cs @@ -0,0 +1,20 @@ +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public class LinkVertexModel : ConnectorPoint + { + public LinkVertexModel(ConnectorViewModel parent, PointBase? position = null) + { + Parent = parent; + X = position?.X ?? 0; + Y = position?.Y ?? 0; + } + + public ConnectorViewModel Parent + { + get; + } + + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/PartCreatedConnectionInfo.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/PartCreatedConnectionInfo.cs new file mode 100644 index 0000000..967bd69 --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/PartCreatedConnectionInfo.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using AIStudio.Wpf.DiagramDesigner.Geometry; + +namespace AIStudio.Wpf.DiagramDesigner +{ + public class PartCreatedConnectionInfo : ConnectorInfoBase + { + private PointBase position; + public override PointBase Position + { + get + { + return position; + } + } + + public PartCreatedConnectionInfo(double X, double Y) : base(ConnectorOrientation.None) + { + this.position = new PointBase(X, Y); + } + } +} diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/IDiagramViewModel.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/IDiagramViewModel.cs index 86b1301..b6d6a6a 100644 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/IDiagramViewModel.cs +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/IDiagramViewModel.cs @@ -188,8 +188,8 @@ namespace AIStudio.Wpf.DiagramDesigner get; set; } void ClearSelectedItems(); - bool BelongToSameGroup(IGroupable item1, IGroupable item2); - Rect GetBoundingRectangle(IEnumerable items); + //bool BelongToSameGroup(IGroupable item1, IGroupable item2); + //Rectangle GetBoundingRectangle(IEnumerable items); void UpdateZIndex(); bool IsReadOnly @@ -241,7 +241,7 @@ namespace AIStudio.Wpf.DiagramDesigner get; set; } - Point CurrentPoint + System.Windows.Point CurrentPoint { get; set; } diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/PartCreatedConnectionInfo.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/PartCreatedConnectionInfo.cs deleted file mode 100644 index 18dba02..0000000 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/PartCreatedConnectionInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows; - -namespace AIStudio.Wpf.DiagramDesigner -{ - public class PartCreatedConnectionInfo : ConnectorInfoBase - { - public Point CurrentLocation { get; private set; } - - public PartCreatedConnectionInfo(Point currentLocation) : base(ConnectorOrientation.None) - { - this.CurrentLocation = currentLocation; - } - } -} diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LogicalGateItemViewModelBase.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/SimpleViewModel/LogicalGateItemViewModelBase.cs similarity index 100% rename from AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/LogicalGateItemViewModelBase.cs rename to AIStudio.Wpf.DiagramDesigner/ViewModels/SimpleViewModel/LogicalGateItemViewModelBase.cs diff --git a/AIStudio.Wpf.Flowchart/Controls/FlowchartEditor.xaml.cs b/AIStudio.Wpf.Flowchart/Controls/FlowchartEditor.xaml.cs index ffdf205..23b2b49 100644 --- a/AIStudio.Wpf.Flowchart/Controls/FlowchartEditor.xaml.cs +++ b/AIStudio.Wpf.Flowchart/Controls/FlowchartEditor.xaml.cs @@ -8,6 +8,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using AIStudio.Wpf.DiagramDesigner; +using AIStudio.Wpf.DiagramDesigner.Geometry; using AIStudio.Wpf.Flowchart.Models; namespace AIStudio.Wpf.Flowchart.Controls @@ -33,12 +34,12 @@ namespace AIStudio.Wpf.Flowchart.Controls _diagramViewModel = new DiagramViewModel(); _diagramViewModel.SetScreenScale(); _diagramViewModel.ShowGrid = true; - _diagramViewModel.GridCellSize = new Size(125 / _diagramViewModel.ScreenScale, 125 / _diagramViewModel.ScreenScale); + _diagramViewModel.GridCellSize = new SizeBase(125 / _diagramViewModel.ScreenScale, 125 / _diagramViewModel.ScreenScale); _diagramViewModel.GridMargin = 0d; _diagramViewModel.CellHorizontalAlignment = CellHorizontalAlignment.Center; _diagramViewModel.CellVerticalAlignment = CellVerticalAlignment.Center; _diagramViewModel.PageSizeType = PageSizeType.Custom; - _diagramViewModel.PageSize = new Size(double.NaN, double.NaN); + _diagramViewModel.PageSize = new SizeBase(double.NaN, double.NaN); _diagramViewModel.ColorViewModel = new ColorViewModel() { LineWidth = 2 }; //_diagramViewModel.DrawModeViewModel = new DrawModeViewModel() { VectorLineDrawMode = DrawMode.BoundaryConnectingLine }; diff --git a/AIStudio.Wpf.Flowchart/FlowchartService.cs b/AIStudio.Wpf.Flowchart/FlowchartService.cs index d2a5042..5c17f05 100644 --- a/AIStudio.Wpf.Flowchart/FlowchartService.cs +++ b/AIStudio.Wpf.Flowchart/FlowchartService.cs @@ -33,7 +33,10 @@ namespace AIStudio.Wpf.Flowchart private static List _users; public static List Users { - get { return _users; } + get + { + return _users; + } set { _users = value; @@ -43,7 +46,10 @@ namespace AIStudio.Wpf.Flowchart private static List _roles; public static List Roles { - get { return _roles; } + get + { + return _roles; + } set { _roles = value; @@ -67,20 +73,22 @@ namespace AIStudio.Wpf.Flowchart { foreach (var edge in connectors) { + if (edge.IsFullConnection == false) continue; + var source = oASteps.FirstOrDefault(p => p.BottomConnector == edge.SourceConnectorInfo || p.LeftConnector == edge.SourceConnectorInfo || p.RightConnector == edge.SourceConnectorInfo); if (source != null) { if (source.Kind == NodeKinds.Decide) { - source.SelectNextStep.Add((edge.SinkConnectorInfo as FullyCreatedConnectorInfo).DataItem.Id.ToString(), "data.Flag" + edge.Text); + source.SelectNextStep.Add(edge.SinkConnectorInfoFully.DataItem.Id.ToString(), "data.Flag" + edge.Text); } else if (source.Kind == NodeKinds.COBegin) { - source.SelectNextStep.Add((edge.SinkConnectorInfo as FullyCreatedConnectorInfo).DataItem.Id.ToString(), "True"); + source.SelectNextStep.Add(edge.SinkConnectorInfoFully.DataItem.Id.ToString(), "True"); } else { - source.NextStepId = (edge.SinkConnectorInfo as FullyCreatedConnectorInfo).DataItem.Id.ToString(); + source.NextStepId = edge.SinkConnectorInfoFully.DataItem.Id.ToString(); } } } @@ -126,7 +134,7 @@ namespace AIStudio.Wpf.Flowchart var nodes = InitStep(oASteps, nextstepid); nodes.Insert(0, oAStartStep); - FlowNodes.Add(viewModel, nodes); + FlowNodes.Add(viewModel, nodes); Approve(oAStartStep, 100); } @@ -213,7 +221,7 @@ namespace AIStudio.Wpf.Flowchart } } } - + } SetStatus(flowNode, status, remark); @@ -287,8 +295,8 @@ namespace AIStudio.Wpf.Flowchart return; } } - catch(Exception ex) - { + catch (Exception ex) + { } } //如果表达式错了,就按第一个处理 @@ -372,6 +380,6 @@ namespace AIStudio.Wpf.Flowchart public static void DisposeData(IDiagramViewModel viewModel) { FlowNodes.Remove(viewModel); - } + } } } diff --git a/AIStudio.Wpf.Flowchart/Models/DiagramDataExtention.cs b/AIStudio.Wpf.Flowchart/Models/DiagramDataExtention.cs index 9ad904b..5ce081a 100644 --- a/AIStudio.Wpf.Flowchart/Models/DiagramDataExtention.cs +++ b/AIStudio.Wpf.Flowchart/Models/DiagramDataExtention.cs @@ -75,10 +75,10 @@ namespace AIStudio.Wpf.Flowchart.Models diagramLink.Width = linkModel.ColorViewModel.LineWidth; diagramLink.Label = linkModel.Text; - if (linkModel.SinkConnectorInfo is FullyCreatedConnectorInfo sinkConnector) + if (linkModel.IsFullConnection) { diagramLink.SourceId = linkModel.SourceConnectorInfo.DataItem.Id.ToString(); - diagramLink.TargetId = sinkConnector.DataItem.Id.ToString(); + diagramLink.TargetId = linkModel.SinkConnectorInfoFully.DataItem.Id.ToString(); //线条形状与箭头待处理 //diagramLink.Router = baseLinkModel.Router?.Method.Name; @@ -91,7 +91,7 @@ namespace AIStudio.Wpf.Flowchart.Models diagramLink.Type = diagramLink.GetType().Name; diagramLink.SourcePortAlignment = linkModel.SourceConnectorInfo.Orientation.ToString(); - diagramLink.TargetPortAlignment = sinkConnector.Orientation.ToString(); + diagramLink.TargetPortAlignment = linkModel.SinkConnectorInfoFully.Orientation.ToString(); } else { diff --git a/AIStudio.Wpf.SFC/SFCService.cs b/AIStudio.Wpf.SFC/SFCService.cs index 4b6e9f2..d59d55b 100644 --- a/AIStudio.Wpf.SFC/SFCService.cs +++ b/AIStudio.Wpf.SFC/SFCService.cs @@ -49,11 +49,13 @@ namespace AIStudio.Wpf.SFC foreach (var edge in connectors) { + if (edge.IsFullConnection == false) continue; + var source = nodes.FirstOrDefault(p => p.BottomConnector == edge.SourceConnectorInfo || p.LeftConnector == edge.SourceConnectorInfo || p.RightConnector == edge.SourceConnectorInfo || p.TopConnector == edge.SourceConnectorInfo); if (source != null) { - source.NextNode.Add((edge.SinkConnectorInfo as FullyCreatedConnectorInfo).DataItem as SFCNode); - ((edge.SinkConnectorInfo as FullyCreatedConnectorInfo).DataItem as SFCNode).PreNode.Add(source); + source.NextNode.Add(edge.SinkConnectorInfoFully.DataItem as SFCNode); + (edge.SinkConnectorInfoFully.DataItem as SFCNode).PreNode.Add(source); } }