diff --git a/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs b/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs index 715a65a..44f2743 100644 --- a/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs +++ b/AIStudio.Wpf.DiagramDesigner/Controls/DesignerCanvas.cs @@ -5,6 +5,7 @@ using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; +using System.Windows.Media.Media3D; using System.Windows.Resources; using AIStudio.Wpf.DiagramDesigner.Helpers; using AIStudio.Wpf.DiagramDesigner.Models; @@ -220,6 +221,48 @@ namespace AIStudio.Wpf.DiagramDesigner #endregion + #region AutoGrowth + + public static readonly DependencyProperty AutoGrowthProperty = + DependencyProperty.Register(nameof(AutoGrowth), + typeof(bool), + typeof(DesignerCanvas), + new FrameworkPropertyMetadata(true)); + + public bool AutoGrowth + { + get + { + return (bool)GetValue(AutoGrowthProperty); + } + set + { + SetValue(AutoGrowthProperty, value); + } + } + #endregion + + #region CornerRadius + + public static readonly DependencyProperty CornerRadiusProperty = + DependencyProperty.Register(nameof(CornerRadius), + typeof(CornerRadius), + typeof(DesignerCanvas), + new FrameworkPropertyMetadata(new CornerRadius())); + + public CornerRadius CornerRadius + { + get + { + return (CornerRadius)GetValue(CornerRadiusProperty); + } + set + { + SetValue(CornerRadiusProperty, value); + } + } + #endregion + #region GridMarginSize 单位mm public static readonly DependencyProperty GridMarginSizeProperty = @@ -291,6 +334,164 @@ namespace AIStudio.Wpf.DiagramDesigner DrawGrid(dc, rect); } + public static Geometry GetRoundRectangle(Rect baseRect, Thickness borderThickness, CornerRadius cornerRadius) + { + // Normalizing the corner radius + if (cornerRadius.TopLeft < double.Epsilon) + { + cornerRadius.TopLeft = 0.0; + } + + if (cornerRadius.TopRight < double.Epsilon) + { + cornerRadius.TopRight = 0.0; + } + + if (cornerRadius.BottomLeft < double.Epsilon) + { + cornerRadius.BottomLeft = 0.0; + } + + if (cornerRadius.BottomRight < double.Epsilon) + { + cornerRadius.BottomRight = 0.0; + } + + // Taking the border thickness into account + var leftHalf = borderThickness.Left * 0.5; + if (leftHalf < double.Epsilon) + { + leftHalf = 0.0; + } + + var topHalf = borderThickness.Top * 0.5; + if (topHalf < double.Epsilon) + { + topHalf = 0.0; + } + + var rightHalf = borderThickness.Right * 0.5; + if (rightHalf < double.Epsilon) + { + rightHalf = 0.0; + } + + var bottomHalf = borderThickness.Bottom * 0.5; + if (bottomHalf < double.Epsilon) + { + bottomHalf = 0.0; + } + + // Create the rectangles for the corners that needs to be curved in the base rectangle + // TopLeft Rectangle + var topLeftRect = new Rect( + baseRect.Location.X, + baseRect.Location.Y, + Math.Max(0.0, cornerRadius.TopLeft - leftHalf), + Math.Max(0.0, cornerRadius.TopLeft - rightHalf)); + + // TopRight Rectangle + var topRightRect = new Rect( + baseRect.Location.X + baseRect.Width - cornerRadius.TopRight + rightHalf, + baseRect.Location.Y, + Math.Max(0.0, cornerRadius.TopRight - rightHalf), + Math.Max(0.0, cornerRadius.TopRight - topHalf)); + + // BottomRight Rectangle + var bottomRightRect = new Rect( + baseRect.Location.X + baseRect.Width - cornerRadius.BottomRight + rightHalf, + baseRect.Location.Y + baseRect.Height - cornerRadius.BottomRight + bottomHalf, + Math.Max(0.0, cornerRadius.BottomRight - rightHalf), + Math.Max(0.0, cornerRadius.BottomRight - bottomHalf)); + + // BottomLeft Rectangle + var bottomLeftRect = new Rect( + baseRect.Location.X, + baseRect.Location.Y + baseRect.Height - cornerRadius.BottomLeft + bottomHalf, + Math.Max(0.0, cornerRadius.BottomLeft - leftHalf), + Math.Max(0.0, cornerRadius.BottomLeft - bottomHalf)); + + // Adjust the _width of the TopLeft and TopRight rectangles so that they are proportional to the _width of the baseRect + if (topLeftRect.Right > topRightRect.Left) + { + var newWidth = (topLeftRect.Width / (topLeftRect.Width + topRightRect.Width)) * baseRect.Width; + topLeftRect = new Rect(topLeftRect.Location.X, topLeftRect.Location.Y, newWidth, topLeftRect.Height); + topRightRect = new Rect( + baseRect.Left + newWidth, + topRightRect.Location.Y, + Math.Max(0.0, baseRect.Width - newWidth), + topRightRect.Height); + } + + // Adjust the height of the TopRight and BottomRight rectangles so that they are proportional to the height of the baseRect + if (topRightRect.Bottom > bottomRightRect.Top) + { + var newHeight = (topRightRect.Height / (topRightRect.Height + bottomRightRect.Height)) * baseRect.Height; + topRightRect = new Rect(topRightRect.Location.X, topRightRect.Location.Y, topRightRect.Width, newHeight); + bottomRightRect = new Rect( + bottomRightRect.Location.X, + baseRect.Top + newHeight, + bottomRightRect.Width, + Math.Max(0.0, baseRect.Height - newHeight)); + } + + // Adjust the _width of the BottomLeft and BottomRight rectangles so that they are proportional to the _width of the baseRect + if (bottomRightRect.Left < bottomLeftRect.Right) + { + var newWidth = (bottomLeftRect.Width / (bottomLeftRect.Width + bottomRightRect.Width)) * baseRect.Width; + bottomLeftRect = new Rect(bottomLeftRect.Location.X, bottomLeftRect.Location.Y, newWidth, bottomLeftRect.Height); + bottomRightRect = new Rect( + baseRect.Left + newWidth, + bottomRightRect.Location.Y, + Math.Max(0.0, baseRect.Width - newWidth), + bottomRightRect.Height); + } + + // Adjust the height of the TopLeft and BottomLeft rectangles so that they are proportional to the height of the baseRect + if (bottomLeftRect.Top < topLeftRect.Bottom) + { + var newHeight = (topLeftRect.Height / (topLeftRect.Height + bottomLeftRect.Height)) * baseRect.Height; + topLeftRect = new Rect(topLeftRect.Location.X, topLeftRect.Location.Y, topLeftRect.Width, newHeight); + bottomLeftRect = new Rect( + bottomLeftRect.Location.X, + baseRect.Top + newHeight, + bottomLeftRect.Width, + Math.Max(0.0, baseRect.Height - newHeight)); + } + + var roundedRectGeometry = new StreamGeometry(); + + using (var context = roundedRectGeometry.Open()) + { + // Begin from the Bottom of the TopLeft Arc and proceed clockwise + context.BeginFigure(topLeftRect.BottomLeft, true, true); + + // TopLeft Arc + context.ArcTo(topLeftRect.TopRight, topLeftRect.Size, 0, false, SweepDirection.Clockwise, true, true); + + // Top Line + context.LineTo(topRightRect.TopLeft, true, true); + + // TopRight Arc + context.ArcTo(topRightRect.BottomRight, topRightRect.Size, 0, false, SweepDirection.Clockwise, true, true); + + // Right Line + context.LineTo(bottomRightRect.TopRight, true, true); + + // BottomRight Arc + context.ArcTo(bottomRightRect.BottomLeft, bottomRightRect.Size, 0, false, SweepDirection.Clockwise, true, true); + + // Bottom Line + context.LineTo(bottomLeftRect.BottomRight, true, true); + + // BottomLeft Arc + context.ArcTo(bottomLeftRect.TopLeft, bottomLeftRect.Size, 0, false, SweepDirection.Clockwise, true, true); + } + + return roundedRectGeometry; + } + #endregion + protected virtual void DrawGrid(DrawingContext dc, Rect rect) { //using .5 forces wpf to draw a single pixel line @@ -302,7 +503,6 @@ namespace AIStudio.Wpf.DiagramDesigner dc.DrawLine(new Pen(new SolidColorBrush(GridColor), 1), new Point(i, GridMarginSize.Height), new Point(i, rect.Height - GridMarginSize.Height)); dc.DrawLine(new Pen(new SolidColorBrush(GridColor), 1), new Point(rect.Width - GridMarginSize.Width, GridMarginSize.Height), new Point(rect.Width - GridMarginSize.Width, rect.Height - GridMarginSize.Height)); } - #endregion #region Format/Move private void _service_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -616,29 +816,36 @@ namespace AIStudio.Wpf.DiagramDesigner protected override Size MeasureOverride(Size constraint) { - Size size = new Size(); - foreach (UIElement element in this.InternalChildren) + if (AutoGrowth) { - double left = Canvas.GetLeft(element); - double top = Canvas.GetTop(element); - left = double.IsNaN(left) ? 0 : left; - top = double.IsNaN(top) ? 0 : top; - - //measure desired size for each child - element.Measure(constraint); - - Size desiredSize = element.DesiredSize; - if (!double.IsNaN(desiredSize.Width) && !double.IsNaN(desiredSize.Height)) + Size size = new Size(); + foreach (UIElement element in this.InternalChildren) { - size.Width = Math.Max(size.Width, left + desiredSize.Width); - size.Height = Math.Max(size.Height, top + desiredSize.Height); - } - } - // add margin - size.Width += 10; - size.Height += 10; + double left = Canvas.GetLeft(element); + double top = Canvas.GetTop(element); + left = double.IsNaN(left) ? 0 : left; + top = double.IsNaN(top) ? 0 : top; - return size; + //measure desired size for each child + element.Measure(constraint); + + Size desiredSize = element.DesiredSize; + if (!double.IsNaN(desiredSize.Width) && !double.IsNaN(desiredSize.Height)) + { + size.Width = Math.Max(size.Width, left + desiredSize.Width); + size.Height = Math.Max(size.Height, top + desiredSize.Height); + } + } + // add margin + size.Width += 10; + size.Height += 10; + + return size; + } + else + { + return base.MeasureOverride(constraint); + } } private Connector HitTesting(Point hitPoint) diff --git a/AIStudio.Wpf.DiagramDesigner/Converters/BorderClipConverter.cs b/AIStudio.Wpf.DiagramDesigner/Converters/BorderClipConverter.cs new file mode 100644 index 0000000..688b15b --- /dev/null +++ b/AIStudio.Wpf.DiagramDesigner/Converters/BorderClipConverter.cs @@ -0,0 +1,203 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; + +namespace AIStudio.Wpf.DiagramDesigner.Converters +{ + public class BorderClipConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length > 1 && values[0] is double width && values[1] is double height) + { + if (width < 1.0 || height < 1.0) + { + return Geometry.Empty; + } + + CornerRadius cornerRadius = default; + Thickness borderThickness = default; + if (values.Length > 2 && values[2] is CornerRadius radius) + { + cornerRadius = radius; + if (values.Length > 3 && values[3] is Thickness thickness) + { + borderThickness = thickness; + } + } + + var geometry = GetRoundRectangle(new Rect(0, 0, width, height), borderThickness, cornerRadius); + geometry.Freeze(); + + return geometry; + } + + return DependencyProperty.UnsetValue; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + // https://wpfspark.wordpress.com/2011/06/08/clipborder-a-wpf-border-that-clips/ + public static Geometry GetRoundRectangle(Rect baseRect, Thickness borderThickness, CornerRadius cornerRadius) + { + // Normalizing the corner radius + if (cornerRadius.TopLeft < double.Epsilon) + { + cornerRadius.TopLeft = 0.0; + } + + if (cornerRadius.TopRight < double.Epsilon) + { + cornerRadius.TopRight = 0.0; + } + + if (cornerRadius.BottomLeft < double.Epsilon) + { + cornerRadius.BottomLeft = 0.0; + } + + if (cornerRadius.BottomRight < double.Epsilon) + { + cornerRadius.BottomRight = 0.0; + } + + // Taking the border thickness into account + var leftHalf = borderThickness.Left * 0.5; + if (leftHalf < double.Epsilon) + { + leftHalf = 0.0; + } + + var topHalf = borderThickness.Top * 0.5; + if (topHalf < double.Epsilon) + { + topHalf = 0.0; + } + + var rightHalf = borderThickness.Right * 0.5; + if (rightHalf < double.Epsilon) + { + rightHalf = 0.0; + } + + var bottomHalf = borderThickness.Bottom * 0.5; + if (bottomHalf < double.Epsilon) + { + bottomHalf = 0.0; + } + + // Create the rectangles for the corners that needs to be curved in the base rectangle + // TopLeft Rectangle + var topLeftRect = new Rect( + baseRect.Location.X, + baseRect.Location.Y, + Math.Max(0.0, cornerRadius.TopLeft - leftHalf), + Math.Max(0.0, cornerRadius.TopLeft - rightHalf)); + + // TopRight Rectangle + var topRightRect = new Rect( + baseRect.Location.X + baseRect.Width - cornerRadius.TopRight + rightHalf, + baseRect.Location.Y, + Math.Max(0.0, cornerRadius.TopRight - rightHalf), + Math.Max(0.0, cornerRadius.TopRight - topHalf)); + + // BottomRight Rectangle + var bottomRightRect = new Rect( + baseRect.Location.X + baseRect.Width - cornerRadius.BottomRight + rightHalf, + baseRect.Location.Y + baseRect.Height - cornerRadius.BottomRight + bottomHalf, + Math.Max(0.0, cornerRadius.BottomRight - rightHalf), + Math.Max(0.0, cornerRadius.BottomRight - bottomHalf)); + + // BottomLeft Rectangle + var bottomLeftRect = new Rect( + baseRect.Location.X, + baseRect.Location.Y + baseRect.Height - cornerRadius.BottomLeft + bottomHalf, + Math.Max(0.0, cornerRadius.BottomLeft - leftHalf), + Math.Max(0.0, cornerRadius.BottomLeft - bottomHalf)); + + // Adjust the _width of the TopLeft and TopRight rectangles so that they are proportional to the _width of the baseRect + if (topLeftRect.Right > topRightRect.Left) + { + var newWidth = (topLeftRect.Width / (topLeftRect.Width + topRightRect.Width)) * baseRect.Width; + topLeftRect = new Rect(topLeftRect.Location.X, topLeftRect.Location.Y, newWidth, topLeftRect.Height); + topRightRect = new Rect( + baseRect.Left + newWidth, + topRightRect.Location.Y, + Math.Max(0.0, baseRect.Width - newWidth), + topRightRect.Height); + } + + // Adjust the height of the TopRight and BottomRight rectangles so that they are proportional to the height of the baseRect + if (topRightRect.Bottom > bottomRightRect.Top) + { + var newHeight = (topRightRect.Height / (topRightRect.Height + bottomRightRect.Height)) * baseRect.Height; + topRightRect = new Rect(topRightRect.Location.X, topRightRect.Location.Y, topRightRect.Width, newHeight); + bottomRightRect = new Rect( + bottomRightRect.Location.X, + baseRect.Top + newHeight, + bottomRightRect.Width, + Math.Max(0.0, baseRect.Height - newHeight)); + } + + // Adjust the _width of the BottomLeft and BottomRight rectangles so that they are proportional to the _width of the baseRect + if (bottomRightRect.Left < bottomLeftRect.Right) + { + var newWidth = (bottomLeftRect.Width / (bottomLeftRect.Width + bottomRightRect.Width)) * baseRect.Width; + bottomLeftRect = new Rect(bottomLeftRect.Location.X, bottomLeftRect.Location.Y, newWidth, bottomLeftRect.Height); + bottomRightRect = new Rect( + baseRect.Left + newWidth, + bottomRightRect.Location.Y, + Math.Max(0.0, baseRect.Width - newWidth), + bottomRightRect.Height); + } + + // Adjust the height of the TopLeft and BottomLeft rectangles so that they are proportional to the height of the baseRect + if (bottomLeftRect.Top < topLeftRect.Bottom) + { + var newHeight = (topLeftRect.Height / (topLeftRect.Height + bottomLeftRect.Height)) * baseRect.Height; + topLeftRect = new Rect(topLeftRect.Location.X, topLeftRect.Location.Y, topLeftRect.Width, newHeight); + bottomLeftRect = new Rect( + bottomLeftRect.Location.X, + baseRect.Top + newHeight, + bottomLeftRect.Width, + Math.Max(0.0, baseRect.Height - newHeight)); + } + + var roundedRectGeometry = new StreamGeometry(); + + using (var context = roundedRectGeometry.Open()) + { + // Begin from the Bottom of the TopLeft Arc and proceed clockwise + context.BeginFigure(topLeftRect.BottomLeft, true, true); + + // TopLeft Arc + context.ArcTo(topLeftRect.TopRight, topLeftRect.Size, 0, false, SweepDirection.Clockwise, true, true); + + // Top Line + context.LineTo(topRightRect.TopLeft, true, true); + + // TopRight Arc + context.ArcTo(topRightRect.BottomRight, topRightRect.Size, 0, false, SweepDirection.Clockwise, true, true); + + // Right Line + context.LineTo(bottomRightRect.TopRight, true, true); + + // BottomRight Arc + context.ArcTo(bottomRightRect.BottomLeft, bottomRightRect.Size, 0, false, SweepDirection.Clockwise, true, true); + + // Bottom Line + context.LineTo(bottomLeftRect.BottomRight, true, true); + + // BottomLeft Arc + context.ArcTo(bottomLeftRect.TopLeft, bottomLeftRect.Size, 0, false, SweepDirection.Clockwise, true, true); + } + + return roundedRectGeometry; + } + } +} \ No newline at end of file diff --git a/AIStudio.Wpf.DiagramDesigner/Themes/DiagramControl.xaml b/AIStudio.Wpf.DiagramDesigner/Themes/DiagramControl.xaml index 98d13ff..c8b9410 100644 --- a/AIStudio.Wpf.DiagramDesigner/Themes/DiagramControl.xaml +++ b/AIStudio.Wpf.DiagramDesigner/Themes/DiagramControl.xaml @@ -859,16 +859,6 @@ - - - - - - - - - - \ No newline at end of file diff --git a/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml b/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml index 8387b0f..5c2adf3 100644 --- a/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml +++ b/AIStudio.Wpf.DiagramDesigner/UserControls/DiagramControl.xaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:dd="clr-namespace:AIStudio.Wpf.DiagramDesigner" + xmlns:converter="clr-namespace:AIStudio.Wpf.DiagramDesigner.Converters" xmlns:c="clr-namespace:AIStudio.Wpf.DiagramDesigner.Controls" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" @@ -14,6 +15,9 @@ + + + @@ -32,6 +36,7 @@ + AllowDrop="{Binding DiagramOption.LayoutOption.AllowDrop}" + ClipToBounds="{Binding DiagramOption.LayoutOption.ClipToBounds}" + AutoGrowth="{Binding DiagramOption.LayoutOption.AutoGrowth}" + CornerRadius="{Binding DiagramOption.LayoutOption.CornerRadius}"> + + + + + + + @@ -67,6 +82,7 @@ + AllowDrop="{Binding DiagramOption.LayoutOption.AllowDrop}" + ClipToBounds="{Binding DiagramOption.LayoutOption.ClipToBounds}" + AutoGrowth="{Binding DiagramOption.LayoutOption.AutoGrowth}" + CornerRadius="{Binding DiagramOption.LayoutOption.CornerRadius}"> + + + + + + + diff --git a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DiagramOption.cs b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DiagramOption.cs index 779d324..d1f3957 100644 --- a/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DiagramOption.cs +++ b/AIStudio.Wpf.DiagramDesigner/ViewModels/BaseViewModel/DiagramOption.cs @@ -370,7 +370,20 @@ namespace AIStudio.Wpf.DiagramDesigner { get; set; } = true; - + + public bool ClipToBounds + { + get; set; + } + public bool AutoGrowth + { + get; set; + } = true; + + public CornerRadius CornerRadius + { + get; set; + } = new CornerRadius(); } public class SnappingOption