diff --git a/HandyControl/Controls/CircleProgressBar.cs b/HandyControl/Controls/CircleProgressBar.cs new file mode 100644 index 00000000..4f4fa5d0 --- /dev/null +++ b/HandyControl/Controls/CircleProgressBar.cs @@ -0,0 +1,81 @@ +using System.Windows; +using System.Windows.Controls.Primitives; +using HandyControl.Data; +using HandyControl.Expression.Shapes; + +namespace HandyControl.Controls +{ + [TemplatePart(Name = IndicatorTemplateName, Type = typeof(Arc))] + public class CircleProgressBar : RangeBase + { + private const string IndicatorTemplateName = "PART_Indicator"; + + public static readonly DependencyProperty ArcThicknessProperty = DependencyProperty.Register( + "ArcThickness", typeof(double), typeof(CircleProgressBar), new PropertyMetadata(default(double))); + + public static readonly DependencyProperty ShowTextProperty = DependencyProperty.Register( + "ShowText", typeof(bool), typeof(CircleProgressBar), new PropertyMetadata(true)); + + private Arc _indicator; + + static CircleProgressBar() + { + FocusableProperty.OverrideMetadata(typeof(CircleProgressBar), + new FrameworkPropertyMetadata(BooleanBoxes.FalseBox)); + MaximumProperty.OverrideMetadata(typeof(CircleProgressBar), new FrameworkPropertyMetadata(100.0)); + } + + public bool ShowText + { + get => (bool) GetValue(ShowTextProperty); + set => SetValue(ShowTextProperty, value); + } + + public double ArcThickness + { + get => (double) GetValue(ArcThicknessProperty); + set => SetValue(ArcThicknessProperty, value); + } + + private void SetProgressBarIndicatorAngle() + { + if (_indicator == null) return; + var minimum = Minimum; + var maximum = Maximum; + var num = Value; + _indicator.EndAngle = (maximum <= minimum ? 0 : (num - minimum) / (maximum - minimum)) * 360; + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _indicator = GetTemplateChild(IndicatorTemplateName) as Arc; + if (_indicator != null) + { + _indicator.StartAngle = 0; + _indicator.EndAngle = 0; + } + + SetProgressBarIndicatorAngle(); + } + + protected override void OnMinimumChanged(double oldMinimum, double newMinimum) + { + base.OnMinimumChanged(oldMinimum, newMinimum); + SetProgressBarIndicatorAngle(); + } + + protected override void OnMaximumChanged(double oldMaximum, double newMaximum) + { + base.OnMaximumChanged(oldMaximum, newMaximum); + SetProgressBarIndicatorAngle(); + } + + protected override void OnValueChanged(double oldValue, double newValue) + { + base.OnValueChanged(oldValue, newValue); + SetProgressBarIndicatorAngle(); + } + } +} \ No newline at end of file diff --git a/HandyControl/Controls/Growl.cs b/HandyControl/Controls/Growl.cs index b56772d7..1da287b4 100644 --- a/HandyControl/Controls/Growl.cs +++ b/HandyControl/Controls/Growl.cs @@ -284,7 +284,7 @@ namespace HandyControl.Controls /// public static void Success(GrowlInfo growlInfo) { - if (growlInfo == null) throw new NullReferenceException("growlInfo is null"); + if (growlInfo == null) throw new ArgumentNullException(nameof(growlInfo)); growlInfo.IconKey = ResourceToken.SuccessGeometry; growlInfo.IconBrushKey = ResourceToken.SuccessBrush; growlInfo.Type = InfoType.Success; @@ -306,7 +306,7 @@ namespace HandyControl.Controls /// public static void Info(GrowlInfo growlInfo) { - if (growlInfo == null) throw new NullReferenceException("growlInfo is null"); + if (growlInfo == null) throw new ArgumentNullException(nameof(growlInfo)); growlInfo.IconKey = ResourceToken.InfoGeometry; growlInfo.IconBrushKey = ResourceToken.InfoBrush; growlInfo.Type = InfoType.Info; @@ -328,7 +328,7 @@ namespace HandyControl.Controls /// public static void Warning(GrowlInfo growlInfo) { - if (growlInfo == null) throw new NullReferenceException("growlInfo is null"); + if (growlInfo == null) throw new ArgumentNullException(nameof(growlInfo)); growlInfo.IconKey = ResourceToken.WarningGeometry; growlInfo.IconBrushKey = ResourceToken.WarningBrush; growlInfo.Type = InfoType.Warning; @@ -350,7 +350,7 @@ namespace HandyControl.Controls /// public static void Error(GrowlInfo growlInfo) { - if (growlInfo == null) throw new NullReferenceException("growlInfo is null"); + if (growlInfo == null) throw new ArgumentNullException(nameof(growlInfo)); growlInfo.IconKey = ResourceToken.ErrorGeometry; growlInfo.IconBrushKey = ResourceToken.DangerBrush; growlInfo.StaysOpen = true; @@ -373,7 +373,7 @@ namespace HandyControl.Controls /// public static void Fatal(GrowlInfo growlInfo) { - if (growlInfo == null) throw new NullReferenceException("growlInfo is null"); + if (growlInfo == null) throw new ArgumentNullException(nameof(growlInfo)); growlInfo.IconKey = ResourceToken.FatalGeometry; growlInfo.IconBrushKey = ResourceToken.PrimaryTextBrush; growlInfo.StaysOpen = true; @@ -401,7 +401,7 @@ namespace HandyControl.Controls /// public static void Ask(GrowlInfo growlInfo) { - if (growlInfo == null) throw new NullReferenceException("growlInfo is null"); + if (growlInfo == null) throw new ArgumentNullException(nameof(growlInfo)); growlInfo.IconKey = ResourceToken.AskGeometry; growlInfo.IconBrushKey = ResourceToken.AccentBrush; growlInfo.StaysOpen = true; diff --git a/HandyControl/Controls/Slider/SliderCanvas.cs b/HandyControl/Controls/InternalCanvas.cs similarity index 89% rename from HandyControl/Controls/Slider/SliderCanvas.cs rename to HandyControl/Controls/InternalCanvas.cs index ee4bade3..b406724c 100644 --- a/HandyControl/Controls/Slider/SliderCanvas.cs +++ b/HandyControl/Controls/InternalCanvas.cs @@ -1,14 +1,12 @@ -using System; -using System.Windows; +using System.Windows; using System.Windows.Controls; -// ReSharper disable once CheckNamespace namespace HandyControl.Controls { - internal class SliderCanvas : Canvas + internal class InternalCanvas : Canvas { internal static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( - "Orientation", typeof(Orientation), typeof(SliderCanvas), new PropertyMetadata(default(Orientation))); + "Orientation", typeof(Orientation), typeof(InternalCanvas), new PropertyMetadata(default(Orientation))); internal Orientation Orientation { diff --git a/HandyControl/Expression/Drawing/BezierCurveFlattener.cs b/HandyControl/Expression/Drawing/BezierCurveFlattener.cs new file mode 100644 index 00000000..e26ee670 --- /dev/null +++ b/HandyControl/Expression/Drawing/BezierCurveFlattener.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Windows; + +namespace HandyControl.Expression.Drawing +{ + internal static class BezierCurveFlattener + { + public const double StandardFlatteningTolerance = 0.25; + + private static void DoCubicForwardDifferencing(Point[] controlPoints, double leftParameter, double rightParameter, double inverseErrorTolerance, ICollection resultPolyline, ICollection resultParameters) + { + double num14; + var num = controlPoints[1].X - controlPoints[0].X; + var num2 = controlPoints[1].Y - controlPoints[0].Y; + var num3 = controlPoints[2].X - controlPoints[1].X; + var num4 = controlPoints[2].Y - controlPoints[1].Y; + var num5 = controlPoints[3].X - controlPoints[2].X; + var num6 = controlPoints[3].Y - controlPoints[2].Y; + var num7 = num3 - num; + var num8 = num4 - num2; + var num9 = num5 - num3; + var num10 = num6 - num4; + var num11 = num9 - num7; + var num12 = num10 - num8; + var vector = controlPoints[3].Subtract(controlPoints[0]); + var length = vector.Length; + if (!MathHelper.IsVerySmall(length)) + num14 = Math.Max(0.0, + Math.Max(Math.Abs((num7 * vector.Y - num8 * vector.X) / length), + Math.Abs((num9 * vector.Y - num10 * vector.X) / length))); + else + num14 = Math.Max(0.0, + Math.Max(GeometryHelper.Distance(controlPoints[1], controlPoints[0]), + GeometryHelper.Distance(controlPoints[2], controlPoints[0]))); + uint num15 = 0; + if (num14 > 0.0) + { + var d = num14 * inverseErrorTolerance; + num15 = d < 2147483647.0 ? Log4UnsignedInt32((uint) (d + 0.5)) : Log4Double(d); + } + + var exp = (int) -num15; + var num18 = exp + exp; + var num19 = num18 + exp; + var num20 = MathHelper.DoubleFromMantissaAndExponent(3.0 * num7, num18); + var num21 = MathHelper.DoubleFromMantissaAndExponent(3.0 * num8, num18); + var num22 = MathHelper.DoubleFromMantissaAndExponent(6.0 * num11, num19); + var num23 = MathHelper.DoubleFromMantissaAndExponent(6.0 * num12, num19); + var num24 = MathHelper.DoubleFromMantissaAndExponent(3.0 * num, exp) + num20 + + 0.16666666666666666 * num22; + var num25 = MathHelper.DoubleFromMantissaAndExponent(3.0 * num2, exp) + num21 + + 0.16666666666666666 * num23; + var num26 = 2.0 * num20 + num22; + var num27 = 2.0 * num21 + num23; + var x = controlPoints[0].X; + var y = controlPoints[0].Y; + var item = new Point(0.0, 0.0); + var num30 = 1 << (int) num15; + var num31 = num30 > 0 ? (rightParameter - leftParameter) / num30 : 0.0; + var num32 = leftParameter; + for (var i = 1; i < num30; i++) + { + x += num24; + y += num25; + item.X = x; + item.Y = y; + resultPolyline.Add(item); + num32 += num31; + resultParameters?.Add(num32); + num24 += num26; + num25 += num27; + num26 += num22; + num27 += num23; + } + } + + private static void DoCubicMidpointSubdivision(Point[] controlPoints, uint depth, double leftParameter, double rightParameter, double inverseErrorTolerance, ICollection resultPolyline, ICollection resultParameters) + { + Point[] pointArray = { controlPoints[0], controlPoints[1], controlPoints[2], controlPoints[3] }; + var pointArray2 = new Point[4]; + pointArray2[3] = pointArray[3]; + pointArray[3] = GeometryHelper.Midpoint(pointArray[3], pointArray[2]); + pointArray[2] = GeometryHelper.Midpoint(pointArray[2], pointArray[1]); + pointArray[1] = GeometryHelper.Midpoint(pointArray[1], pointArray[0]); + pointArray2[2] = pointArray[3]; + pointArray[3] = GeometryHelper.Midpoint(pointArray[3], pointArray[2]); + pointArray[2] = GeometryHelper.Midpoint(pointArray[2], pointArray[1]); + pointArray2[1] = pointArray[3]; + pointArray[3] = GeometryHelper.Midpoint(pointArray[3], pointArray[2]); + pointArray2[0] = pointArray[3]; + depth--; + var num = (leftParameter + rightParameter) * 0.5; + if (depth > 0) + { + DoCubicMidpointSubdivision(pointArray, depth, leftParameter, num, inverseErrorTolerance, + resultPolyline, resultParameters); + resultPolyline.Add(pointArray2[0]); + resultParameters?.Add(num); + DoCubicMidpointSubdivision(pointArray2, depth, num, rightParameter, inverseErrorTolerance, + resultPolyline, resultParameters); + } + else + { + DoCubicForwardDifferencing(pointArray, leftParameter, num, inverseErrorTolerance, + resultPolyline, resultParameters); + resultPolyline.Add(pointArray2[0]); + resultParameters?.Add(num); + DoCubicForwardDifferencing(pointArray2, num, rightParameter, inverseErrorTolerance, + resultPolyline, resultParameters); + } + } + + private static void EnsureErrorTolerance(ref double errorTolerance) + { + if (errorTolerance <= 0.0) + { + errorTolerance = 0.25; + } + } + + public static void FlattenCubic(Point[] controlPoints, double errorTolerance, ICollection resultPolyline, bool skipFirstPoint, ICollection resultParameters = null) + { + if (resultPolyline == null) throw new ArgumentNullException(nameof(resultPolyline)); + if (controlPoints == null) throw new ArgumentNullException(nameof(controlPoints)); + if (controlPoints.Length != 4) throw new ArgumentOutOfRangeException(nameof(controlPoints)); + EnsureErrorTolerance(ref errorTolerance); + if (!skipFirstPoint) + { + resultPolyline.Add(controlPoints[0]); + resultParameters?.Add(0.0); + } + + if (IsCubicChordMonotone(controlPoints, errorTolerance * errorTolerance)) + { + var flattener = new AdaptiveForwardDifferencingCubicFlattener(controlPoints, errorTolerance, + errorTolerance, true); + var p = new Point(); + var u = 0.0; + while (flattener.Next(ref p, ref u)) + { + resultPolyline.Add(p); + resultParameters?.Add(u); + } + } + else + { + var x = controlPoints[3].X - controlPoints[2].X + controlPoints[1].X - controlPoints[0].X; + var y = controlPoints[3].Y - controlPoints[2].Y + controlPoints[1].Y - controlPoints[0].Y; + var num4 = 1.0 / errorTolerance; + var depth = Log8UnsignedInt32((uint) (MathHelper.Hypotenuse(x, y) * num4 + 0.5)); + if (depth > 0) depth--; + if (depth > 0) + DoCubicMidpointSubdivision(controlPoints, depth, 0.0, 1.0, 0.75 * num4, resultPolyline, + resultParameters); + else + DoCubicForwardDifferencing(controlPoints, 0.0, 1.0, 0.75 * num4, resultPolyline, + resultParameters); + } + + resultPolyline.Add(controlPoints[3]); + resultParameters?.Add(1.0); + } + + public static void FlattenQuadratic(Point[] controlPoints, double errorTolerance, ICollection resultPolyline, bool skipFirstPoint, ICollection resultParameters = null) + { + if (resultPolyline == null) + { + throw new ArgumentNullException(nameof(resultPolyline)); + } + if (controlPoints == null) + { + throw new ArgumentNullException(nameof(controlPoints)); + } + if (controlPoints.Length != 3) + { + throw new ArgumentOutOfRangeException(nameof(controlPoints)); + } + EnsureErrorTolerance(ref errorTolerance); + Point[] pointArray = { controlPoints[0], GeometryHelper.Lerp(controlPoints[0], controlPoints[1], 0.66666666666666663), GeometryHelper.Lerp(controlPoints[1], controlPoints[2], 0.33333333333333331), controlPoints[2] }; + FlattenCubic(pointArray, errorTolerance, resultPolyline, skipFirstPoint, resultParameters); + } + + private static bool IsCubicChordMonotone(Point[] controlPoints, double squaredTolerance) + { + double num = GeometryHelper.SquaredDistance(controlPoints[0], controlPoints[3]); + if (num <= squaredTolerance) + { + return false; + } + Vector lhs = controlPoints[3].Subtract(controlPoints[0]); + Vector rhs = controlPoints[1].Subtract(controlPoints[0]); + double num2 = GeometryHelper.Dot(lhs, rhs); + if ((num2 < 0.0) || (num2 > num)) + { + return false; + } + Vector vector3 = controlPoints[2].Subtract(controlPoints[0]); + double num3 = GeometryHelper.Dot(lhs, vector3); + if ((num3 < 0.0) || (num3 > num)) + { + return false; + } + if (num2 > num3) + { + return false; + } + return true; + } + + private static uint Log4Double(double d) + { + uint num = 0; + while (d > 1.0) + { + d *= 0.25; + num++; + } + return num; + } + + private static uint Log4UnsignedInt32(uint i) + { + uint num = 0; + while (i > 0) + { + i = i >> 2; + num++; + } + return num; + } + + private static uint Log8UnsignedInt32(uint i) + { + uint num = 0; + while (i > 0) + { + i = i >> 3; + num++; + } + return num; + } + + private class AdaptiveForwardDifferencingCubicFlattener + { + private double _aX; + + private double _aY; + + private double _bX; + + private double _bY; + + private double _cX; + + private double _cY; + + private readonly double _distanceTolerance; + + private readonly bool _doParameters; + + private double _dParameter; + + private double _dX; + + private double _dY; + + private readonly double _flatnessTolerance; + + private int _numSteps; + + private double _parameter; + + internal AdaptiveForwardDifferencingCubicFlattener(Point[] controlPoints, + double flatnessTolerance, double distanceTolerance, bool doParameters) + { + _numSteps = 1; + _dParameter = 1.0; + _flatnessTolerance = 3.0 * flatnessTolerance; + _distanceTolerance = distanceTolerance; + _doParameters = doParameters; + _aX = -controlPoints[0].X + 3.0 * (controlPoints[1].X - controlPoints[2].X) + + controlPoints[3].X; + _aY = -controlPoints[0].Y + 3.0 * (controlPoints[1].Y - controlPoints[2].Y) + + controlPoints[3].Y; + _bX = 3.0 * (controlPoints[0].X - 2.0 * controlPoints[1].X + controlPoints[2].X); + _bY = 3.0 * (controlPoints[0].Y - 2.0 * controlPoints[1].Y + controlPoints[2].Y); + _cX = 3.0 * (-controlPoints[0].X + controlPoints[1].X); + _cY = 3.0 * (-controlPoints[0].Y + controlPoints[1].Y); + _dX = controlPoints[0].X; + _dY = controlPoints[0].Y; + } + + private void DoubleStepSize() + { + _aX *= 8.0; + _aY *= 8.0; + _bX *= 4.0; + _bY *= 4.0; + _cX += _cX; + _cY += _cY; + if (_doParameters) _dParameter *= 2.0; + _numSteps = _numSteps >> 1; + } + + private void HalveStepSize() + { + _aX *= 0.125; + _aY *= 0.125; + _bX *= 0.25; + _bY *= 0.25; + _cX *= 0.5; + _cY *= 0.5; + if (_doParameters) _dParameter *= 0.5; + _numSteps = _numSteps << 1; + } + + private void IncrementDifferencesAndParameter() + { + _dX = _aX + _bX + _cX + _dX; + _dY = _aY + _bY + _cY + _dY; + _cX = _aX + _aX + _aX + _bX + _bX + _cX; + _cY = _aY + _aY + _aY + _bY + _bY + _cY; + _bX = _aX + _aX + _aX + _bX; + _bY = _aY + _aY + _aY + _bY; + _numSteps--; + _parameter += _dParameter; + } + + private bool MustSubdivide(double flatnessTolerance) + { + var num = -(_aY + _bY + _cY); + var num2 = _aX + _bX + _cX; + var num3 = Math.Abs(num) + Math.Abs(num2); + if (num3 <= _distanceTolerance) return false; + num3 *= flatnessTolerance; + if (Math.Abs(_cX * num + _cY * num2) <= num3 && + Math.Abs((_bX + _cX + _cX) * num + (_bY + _cY + _cY) * num2) <= num3) return false; + return true; + } + + // ReSharper disable once RedundantAssignment + internal bool Next(ref Point p, ref double u) + { + while (MustSubdivide(_flatnessTolerance)) HalveStepSize(); + if ((_numSteps & 1) == 0) + while (_numSteps > 1 && !MustSubdivide(_flatnessTolerance * 0.25)) + DoubleStepSize(); + IncrementDifferencesAndParameter(); + p.X = _dX; + p.Y = _dY; + u = _parameter; + return _numSteps != 0; + } + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/CommonExtensions.cs b/HandyControl/Expression/Drawing/CommonExtensions.cs new file mode 100644 index 00000000..a3e22a5a --- /dev/null +++ b/HandyControl/Expression/Drawing/CommonExtensions.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Windows; + +namespace HandyControl.Expression.Drawing +{ + internal static class CommonExtensions + { + public static void AddRange(this ICollection collection, IEnumerable newItems) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + if (collection is List list) + { + list.AddRange(newItems); + } + else + { + foreach (var local in newItems) + { + collection.Add(local); + } + } + } + + public static bool SetIfDifferent(this DependencyObject dependencyObject, DependencyProperty dependencyProperty, object value) + { + if (!Equals(dependencyObject.GetValue(dependencyProperty), value)) + { + dependencyObject.SetValue(dependencyProperty, value); + return true; + } + return false; + } + + public static bool EnsureListCount(this IList list, int count, Func factory = null) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (!list.EnsureListCountAtLeast(count, factory)) + { + if (list.Count <= count) + { + return false; + } + + if (list is List list2) + { + list2.RemoveRange(count, list.Count - count); + } + else + { + for (int i = list.Count - 1; i >= count; i--) + { + list.RemoveAt(i); + } + } + } + return true; + } + + public static bool EnsureListCountAtLeast(this IList list, int count, Func factory = null) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (list.Count >= count) + { + return false; + } + + if ((list is List list2) && (factory == null)) + { + list2.AddRange(new T[count - list.Count]); + } + else + { + for (int i = list.Count; i < count; i++) + { + list.Add((factory == null) ? default(T) : factory()); + } + } + return true; + } + + public static bool ClearIfSet(this DependencyObject dependencyObject, DependencyProperty dependencyProperty) + { + if (dependencyObject.ReadLocalValue(dependencyProperty) != DependencyProperty.UnsetValue) + { + dependencyObject.ClearValue(dependencyProperty); + return true; + } + return false; + } + + public static void RemoveLast(this IList list) + { + list.RemoveAt(list.Count - 1); + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/ExceptionStringTable.cs b/HandyControl/Expression/Drawing/ExceptionStringTable.cs new file mode 100644 index 00000000..d2deda04 --- /dev/null +++ b/HandyControl/Expression/Drawing/ExceptionStringTable.cs @@ -0,0 +1,39 @@ +using System.CodeDom.Compiler; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Resources; +using System.Runtime.CompilerServices; + +namespace HandyControl.Expression.Drawing +{ + [CompilerGenerated] + [GeneratedCode("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [DebuggerNonUserCode] + internal class ExceptionStringTable + { + private static ResourceManager ResourceMan; + + [EditorBrowsable(EditorBrowsableState.Advanced)] + internal static CultureInfo Culture { get; set; } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + internal static ResourceManager ResourceManager + { + get + { + if (ReferenceEquals(ResourceMan, null)) + { + var manager = new ResourceManager("Microsoft.Expression.Drawing.ExceptionStringTable", + typeof(ExceptionStringTable).Assembly); + ResourceMan = manager; + } + + return ResourceMan; + } + } + + internal static string TypeNotSupported => + ResourceManager.GetString("TypeNotSupported", Culture); + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/GeometryHelper.cs b/HandyControl/Expression/Drawing/GeometryHelper.cs new file mode 100644 index 00000000..8fdc12d9 --- /dev/null +++ b/HandyControl/Expression/Drawing/GeometryHelper.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Media; + +namespace HandyControl.Expression.Drawing +{ + public static class GeometryHelper + { + internal static void ApplyTransform(this IList points, GeneralTransform transform) + { + for (var i = 0; i < points.Count; i++) points[i] = transform.Transform(points[i]); + } + + internal static Rect Bounds(this Size size) + { + return new Rect(0.0, 0.0, size.Width, size.Height); + } + + internal static Point Center(this Rect rect) + { + return new Point(rect.X + rect.Width / 2.0, rect.Y + rect.Height / 2.0); + } + + internal static Vector Subtract(this Point lhs, Point rhs) + { + return new Vector(lhs.X - rhs.X, lhs.Y - rhs.Y); + } + + internal static Thickness Subtract(this Rect lhs, Rect rhs) + { + return new Thickness(rhs.Left - lhs.Left, + rhs.Top - lhs.Top, lhs.Right - rhs.Right, + lhs.Bottom - rhs.Bottom); + } + + internal static Point Lerp(Point pointA, Point pointB, double alpha) + { + return new Point( + MathHelper.Lerp(pointA.X, pointB.X, alpha), + MathHelper.Lerp(pointA.Y, pointB.Y, alpha)); + } + + internal static Vector Lerp(Vector vectorA, Vector vectorB, double alpha) + { + return new Vector(MathHelper.Lerp(vectorA.X, vectorB.X, alpha), + MathHelper.Lerp(vectorA.Y, vectorB.Y, alpha)); + } + + internal static double Distance(Point lhs, Point rhs) + { + var num = lhs.X - rhs.X; + var num2 = lhs.Y - rhs.Y; + return Math.Sqrt(num * num + num2 * num2); + } + + internal static Rect Resize(this Rect rect, double ratio) + { + return rect.Resize(ratio, ratio); + } + + internal static Rect Resize(this Rect rect, double ratioX, double ratioY) + { + var point = rect.Center(); + var width = rect.Width * ratioX; + var height = rect.Height * ratioY; + return new Rect(point.X - width / 2.0, point.Y - height / 2.0, width, height); + } + + internal static Rect GetStretchBound(Rect logicalBound, Stretch stretch, Size aspectRatio) + { + if (stretch == Stretch.None) stretch = Stretch.Fill; + if (stretch == Stretch.Fill || !aspectRatio.HasValidArea()) return logicalBound; + var point = logicalBound.Center(); + if (stretch == Stretch.Uniform) + { + if (aspectRatio.Width * logicalBound.Height < logicalBound.Width * aspectRatio.Height) + logicalBound.Width = logicalBound.Height * aspectRatio.Width / aspectRatio.Height; + else + logicalBound.Height = logicalBound.Width * aspectRatio.Height / aspectRatio.Width; + } + else if (stretch == Stretch.UniformToFill) + { + if (aspectRatio.Width * logicalBound.Height < logicalBound.Width * aspectRatio.Height) + logicalBound.Height = logicalBound.Width * aspectRatio.Height / aspectRatio.Width; + else + logicalBound.Width = logicalBound.Height * aspectRatio.Width / aspectRatio.Height; + } + + return new Rect(point.X - logicalBound.Width / 2.0, point.Y - logicalBound.Height / 2.0, + logicalBound.Width, logicalBound.Height); + } + + internal static Point GetArcPoint(double degree) + { + var a = degree * 3.1415926535897931 / 180.0; + return new Point(0.5 + 0.5 * Math.Sin(a), 0.5 - 0.5 * Math.Cos(a)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static Point GetArcPoint(double degree, Rect bound) + { + var arcPoint = GetArcPoint(degree); + return RelativeToAbsolutePoint(bound, arcPoint); + } + + internal static Point RelativeToAbsolutePoint(Rect bound, Point relative) + { + return new Point(bound.X + relative.X * bound.Width, bound.Y + relative.Y * bound.Height); + } + + internal static double SquaredDistance(Point lhs, Point rhs) + { + var num = lhs.X - rhs.X; + var num2 = lhs.Y - rhs.Y; + return num * num + num2 * num2; + } + + internal static Point Midpoint(Point lhs, Point rhs) + { + return new Point((lhs.X + rhs.X) / 2.0, (lhs.Y + rhs.Y) / 2.0); + } + + internal static bool HasValidArea(this Size size) + { + return size.Width > 0.0 && size.Height > 0.0 && !double.IsInfinity(size.Width) && + !double.IsInfinity(size.Height); + } + + internal static Rect Inflate(Rect rect, double offset) + { + return Inflate(rect, new Thickness(offset)); + } + + internal static Rect Inflate(Rect rect, Thickness thickness) + { + var width = rect.Width + thickness.Left + thickness.Right; + var height = rect.Height + thickness.Top + thickness.Bottom; + var x = rect.X - thickness.Left; + if (width < 0.0) + { + x += width / 2.0; + width = 0.0; + } + + var y = rect.Y - thickness.Top; + if (height < 0.0) + { + y += height / 2.0; + height = 0.0; + } + + return new Rect(x, y, width, height); + } + + internal static double Dot(Point lhs, Point rhs) + { + return lhs.X * rhs.X + lhs.Y * rhs.Y; + } + + internal static double Dot(Vector lhs, Vector rhs) + { + return lhs.X * rhs.X + lhs.Y * rhs.Y; + } + + internal static Point Plus(this Point lhs, Point rhs) + { + return new Point(lhs.X + rhs.X, lhs.Y + rhs.Y); + } + + internal static Point Minus(this Point lhs, Point rhs) + { + return new Point(lhs.X - rhs.X, lhs.Y - rhs.Y); + } + + internal static Vector Normal(Point lhs, Point rhs) + { + return new Vector(lhs.Y - rhs.Y, rhs.X - lhs.X).Normalized(); + } + + internal static Vector Normalized(this Vector vector) + { + var vector2 = new Vector(vector.X, vector.Y); + var length = vector2.Length; + if (MathHelper.IsVerySmall(length)) return new Vector(0.0, 1.0); + return vector2 / length; + } + + internal static double Determinant(Point lhs, Point rhs) + { + return lhs.X * rhs.Y - lhs.Y * rhs.X; + } + + internal static bool EnsureSegmentType(out T result, IList list, int index, + Func factory) where T : PathSegment + { + result = list[index] as T; + if (result == null) + { + list[index] = result = factory(); + return true; + } + + return false; + } + + internal static bool EnsureGeometryType(out T result, ref Geometry value, Func factory) + where T : Geometry + { + result = value as T; + if (result == null) + { + value = result = factory(); + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/MarchLocation.cs b/HandyControl/Expression/Drawing/MarchLocation.cs new file mode 100644 index 00000000..420104a2 --- /dev/null +++ b/HandyControl/Expression/Drawing/MarchLocation.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Windows; + +namespace HandyControl.Expression.Drawing +{ + internal class MarchLocation + { + public double After { get; private set; } + + public double Before { get; private set; } + + public int Index { get; private set; } + + public double Ratio { get; private set; } + + public MarchStopReason Reason { get; private set; } + + public double Remain { get; private set; } + + public static MarchLocation Create(MarchStopReason reason, int index, double before, double after, + double remain) + { + var num = before + after; + return new MarchLocation + { + Reason = reason, + Index = index, + Remain = remain, + Before = MathHelper.EnsureRange(before, 0.0, num), + After = MathHelper.EnsureRange(after, 0.0, num), + Ratio = MathHelper.EnsureRange(MathHelper.SafeDivide(before, num, 0.0), 0.0, 1.0) + }; + } + + public double GetArcLength(IList accumulatedLengths) + { + return MathHelper.Lerp(accumulatedLengths[Index], accumulatedLengths[Index + 1], Ratio); + } + + public Vector GetNormal(PolylineData polyline, double cornerRadius = 0.0) + { + return polyline.SmoothNormal(Index, Ratio, cornerRadius); + } + + public Point GetPoint(IList points) + { + return GeometryHelper.Lerp(points[Index], points[Index + 1], Ratio); + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/MarchStopReason.cs b/HandyControl/Expression/Drawing/MarchStopReason.cs new file mode 100644 index 00000000..7a5a53f7 --- /dev/null +++ b/HandyControl/Expression/Drawing/MarchStopReason.cs @@ -0,0 +1,9 @@ +namespace HandyControl.Expression.Drawing +{ + internal enum MarchStopReason + { + CompleteStep, + CompletePolyline, + CornerPoint + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/MathHelper.cs b/HandyControl/Expression/Drawing/MathHelper.cs new file mode 100644 index 00000000..2db4f04e --- /dev/null +++ b/HandyControl/Expression/Drawing/MathHelper.cs @@ -0,0 +1,65 @@ +using System; + +namespace HandyControl.Expression.Drawing +{ + internal static class MathHelper + { + public static bool AreClose(double value1, double value2) => + // ReSharper disable once CompareOfFloatsByEqualityOperator + value1 == value2 || IsVerySmall(value1 - value2); + + public static double Lerp(double x, double y, double alpha) => x * (1.0 - alpha) + y * alpha; + + public static bool IsVerySmall(double value) => Math.Abs(value) < 1E-06; + + public static bool IsFiniteDouble(double x) => !double.IsInfinity(x) && !double.IsNaN(x); + + public static double DoubleFromMantissaAndExponent(double x, int exp) => x * Math.Pow(2.0, exp); + + public static bool GreaterThan(double value1, double value2) => value1 > value2 && !AreClose(value1, value2); + + public static bool GreaterThanOrClose(double value1, double value2) + { + if (value1 <= value2) + { + return AreClose(value1, value2); + } + return true; + } + + public static double Hypotenuse(double x, double y) => Math.Sqrt(x * x + y * y); + + public static bool LessThan(double value1, double value2) => value1 < value2 && !AreClose(value1, value2); + + public static bool LessThanOrClose(double value1, double value2) + { + if (value1 >= value2) + { + return AreClose(value1, value2); + } + return true; + } + + public static double EnsureRange(double value, double? min, double? max) + { + if (min.HasValue && value < min.Value) + { + return min.Value; + } + if (max.HasValue && value > max.Value) + { + return max.Value; + } + return value; + } + + public static double SafeDivide(double lhs, double rhs, double fallback) + { + if (!IsVerySmall(rhs)) + { + return lhs / rhs; + } + return fallback; + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/PathFigureHelper.cs b/HandyControl/Expression/Drawing/PathFigureHelper.cs new file mode 100644 index 00000000..40c906ce --- /dev/null +++ b/HandyControl/Expression/Drawing/PathFigureHelper.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Media; + +namespace HandyControl.Expression.Drawing +{ + internal static class PathFigureHelper + { + public static IEnumerable AllSegments(this PathFigure figure) + { + if (figure != null && figure.Segments.Count > 0) + { + var startPoint = figure.StartPoint; + // ReSharper disable once GenericEnumeratorNotDisposed + var enumerator = figure.Segments.GetEnumerator(); + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + var lastPoint = current.GetLastPoint(); + yield return new PathSegmentData(startPoint, current); + startPoint = lastPoint; + } + } + } + + internal static void ApplyTransform(this PathFigure figure, GeneralTransform transform) + { + figure.StartPoint = transform.Transform(figure.StartPoint); + for (var i = 0; i < figure.Segments.Count; i++) + { + var segment = + PathSegmentHelper.ApplyTransform(figure.Segments[i], figure.StartPoint, transform); + if (!Equals(figure.Segments[i], segment)) figure.Segments[i] = segment; + } + } + + internal static void FlattenFigure(PathFigure figure, IList points, double tolerance, + bool removeRepeat) + { + if (figure == null) throw new ArgumentNullException(nameof(figure)); + if (points == null) throw new ArgumentNullException(nameof(points)); + if (tolerance < 0.0) throw new ArgumentOutOfRangeException(nameof(tolerance)); + var list = removeRepeat ? new List() : points; + list.Add(figure.StartPoint); + foreach (var data in figure.AllSegments()) + data.PathSegment.FlattenSegment(list, data.StartPoint, tolerance); + if (figure.IsClosed) list.Add(figure.StartPoint); + if (removeRepeat && list.Count > 0) + { + points.Add(list[0]); + for (var i = 1; i < list.Count; i++) + if (!MathHelper.IsVerySmall(GeometryHelper.SquaredDistance(points.Last(), list[i]))) + points.Add(list[i]); + } + } + + internal static bool SyncEllipseFigure(PathFigure figure, Rect bounds, SweepDirection sweepDirection, + bool isFilled = true) + { + var flag = false; + var pointArray = new Point[2]; + var size = new Size(bounds.Width / 2.0, bounds.Height / 2.0); + var point = bounds.Center(); + if (size.Width > size.Height) + { + pointArray[0] = new Point(bounds.Left, point.Y); + pointArray[1] = new Point(bounds.Right, point.Y); + } + else + { + pointArray[0] = new Point(point.X, bounds.Top); + pointArray[1] = new Point(point.X, bounds.Bottom); + } + + flag |= figure.SetIfDifferent(PathFigure.IsClosedProperty, true); + flag |= figure.SetIfDifferent(PathFigure.IsFilledProperty, isFilled); + flag |= figure.SetIfDifferent(PathFigure.StartPointProperty, pointArray[0]); + flag |= figure.Segments.EnsureListCount(2, () => new ArcSegment()); + flag |= GeometryHelper.EnsureSegmentType(out var segment, figure.Segments, 0, () => new ArcSegment()); + flag |= segment.SetIfDifferent(ArcSegment.PointProperty, pointArray[1]); + flag |= segment.SetIfDifferent(ArcSegment.SizeProperty, size); + flag |= segment.SetIfDifferent(ArcSegment.IsLargeArcProperty, false); + flag |= segment.SetIfDifferent(ArcSegment.SweepDirectionProperty, sweepDirection); + flag |= GeometryHelper.EnsureSegmentType(out segment, figure.Segments, 1, () => new ArcSegment()); + flag |= segment.SetIfDifferent(ArcSegment.PointProperty, pointArray[0]); + flag |= segment.SetIfDifferent(ArcSegment.SizeProperty, size); + flag |= segment.SetIfDifferent(ArcSegment.IsLargeArcProperty, false); + return flag | segment.SetIfDifferent(ArcSegment.SweepDirectionProperty, sweepDirection); + } + + internal static bool SyncPolylineFigure(PathFigure figure, IList points, bool isClosed, + bool isFilled = true) + { + if (figure == null) throw new ArgumentNullException(nameof(figure)); + var flag = false; + if (points == null || points.Count == 0) + { + flag |= figure.ClearIfSet(PathFigure.StartPointProperty); + flag |= figure.Segments.EnsureListCount(0); + } + else + { + flag |= figure.SetIfDifferent(PathFigure.StartPointProperty, points[0]); + flag |= figure.Segments.EnsureListCount(1, () => new PolyLineSegment()); + flag |= PathSegmentHelper.SyncPolylineSegment(figure.Segments, 0, points, 1, + points.Count - 1); + } + + flag |= figure.SetIfDifferent(PathFigure.IsClosedProperty, isClosed); + return flag | figure.SetIfDifferent(PathFigure.IsFilledProperty, isFilled); + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/PathGeometryHelper.cs b/HandyControl/Expression/Drawing/PathGeometryHelper.cs new file mode 100644 index 00000000..d9307403 --- /dev/null +++ b/HandyControl/Expression/Drawing/PathGeometryHelper.cs @@ -0,0 +1,25 @@ +using System.Windows.Media; + +namespace HandyControl.Expression.Drawing +{ + internal static class PathGeometryHelper + { + public static bool IsStroked(this PathSegment pathSegment) => pathSegment.IsStroked; + + public static PathGeometry AsPathGeometry(this Geometry original) + { + if (original == null) + { + return null; + } + + if (!(original is PathGeometry geometry)) + { + return PathGeometry.CreateFromGeometry(original); + } + return geometry; + } + + internal static Geometry FixPathGeometryBoundary(Geometry geometry) => geometry; + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/PathSegmentData.cs b/HandyControl/Expression/Drawing/PathSegmentData.cs new file mode 100644 index 00000000..6e7bb7b2 --- /dev/null +++ b/HandyControl/Expression/Drawing/PathSegmentData.cs @@ -0,0 +1,17 @@ +using System.Windows; + +namespace HandyControl.Expression.Drawing +{ + internal sealed class PathSegmentData + { + public PathSegmentData(Point startPoint, System.Windows.Media.PathSegment pathSegment) + { + PathSegment = pathSegment; + StartPoint = startPoint; + } + + public System.Windows.Media.PathSegment PathSegment { get; } + + public Point StartPoint { get; } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/PathSegmentHelper.cs b/HandyControl/Expression/Drawing/PathSegmentHelper.cs new file mode 100644 index 00000000..e64fce79 --- /dev/null +++ b/HandyControl/Expression/Drawing/PathSegmentHelper.cs @@ -0,0 +1,755 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Media; + +namespace HandyControl.Expression.Drawing +{ + internal static class PathSegmentHelper + { + public static PathSegment ApplyTransform(PathSegment segment, Point start, GeneralTransform transform) + { + return PathSegmentImplementation.Create(segment, start).ApplyTransform(transform); + } + + public static PathSegment ArcToBezierSegments(ArcSegment arcSegment, Point startPoint) + { + var isStroked = arcSegment.IsStroked(); + ArcToBezierHelper.ArcToBezier(startPoint.X, startPoint.Y, arcSegment.Size.Width, + arcSegment.Size.Height, arcSegment.RotationAngle, arcSegment.IsLargeArc, + arcSegment.SweepDirection == SweepDirection.Clockwise, arcSegment.Point.X, arcSegment.Point.Y, + out var pointArray, out var num); + switch (num) + { + case -1: + return null; + + case 0: + return CreateLineSegment(arcSegment.Point, isStroked); + + case 1: + return CreateBezierSegment(pointArray[0], pointArray[1], pointArray[2], isStroked); + } + + return CreatePolyBezierSegment(pointArray, 0, num * 3, isStroked); + } + + public static ArcSegment CreateArcSegment(Point point, Size size, bool isLargeArc, bool clockwise, + double rotationAngle = 0.0, bool isStroked = true) + { + var dependencyObject = new ArcSegment(); + dependencyObject.SetIfDifferent(ArcSegment.PointProperty, point); + dependencyObject.SetIfDifferent(ArcSegment.SizeProperty, size); + dependencyObject.SetIfDifferent(ArcSegment.IsLargeArcProperty, isLargeArc); + dependencyObject.SetIfDifferent(ArcSegment.SweepDirectionProperty, + clockwise ? SweepDirection.Clockwise : SweepDirection.Counterclockwise); + dependencyObject.SetIfDifferent(ArcSegment.RotationAngleProperty, rotationAngle); + dependencyObject.SetIsStroked(isStroked); + return dependencyObject; + } + + public static BezierSegment CreateBezierSegment(Point point1, Point point2, Point point3, + bool isStroked = true) + { + var segment = new BezierSegment + { + Point1 = point1, + Point2 = point2, + Point3 = point3 + }; + segment.SetIsStroked(isStroked); + return segment; + } + + public static LineSegment CreateLineSegment(Point point, bool isStroked = true) + { + var segment = new LineSegment + { + Point = point + }; + segment.SetIsStroked(isStroked); + return segment; + } + + public static PolyBezierSegment CreatePolyBezierSegment(IList points, int start, int count, + bool isStroked = true) + { + if (points == null) throw new ArgumentNullException(nameof(points)); + count = count / 3 * 3; + if (count < 0 || points.Count < start + count) throw new ArgumentOutOfRangeException(nameof(count)); + var segment = new PolyBezierSegment + { + Points = new PointCollection() + }; + for (var i = 0; i < count; i++) segment.Points.Add(points[start + i]); + segment.SetIsStroked(isStroked); + return segment; + } + + public static PolyLineSegment CreatePolylineSegment(IList points, int start, int count, + bool isStroked = true) + { + if (count < 0 || points.Count < start + count) throw new ArgumentOutOfRangeException(nameof(count)); + var segment = new PolyLineSegment + { + Points = new PointCollection() + }; + for (var i = 0; i < count; i++) segment.Points.Add(points[start + i]); + segment.SetIsStroked(isStroked); + return segment; + } + + public static PolyQuadraticBezierSegment CreatePolyQuadraticBezierSegment(IList points, + int start, int count, bool isStroked = true) + { + if (points == null) throw new ArgumentNullException(nameof(points)); + count = count / 2 * 2; + if (count < 0 || points.Count < start + count) throw new ArgumentOutOfRangeException(nameof(count)); + var segment = new PolyQuadraticBezierSegment + { + Points = new PointCollection() + }; + for (var i = 0; i < count; i++) segment.Points.Add(points[start + i]); + segment.SetIsStroked(isStroked); + return segment; + } + + public static QuadraticBezierSegment CreateQuadraticBezierSegment(Point point1, Point point2, + bool isStroked = true) + { + var segment = new QuadraticBezierSegment + { + Point1 = point1, + Point2 = point2 + }; + segment.SetIsStroked(isStroked); + return segment; + } + + public static void FlattenSegment(this PathSegment segment, IList points, Point start, + double tolerance) + { + PathSegmentImplementation.Create(segment, start).Flatten(points, tolerance); + } + + public static Point GetLastPoint(this PathSegment segment) + { + return segment.GetPoint(-1); + } + + public static Point GetPoint(this PathSegment segment, int index) + { + return PathSegmentImplementation.Create(segment).GetPoint(index); + } + + public static int GetPointCount(this PathSegment segment) + { + if (segment is ArcSegment) return 1; + if (segment is LineSegment) return 1; + if (segment is QuadraticBezierSegment) return 2; + if (segment is BezierSegment) return 3; + if (segment is PolyLineSegment segment2) return segment2.Points.Count; + if (segment is PolyQuadraticBezierSegment segment3) return segment3.Points.Count / 2 * 2; + if (segment is PolyBezierSegment segment4) return segment4.Points.Count / 3 * 3; + return 0; + } + + public static IEnumerable GetSimpleSegments(this PathSegment segment, Point start) + { + return PathSegmentImplementation.Create(segment, start).GetSimpleSegments(); + } + + public static bool IsEmpty(this PathSegment segment) + { + return segment.GetPointCount() == 0; + } + + private static void SetIsStroked(this PathSegment segment, bool isStroked) + { + if (segment.IsStroked != isStroked) segment.IsStroked = isStroked; + } + + public static bool SyncPolyBezierSegment(PathSegmentCollection collection, int index, + IList points, int start, int count) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (index < 0 || index >= collection.Count) throw new ArgumentOutOfRangeException(nameof(index)); + if (points == null) throw new ArgumentNullException(nameof(points)); + if (start < 0) throw new ArgumentOutOfRangeException(nameof(start)); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); + if (points.Count < start + count) throw new ArgumentOutOfRangeException(nameof(count)); + var flag = false; + count = count / 3 * 3; + var segment = collection[index] as PolyBezierSegment; + if (segment == null) + { + collection[index] = segment = new PolyBezierSegment(); + flag = true; + } + + segment.Points.EnsureListCount(count); + for (var i = 0; i < count; i++) + if (segment.Points[i] != points[i + start]) + { + segment.Points[i] = points[i + start]; + flag = true; + } + + return flag; + } + + public static bool SyncPolylineSegment(PathSegmentCollection collection, int index, + IList points, int start, int count) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (index < 0 || index >= collection.Count) throw new ArgumentOutOfRangeException(nameof(index)); + if (points == null) throw new ArgumentNullException(nameof(points)); + if (start < 0) throw new ArgumentOutOfRangeException(nameof(start)); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); + if (points.Count < start + count) throw new ArgumentOutOfRangeException(nameof(count)); + var flag = false; + var segment = collection[index] as PolyLineSegment; + if (segment == null) + { + collection[index] = segment = new PolyLineSegment(); + flag = true; + } + + flag |= segment.Points.EnsureListCount(count); + for (var i = 0; i < count; i++) + if (segment.Points[i] != points[i + start]) + { + segment.Points[i] = points[i + start]; + flag = true; + } + + return flag; + } + + + private class ArcSegmentImplementation : PathSegmentImplementation + { + private ArcSegment _segment; + + public override PathSegment ApplyTransform(GeneralTransform transform) + { + var segment = ArcToBezierSegments(_segment, Start); + if (segment != null) return PathSegmentHelper.ApplyTransform(segment, Start, transform); + _segment.Point = transform.Transform(_segment.Point); + return _segment; + } + + public static PathSegmentImplementation Create(ArcSegment source) + { + if (source != null) return new ArcSegmentImplementation {_segment = source}; + return null; + } + + public override void Flatten(IList points, double tolerance) + { + var segment = ArcToBezierSegments(_segment, Start); + segment?.FlattenSegment(points, Start, tolerance); + } + + public override Point GetPoint(int index) + { + if (index < -1 || index > 0) throw new ArgumentOutOfRangeException(nameof(index)); + return _segment.Point; + } + + public override IEnumerable GetSimpleSegments() + { + var segment = ArcToBezierSegments(_segment, Start); + if (segment != null) return segment.GetSimpleSegments(Start); + return Enumerable.Empty(); + } + } + + + private static class ArcToBezierHelper + { + private static bool AcceptRadius(double rHalfChord2, double rFuzz2, ref double rRadius) + { + var flag = rRadius * rRadius > rHalfChord2 * rFuzz2; + if (flag && rRadius < 0.0) rRadius = -rRadius; + return flag; + } + + public static void ArcToBezier(double xStart, double yStart, double xRadius, double yRadius, + double rRotation, bool fLargeArc, bool fSweepUp, double xEnd, double yEnd, out Point[] pPt, + out int cPieces) + { + var num = 1E-06; + pPt = new Point[12]; + var num12 = num * num; + var flag = false; + cPieces = -1; + var num2 = 0.5 * (xEnd - xStart); + var num3 = 0.5 * (yEnd - yStart); + var num4 = num2 * num2 + num3 * num3; + if (num4 >= num12) + { + if (!AcceptRadius(num4, num12, ref xRadius) || !AcceptRadius(num4, num12, ref yRadius)) + { + cPieces = 0; + } + else + { + double num5; + double num6; + // ReSharper disable once InlineOutVariableDeclaration + double num7; + // ReSharper disable once InlineOutVariableDeclaration + double num8; + double num9; + double num10; + Point point4; + if (Math.Abs(rRotation) < num) + { + num5 = 1.0; + num6 = 0.0; + } + else + { + rRotation = -rRotation * 3.1415926535897931 / 180.0; + num5 = Math.Cos(rRotation); + num6 = Math.Sin(rRotation); + var num15 = num2 * num5 - num3 * num6; + num3 = num2 * num6 + num3 * num5; + num2 = num15; + } + + num2 /= xRadius; + num3 /= yRadius; + num4 = num2 * num2 + num3 * num3; + if (num4 > 1.0) + { + var num16 = Math.Sqrt(num4); + xRadius *= num16; + yRadius *= num16; + num9 = num10 = 0.0; + flag = true; + num2 /= num16; + num3 /= num16; + } + else + { + var num17 = Math.Sqrt((1.0 - num4) / num4); + if (fLargeArc != fSweepUp) + { + num9 = -num17 * num3; + num10 = num17 * num2; + } + else + { + num9 = num17 * num3; + num10 = -num17 * num2; + } + } + + var ptStart = new Point(-num2 - num9, -num3 - num10); + var ptEnd = new Point(num2 - num9, num3 - num10); + var matrix = new Matrix(num5 * xRadius, -num6 * xRadius, num6 * yRadius, + num5 * yRadius, 0.5 * (xEnd + xStart), 0.5 * (yEnd + yStart)); + if (!flag) + { + matrix.OffsetX += matrix.M11 * num9 + matrix.M21 * num10; + matrix.OffsetY += matrix.M12 * num9 + matrix.M22 * num10; + } + + GetArcAngle(ptStart, ptEnd, fLargeArc, fSweepUp, out num7, out num8, out cPieces); + var bezierDistance = GetBezierDistance(num7); + if (!fSweepUp) bezierDistance = -bezierDistance; + var rhs = new Point(-bezierDistance * ptStart.Y, bezierDistance * ptStart.X); + var index = 0; + pPt = new Point[cPieces * 3]; + for (var i = 1; i < cPieces; i++) + { + var lhs = new Point(ptStart.X * num7 - ptStart.Y * num8, + ptStart.X * num8 + ptStart.Y * num7); + point4 = new Point(-bezierDistance * lhs.Y, bezierDistance * lhs.X); + pPt[index++] = matrix.Transform(ptStart.Plus(rhs)); + pPt[index++] = matrix.Transform(lhs.Minus(point4)); + pPt[index++] = matrix.Transform(lhs); + ptStart = lhs; + rhs = point4; + } + + point4 = new Point(-bezierDistance * ptEnd.Y, bezierDistance * ptEnd.X); + pPt[index++] = matrix.Transform(ptStart.Plus(rhs)); + pPt[index++] = matrix.Transform(ptEnd.Minus(point4)); + pPt[index] = new Point(xEnd, yEnd); + } + } + } + + private static void GetArcAngle(Point ptStart, Point ptEnd, bool fLargeArc, bool fSweepUp, + out double rCosArcAngle, out double rSinArcAngle, out int cPieces) + { + rCosArcAngle = GeometryHelper.Dot(ptStart, ptEnd); + rSinArcAngle = GeometryHelper.Determinant(ptStart, ptEnd); + if (rCosArcAngle >= 0.0) + { + if (!fLargeArc) + { + cPieces = 1; + return; + } + + cPieces = 4; + } + else if (fLargeArc) + { + cPieces = 3; + } + else + { + cPieces = 2; + } + + var d = Math.Atan2(rSinArcAngle, rCosArcAngle); + if (fSweepUp) + { + if (d < 0.0) d += 6.2831853071795862; + } + else if (d > 0.0) + { + d -= 6.2831853071795862; + } + + d /= cPieces; + rCosArcAngle = Math.Cos(d); + rSinArcAngle = Math.Sin(d); + } + + private static double GetBezierDistance(double rDot, double rRadius = 1.0) + { + var num = rRadius * rRadius; + var num5 = 0.0; + var d = 0.5 * (num + rDot); + if (d < 0.0) return num5; + var num3 = num - d; + if (num3 <= 0.0) return num5; + var num4 = Math.Sqrt(num3); + var num2 = 4.0 * (rRadius - Math.Sqrt(d)) / 3.0; + if (num2 <= num4 * 1E-06) return 0.0; + return num2 / num4; + } + } + + + private class BezierSegmentImplementation : PathSegmentImplementation + { + private BezierSegment _segment; + + public override PathSegment ApplyTransform(GeneralTransform transform) + { + _segment.Point1 = transform.Transform(_segment.Point1); + _segment.Point2 = transform.Transform(_segment.Point2); + _segment.Point3 = transform.Transform(_segment.Point3); + return _segment; + } + + public static PathSegmentImplementation Create(BezierSegment source) + { + if (source != null) return new BezierSegmentImplementation {_segment = source}; + return null; + } + + public override void Flatten(IList points, double tolerance) + { + Point[] controlPoints = {Start, _segment.Point1, _segment.Point2, _segment.Point3}; + var resultPolyline = new List(); + BezierCurveFlattener.FlattenCubic(controlPoints, tolerance, resultPolyline, true); + points.AddRange(resultPolyline); + } + + public override Point GetPoint(int index) + { + if (index < -1 || index > 2) throw new ArgumentOutOfRangeException(nameof(index)); + if (index == 0) return _segment.Point1; + if (index == 1) return _segment.Point2; + return _segment.Point3; + } + + public override IEnumerable GetSimpleSegments() + { + yield return SimpleSegment.Create(Start, _segment.Point1, _segment.Point2, _segment.Point3); + } + } + + + private class LineSegmentImplementation : PathSegmentImplementation + { + private LineSegment _segment; + + public override PathSegment ApplyTransform(GeneralTransform transform) + { + _segment.Point = transform.Transform(_segment.Point); + return _segment; + } + + public static PathSegmentImplementation Create(LineSegment source) + { + if (source != null) return new LineSegmentImplementation {_segment = source}; + return null; + } + + public override void Flatten(IList points, double tolerance) + { + points.Add(_segment.Point); + } + + public override Point GetPoint(int index) + { + if (index < -1 || index > 0) throw new ArgumentOutOfRangeException(nameof(index)); + return _segment.Point; + } + + public override IEnumerable GetSimpleSegments() + { + yield return SimpleSegment.Create(Start, _segment.Point); + } + } + + + private abstract class PathSegmentImplementation + { + protected Point Start { get; private set; } + + public abstract PathSegment ApplyTransform(GeneralTransform transform); + + public static PathSegmentImplementation Create(PathSegment segment) + { + PathSegmentImplementation implementation; + if ((implementation = BezierSegmentImplementation.Create(segment as BezierSegment)) == null && + (implementation = LineSegmentImplementation.Create(segment as LineSegment)) == null && + (implementation = ArcSegmentImplementation.Create(segment as ArcSegment)) == null && + (implementation = PolyLineSegmentImplementation.Create(segment as PolyLineSegment)) == + null && + (implementation = PolyBezierSegmentImplementation.Create(segment as PolyBezierSegment)) == + null && + (implementation = + QuadraticBezierSegmentImplementation.Create(segment as QuadraticBezierSegment)) == + null && + (implementation = + PolyQuadraticBezierSegmentImplementation.Create(segment as PolyQuadraticBezierSegment) + ) == null) + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, + ExceptionStringTable.TypeNotSupported, new object[] {segment.GetType().FullName})); + return implementation; + } + + public static PathSegmentImplementation Create(PathSegment segment, Point start) + { + var implementation = Create(segment); + implementation.Start = start; + return implementation; + } + + public abstract void Flatten(IList points, double tolerance); + + public abstract Point GetPoint(int index); + + public abstract IEnumerable GetSimpleSegments(); + } + + + private class PolyBezierSegmentImplementation : PathSegmentImplementation + { + private PolyBezierSegment _segment; + + public override PathSegment ApplyTransform(GeneralTransform transform) + { + _segment.Points.ApplyTransform(transform); + return _segment; + } + + public static PathSegmentImplementation Create(PolyBezierSegment source) + { + if (source != null) return new PolyBezierSegmentImplementation {_segment = source}; + return null; + } + + public override void Flatten(IList points, double tolerance) + { + var start = Start; + var num = _segment.Points.Count / 3 * 3; + for (var i = 0; i < num; i += 3) + { + Point[] controlPoints = {start, _segment.Points[i], _segment.Points[i + 1], _segment.Points[i + 2]}; + var resultPolyline = new List(); + BezierCurveFlattener.FlattenCubic(controlPoints, tolerance, resultPolyline, true); + points.AddRange(resultPolyline); + start = _segment.Points[i + 2]; + } + } + + public override Point GetPoint(int index) + { + var num = _segment.Points.Count / 3 * 3; + if (index < -1 || index > num - 1) throw new ArgumentOutOfRangeException(nameof(index)); + if (index != -1) return _segment.Points[index]; + return _segment.Points[num - 1]; + } + + public override IEnumerable GetSimpleSegments() + { + var start = Start; + IList points = _segment.Points; + var iteratorVariable2 = _segment.Points.Count / 3; + var iteratorVariable3 = 0; + while (true) + { + if (iteratorVariable3 >= iteratorVariable2) yield break; + var iteratorVariable4 = iteratorVariable3 * 3; + yield return SimpleSegment.Create(start, points[iteratorVariable4], + points[iteratorVariable4 + 1], points[iteratorVariable4 + 2]); + start = points[iteratorVariable4 + 2]; + iteratorVariable3++; + } + } + } + + + private class PolyLineSegmentImplementation : PathSegmentImplementation + { + private PolyLineSegment _segment; + + public override PathSegment ApplyTransform(GeneralTransform transform) + { + _segment.Points.ApplyTransform(transform); + return _segment; + } + + public static PathSegmentImplementation Create(PolyLineSegment source) + { + if (source != null) return new PolyLineSegmentImplementation {_segment = source}; + return null; + } + + public override void Flatten(IList points, double tolerance) + { + points.AddRange(_segment.Points); + } + + public override Point GetPoint(int index) + { + if (index < -1 || index > _segment.Points.Count - 1) + throw new ArgumentOutOfRangeException(nameof(index)); + if (index != -1) return _segment.Points[index]; + return _segment.Points.Last(); + } + + public override IEnumerable GetSimpleSegments() + { + var start = Start; + // ReSharper disable once GenericEnumeratorNotDisposed + var enumerator = _segment.Points.GetEnumerator(); + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + yield return SimpleSegment.Create(start, current); + start = current; + } + } + } + + + private class PolyQuadraticBezierSegmentImplementation : PathSegmentImplementation + { + private PolyQuadraticBezierSegment _segment; + + public override PathSegment ApplyTransform(GeneralTransform transform) + { + _segment.Points.ApplyTransform(transform); + return _segment; + } + + public static PathSegmentImplementation Create(PolyQuadraticBezierSegment source) + { + if (source != null) return new PolyQuadraticBezierSegmentImplementation {_segment = source}; + return null; + } + + public override void Flatten(IList points, double tolerance) + { + var start = Start; + var num = _segment.Points.Count / 2 * 2; + for (var i = 0; i < num; i += 2) + { + Point[] controlPoints = {start, _segment.Points[i], _segment.Points[i + 1]}; + var resultPolyline = new List(); + BezierCurveFlattener.FlattenQuadratic(controlPoints, tolerance, resultPolyline, true); + points.AddRange(resultPolyline); + start = _segment.Points[i + 1]; + } + } + + public override Point GetPoint(int index) + { + var num = _segment.Points.Count / 2 * 2; + if (index < -1 || index > num - 1) throw new ArgumentOutOfRangeException(nameof(index)); + if (index != -1) return _segment.Points[index]; + return _segment.Points[num - 1]; + } + + public override IEnumerable GetSimpleSegments() + { + var start = Start; + IList points = _segment.Points; + var iteratorVariable2 = _segment.Points.Count / 2; + var iteratorVariable3 = 0; + while (true) + { + if (iteratorVariable3 >= iteratorVariable2) yield break; + var iteratorVariable4 = iteratorVariable3 * 2; + yield return SimpleSegment.Create(start, points[iteratorVariable4], + points[iteratorVariable4 + 1]); + start = points[iteratorVariable4 + 1]; + iteratorVariable3++; + } + } + } + + + private class QuadraticBezierSegmentImplementation : PathSegmentImplementation + { + private QuadraticBezierSegment _segment; + + public override PathSegment ApplyTransform(GeneralTransform transform) + { + _segment.Point1 = transform.Transform(_segment.Point1); + _segment.Point2 = transform.Transform(_segment.Point2); + return _segment; + } + + public static PathSegmentImplementation Create(QuadraticBezierSegment source) + { + if (source != null) return new QuadraticBezierSegmentImplementation {_segment = source}; + return null; + } + + public override void Flatten(IList points, double tolerance) + { + Point[] controlPoints = {Start, _segment.Point1, _segment.Point2}; + var resultPolyline = new List(); + BezierCurveFlattener.FlattenQuadratic(controlPoints, tolerance, resultPolyline, true); + points.AddRange(resultPolyline); + } + + public override Point GetPoint(int index) + { + if (index < -1 || index > 1) throw new ArgumentOutOfRangeException(nameof(index)); + return index == 0 ? _segment.Point1 : _segment.Point2; + } + + public override IEnumerable GetSimpleSegments() + { + yield return SimpleSegment.Create(Start, _segment.Point1, _segment.Point2); + } + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/PolylineData.cs b/HandyControl/Expression/Drawing/PolylineData.cs new file mode 100644 index 00000000..7520e267 --- /dev/null +++ b/HandyControl/Expression/Drawing/PolylineData.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; + +namespace HandyControl.Expression.Drawing +{ + internal class PolylineData + { + private IList _accumulates; + + private IList _angles; + + private IList _lengths; + + private IList _normals; + + private double? _totalLength; + + public PolylineData(IList points) + { + if (points == null) throw new ArgumentNullException(nameof(points)); + if (points.Count <= 1) throw new ArgumentOutOfRangeException(nameof(points)); + Points = points; + } + + public IList AccumulatedLength => + _accumulates ?? ComputeAccumulatedLength(); + + public IList Angles => + _angles ?? ComputeAngles(); + + public int Count => + Points.Count; + + public bool IsClosed => + Points[0] == Points.Last(); + + public IList Lengths => + _lengths ?? ComputeLengths(); + + public IList Normals => + _normals ?? ComputeNormals(); + + public IList Points { get; } + + public double TotalLength + { + get + { + var length = _totalLength; + return length ?? ComputeTotalLength(); + } + } + + private IList ComputeAccumulatedLength() + { + _accumulates = new double[Count]; + _accumulates[0] = 0.0; + for (var i = 1; i < Count; i++) _accumulates[i] = _accumulates[i - 1] + Lengths[i - 1]; + _totalLength = _accumulates.Last(); + return _accumulates; + } + + private IList ComputeAngles() + { + _angles = new double[Count]; + for (var i = 1; i < Count - 1; i++) _angles[i] = -GeometryHelper.Dot(Normals[i - 1], Normals[i]); + if (IsClosed) + { + var num2 = -GeometryHelper.Dot(Normals[0], Normals[Count - 2]); + _angles[0] = _angles[Count - 1] = num2; + } + else + { + _angles[0] = _angles[Count - 1] = 1.0; + } + + return _angles; + } + + private IList ComputeLengths() + { + _lengths = new double[Count]; + for (var i = 0; i < Count; i++) _lengths[i] = Difference(i).Length; + return _lengths; + } + + private IList ComputeNormals() + { + _normals = new Vector[Points.Count]; + for (var i = 0; i < Count - 1; i++) _normals[i] = GeometryHelper.Normal(Points[i], Points[i + 1]); + _normals[Count - 1] = _normals[Count - 2]; + return _normals; + } + + private double ComputeTotalLength() + { + ComputeAccumulatedLength(); + // ReSharper disable once PossibleInvalidOperationException + return _totalLength.Value; + } + + public Vector Difference(int index) + { + var num = (index + 1) % Count; + return Points[num].Subtract(Points[index]); + } + + public Vector SmoothNormal(int index, double fraction, double cornerRadius) + { + if (cornerRadius > 0.0) + { + var num = Lengths[index]; + if (MathHelper.IsVerySmall(num)) + { + var num2 = index - 1; + if (num2 < 0 && IsClosed) num2 = Count - 1; + var num3 = index + 1; + if (IsClosed && num3 >= Count - 1) num3 = 0; + if (num2 >= 0 && num3 < Count) + return GeometryHelper.Lerp(Normals[num3], Normals[num2], 0.5).Normalized(); + return Normals[index]; + } + + var num4 = Math.Min(cornerRadius / num, 0.5); + if (fraction <= num4) + { + var num5 = index - 1; + if (IsClosed && num5 == -1) num5 = Count - 1; + if (num5 >= 0) + { + var alpha = (num4 - fraction) / (2.0 * num4); + return GeometryHelper.Lerp(Normals[index], Normals[num5], alpha).Normalized(); + } + } + else if (fraction >= 1.0 - num4) + { + var num7 = index + 1; + if (IsClosed && num7 >= Count - 1) num7 = 0; + if (num7 < Count) + { + var num8 = (fraction + num4 - 1.0) / (2.0 * num4); + return GeometryHelper.Lerp(Normals[index], Normals[num7], num8).Normalized(); + } + } + } + + return Normals[index]; + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/PolylineHelper.cs b/HandyControl/Expression/Drawing/PolylineHelper.cs new file mode 100644 index 00000000..b82d8928 --- /dev/null +++ b/HandyControl/Expression/Drawing/PolylineHelper.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HandyControl.Expression.Drawing +{ + internal static class PolylineHelper + { + public static IEnumerable GetWrappedPolylines(IList lines, + ref double startArcLength) + { + var count = 0; + for (var i = 0; i < lines.Count; i++) + { + count = i; + startArcLength -= lines[i].TotalLength; + if (MathHelper.LessThanOrClose(startArcLength, 0.0)) break; + } + + if (!MathHelper.LessThanOrClose(startArcLength, 0.0)) + throw new ArgumentOutOfRangeException(nameof(startArcLength)); + startArcLength += lines[count].TotalLength; + return lines.Skip(count).Concat(lines.Take(count + 1)); + } + + public static void PathMarch(PolylineData polyline, double startArcLength, double cornerThreshold, + Func stopCallback) + { + if (polyline == null) throw new ArgumentNullException(nameof(polyline)); + var count = polyline.Count; + if (count <= 1) throw new ArgumentOutOfRangeException(nameof(polyline)); + var flag = false; + var x = startArcLength; + var before = 0.0; + var index = 0; + var num5 = Math.Cos(cornerThreshold * 3.1415926535897931 / 180.0); + while (true) + { + var num6 = polyline.Lengths[index]; + if (!MathHelper.IsFiniteDouble(x)) return; + if (MathHelper.IsVerySmall(x)) + { + x = stopCallback(MarchLocation.Create(MarchStopReason.CompleteStep, index, before, + num6 - before, x)); + flag = true; + } + else if (MathHelper.GreaterThan(x, 0.0)) + { + if (MathHelper.LessThanOrClose(x + before, num6)) + { + before += x; + x = stopCallback(MarchLocation.Create(MarchStopReason.CompleteStep, index, before, + num6 - before, 0.0)); + flag = true; + } + else if (index < count - 2) + { + index++; + var num7 = num6 - before; + x -= num7; + before = 0.0; + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (flag && num5 != 1.0 && polyline.Angles[index] > num5) + { + num6 = polyline.Lengths[index]; + x = stopCallback(MarchLocation.Create(MarchStopReason.CornerPoint, index, before, + num6 - before, x)); + } + } + else + { + var num8 = num6 - before; + x -= num8; + num6 = polyline.Lengths[index]; + before = polyline.Lengths[index]; + x = stopCallback(MarchLocation.Create(MarchStopReason.CompletePolyline, index, before, + num6 - before, x)); + flag = true; + } + } + else if (MathHelper.LessThan(x, 0.0)) + { + if (MathHelper.GreaterThanOrClose(x + before, 0.0)) + { + before += x; + x = stopCallback(MarchLocation.Create(MarchStopReason.CompleteStep, index, before, + num6 - before, 0.0)); + flag = true; + } + else if (index > 0) + { + index--; + x += before; + before = polyline.Lengths[index]; + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (flag && num5 != 1.0 && polyline.Angles[index + 1] > num5) + { + num6 = polyline.Lengths[index]; + x = stopCallback(MarchLocation.Create(MarchStopReason.CornerPoint, index, before, + num6 - before, x)); + } + } + else + { + x += before; + num6 = polyline.Lengths[index]; + before = 0.0; + x = stopCallback(MarchLocation.Create(MarchStopReason.CompletePolyline, index, before, + num6 - before, x)); + flag = true; + } + } + } + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/RandomEngine.cs b/HandyControl/Expression/Drawing/RandomEngine.cs new file mode 100644 index 00000000..f370bd0d --- /dev/null +++ b/HandyControl/Expression/Drawing/RandomEngine.cs @@ -0,0 +1,45 @@ +using System; + +namespace HandyControl.Expression.Drawing +{ + internal class RandomEngine + { + private double? _anotherSample; + + private Random _random; + + public RandomEngine(long seed) => Initialize(seed); + + private double Gaussian() + { + double num; + double num2; + double num3; + if (_anotherSample.HasValue) + { + var num4 = _anotherSample.Value; + _anotherSample = null; + return num4; + } + + do + { + num2 = 2.0 * Uniform() - 1.0; + num3 = 2.0 * Uniform() - 1.0; + num = num2 * num2 + num3 * num3; + } while (num >= 1.0); + + var num5 = Math.Sqrt(-2.0 * Math.Log(num) / num); + _anotherSample = num2 * num5; + return num3 * num5; + } + + private void Initialize(long seed) => _random = new Random((int) seed); + + public double NextGaussian(double mean, double variance) => Gaussian() * variance + mean; + + public double NextUniform(double min, double max) => Uniform() * (max - min) + min; + + private double Uniform() => _random.NextDouble(); + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Drawing/SimpleSegment.cs b/HandyControl/Expression/Drawing/SimpleSegment.cs new file mode 100644 index 00000000..0c117f0f --- /dev/null +++ b/HandyControl/Expression/Drawing/SimpleSegment.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Windows; + +namespace HandyControl.Expression.Drawing +{ + internal class SimpleSegment + { + public enum SegmentType + { + Line, + + CubicBeizer + } + + + private SimpleSegment() + { + } + + public Point[] Points { get; private set; } + + public SegmentType Type { get; private set; } + + public static SimpleSegment Create(Point point0, Point point1) + { + var segment = new SimpleSegment + { + Type = SegmentType.Line, + Points = new[] {point0, point1} + }; + return segment; + } + + public static SimpleSegment Create(Point point0, Point point1, Point point2) + { + var point = GeometryHelper.Lerp(point0, point1, 0.66666666666666663); + var point3 = GeometryHelper.Lerp(point1, point2, 0.33333333333333331); + var segment = new SimpleSegment + { + Type = SegmentType.CubicBeizer, + Points = new[] {point0, point, point3, point2} + }; + return segment; + } + + public static SimpleSegment Create(Point point0, Point point1, Point point2, Point point3) + { + var segment = new SimpleSegment + { + Type = SegmentType.CubicBeizer, + Points = new[] {point0, point1, point2, point3} + }; + return segment; + } + + public void Flatten(IList resultPolyline, double tolerance, IList resultParameters) + { + switch (Type) + { + case SegmentType.Line: + resultPolyline.Add(Points[1]); + if (resultParameters == null) break; + resultParameters.Add(1.0); + return; + + case SegmentType.CubicBeizer: + BezierCurveFlattener.FlattenCubic(Points, tolerance, resultPolyline, true, + resultParameters); + break; + + default: + return; + } + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/ArcGeometrySource.cs b/HandyControl/Expression/Media/ArcGeometrySource.cs new file mode 100644 index 00000000..8270314a --- /dev/null +++ b/HandyControl/Expression/Media/ArcGeometrySource.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Media; +using HandyControl.Expression.Drawing; + +namespace HandyControl.Expression.Media +{ + internal class ArcGeometrySource : GeometrySource + { + private double _absoluteThickness; + + private double _relativeThickness; + + private static bool AreCloseEnough(double angleA, double angleB) + { + return Math.Abs(angleA - angleB) < 0.001; + } + + internal static double[] ComputeAngleRanges(double radiusX, double radiusY, double intersect, + double start, double end) + { + var values = new List + { + start, + end, + intersect, + 180.0 - intersect, + 180.0 + intersect, + 360.0 - intersect, + 360.0 + intersect, + 540.0 - intersect, + 540.0 + intersect, + 720.0 - intersect + }; + values.Sort(); + var index = values.IndexOf(start); + var num2 = values.IndexOf(end); + if (num2 == index) + { + num2++; + } + else if (start < end) + { + IncreaseDuplicatedIndex(values, ref index); + DecreaseDuplicatedIndex(values, ref num2); + } + else if (start > end) + { + DecreaseDuplicatedIndex(values, ref index); + IncreaseDuplicatedIndex(values, ref num2); + } + + var list = new List(); + if (index < num2) + for (var i = index; i <= num2; i++) + list.Add(values[i]); + else + for (var j = index; j >= num2; j--) + list.Add(values[j]); + var num5 = EnsureFirstQuadrant((list[0] + list[1]) / 2.0); + if (radiusX < radiusY && num5 < intersect || radiusX > radiusY && num5 > intersect) + list.RemoveAt(0); + if (list.Count % 2 == 1) list.RemoveLast(); + if (list.Count == 0) + { + var num6 = Math.Min(index, num2) - 1; + if (num6 < 0) num6 = Math.Max(index, num2) + 1; + list.Add(values[num6]); + list.Add(values[num6]); + } + + return list.ToArray(); + } + + protected override Rect ComputeLogicalBounds(Rect layoutBounds, IGeometrySourceParameters parameters) + { + return GeometryHelper.GetStretchBound(base.ComputeLogicalBounds(layoutBounds, parameters), + parameters.Stretch, new Size(1.0, 1.0)); + } + + private static IList ComputeOneInnerCurve(double start, double end, Rect bounds, double offset) + { + var num = bounds.Width / 2.0; + var num2 = bounds.Height / 2.0; + var point = bounds.Center(); + start = start * 3.1415926535897931 / 180.0; + end = end * 3.1415926535897931 / 180.0; + var num3 = 0.17453292519943295; + var num4 = (int) Math.Ceiling(Math.Abs(end - start) / num3); + num4 = Math.Max(2, num4); + var list = new List(num4); + var list2 = new List(num4); + var lhs = new Point(); + var item = new Point(); + var vector = new Vector(); + var vector2 = new Vector(); + var vector3 = new Vector(); + var vector4 = new Vector(); + for (var i = 0; i < num4; i++) + { + var a = MathHelper.Lerp(start, end, i / (double) (num4 - 1)); + var num10 = Math.Sin(a); + var num11 = Math.Cos(a); + lhs.X = point.X + num * num10; + lhs.Y = point.Y - num2 * num11; + vector.X = num * num11; + vector.Y = num2 * num10; + vector2.X = -num2 * num10; + vector2.Y = num * num11; + var d = num2 * num2 * num10 * num10 + num * num * num11 * num11; + var num6 = Math.Sqrt(d); + var num7 = 2.0 * num10 * num11 * (num2 * num2 - num * num); + vector3.X = -num2 * num11; + vector3.Y = -num * num10; + item.X = lhs.X + offset * vector2.X / num6; + item.Y = lhs.Y + offset * vector2.Y / num6; + vector4.X = vector.X + offset / num6 * (vector3.X - 0.5 * vector2.X / d * num7); + vector4.Y = vector.Y + offset / num6 * (vector3.Y - 0.5 * vector2.Y / d * num7); + list.Add(item); + list2.Add(-vector4.Normalized()); + } + + var list3 = new List(num4 * 3 + 1) + { + list[0] + }; + for (var j = 1; j < num4; j++) + { + lhs = list[j - 1]; + item = list[j]; + var num13 = GeometryHelper.Distance(lhs, item) / 3.0; + list3.Add(lhs + list2[j - 1] * num13); + list3.Add(item - list2[j] * num13); + list3.Add(item); + } + + return list3; + } + + private static void DecreaseDuplicatedIndex(IList values, ref int index) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + while (index > 0 && values[index] == values[index - 1]) index--; + } + + internal static double EnsureFirstQuadrant(double angle) + { + angle = Math.Abs(angle % 180.0); + if (angle <= 90.0) return angle; + return 180.0 - angle; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static Size GetArcSize(Rect bound) + { + return new Size(bound.Width / 2.0, bound.Height / 2.0); + } + + private static void IncreaseDuplicatedIndex(IList values, ref int index) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + while (index < values.Count - 1 && values[index] == values[index + 1]) index++; + } + + internal static double InnerCurveSelfIntersect(double radiusX, double radiusY, double thickness) + { + var angleA = 0.0; + var angleB = 1.5707963267948966; + var flag = radiusX <= radiusY; + var vector = new Vector(); + while (!AreCloseEnough(angleA, angleB)) + { + var d = (angleA + angleB) / 2.0; + var num4 = Math.Cos(d); + var num5 = Math.Sin(d); + vector.X = radiusY * num5; + vector.Y = radiusX * num4; + vector.Normalize(); + if (flag) + { + var num6 = radiusX * num5 - vector.X * thickness; + if (num6 > 0.0) + angleB = d; + else if (num6 < 0.0) angleA = d; + } + else + { + var num7 = radiusY * num4 - vector.Y * thickness; + if (num7 < 0.0) + angleB = d; + else if (num7 > 0.0) angleA = d; + } + } + + var num8 = (angleA + angleB) / 2.0; + if (AreCloseEnough(num8, 0.0)) return 0.0; + if (!AreCloseEnough(num8, 1.5707963267948966)) return num8 * 180.0 / 3.1415926535897931; + return 90.0; + } + + private static double NormalizeAngle(double degree) + { + if (degree < 0.0 || degree > 360.0) + { + degree = degree % 360.0; + if (degree < 0.0) degree += 360.0; + } + + return degree; + } + + private void NormalizeThickness(IArcGeometrySourceParameters parameters) + { + var num = LogicalBounds.Width / 2.0; + var num2 = LogicalBounds.Height / 2.0; + var rhs = Math.Min(num, num2); + var arcThickness = parameters.ArcThickness; + if (parameters.ArcThicknessUnit == UnitType.Pixel) + arcThickness = MathHelper.SafeDivide(arcThickness, rhs, 0.0); + _relativeThickness = MathHelper.EnsureRange(arcThickness, 0.0, 1.0); + _absoluteThickness = rhs * _relativeThickness; + } + + private bool SyncPieceWiseInnerCurves(PathFigure figure, int index, ref Point firstPoint, + params double[] angles) + { + var flag = false; + var length = angles.Length; + var logicalBounds = LogicalBounds; + var absoluteThickness = _absoluteThickness; + flag |= figure.Segments.EnsureListCount(index + length / 2, () => new PolyBezierSegment()); + for (var i = 0; i < length / 2; i++) + { + var points = ComputeOneInnerCurve(angles[i * 2], angles[i * 2 + 1], logicalBounds, + absoluteThickness); + if (i == 0) firstPoint = points[0]; + flag |= PathSegmentHelper.SyncPolyBezierSegment(figure.Segments, index + i, points, 1, + points.Count - 1); + } + + return flag; + } + + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")] + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] + protected override bool UpdateCachedGeometry(IArcGeometrySourceParameters parameters) + { + var flag = false; + NormalizeThickness(parameters); + var relativeMode = parameters.ArcThicknessUnit == UnitType.Percent; + var flag3 = MathHelper.AreClose(parameters.StartAngle, parameters.EndAngle); + var angle = NormalizeAngle(parameters.StartAngle); + var end = NormalizeAngle(parameters.EndAngle); + if (end < angle) end += 360.0; + var isFilled = _relativeThickness == 1.0; + var flag5 = _relativeThickness == 0.0; + if (flag3) return flag | UpdateZeroAngleGeometry(relativeMode, angle); + if (MathHelper.IsVerySmall((end - angle) % 360.0)) + { + if (flag5 || isFilled) return flag | UpdateEllipseGeometry(isFilled); + return flag | UpdateFullRingGeometry(relativeMode); + } + + if (isFilled) return flag | UpdatePieGeometry(angle, end); + if (flag5) return flag | UpdateOpenArcGeometry(angle, end); + return flag | UpdateRingArcGeometry(relativeMode, angle, end); + } + + private bool UpdateEllipseGeometry(bool isFilled) + { + var flag = false; + var y = MathHelper.Lerp(LogicalBounds.Top, LogicalBounds.Bottom, 0.5); + var point = new Point(LogicalBounds.Left, y); + var point2 = new Point(LogicalBounds.Right, y); + flag |= GeometryHelper.EnsureGeometryType(out var geometry, ref CachedGeometry, + () => new PathGeometry()); + flag |= geometry.Figures.EnsureListCount(1, () => new PathFigure()); + var dependencyObject = geometry.Figures[0]; + flag |= dependencyObject.SetIfDifferent(PathFigure.IsClosedProperty, true); + flag |= dependencyObject.SetIfDifferent(PathFigure.IsFilledProperty, isFilled); + flag |= dependencyObject.Segments.EnsureListCount(2, () => new ArcSegment()); + flag |= dependencyObject.SetIfDifferent(PathFigure.StartPointProperty, point); + flag |= GeometryHelper.EnsureSegmentType(out var segment, dependencyObject.Segments, 0, + () => new ArcSegment()); + flag |= GeometryHelper.EnsureSegmentType(out var segment2, dependencyObject.Segments, 1, + () => new ArcSegment()); + var size = new Size(LogicalBounds.Width / 2.0, LogicalBounds.Height / 2.0); + flag |= segment.SetIfDifferent(ArcSegment.IsLargeArcProperty, false); + flag |= segment.SetIfDifferent(ArcSegment.SizeProperty, size); + flag |= segment.SetIfDifferent(ArcSegment.SweepDirectionProperty, SweepDirection.Clockwise); + flag |= segment.SetIfDifferent(ArcSegment.PointProperty, point2); + flag |= segment2.SetIfDifferent(ArcSegment.IsLargeArcProperty, false); + flag |= segment2.SetIfDifferent(ArcSegment.SizeProperty, size); + flag |= segment2.SetIfDifferent(ArcSegment.SweepDirectionProperty, SweepDirection.Clockwise); + return flag | segment2.SetIfDifferent(ArcSegment.PointProperty, point); + } + + private bool UpdateFullRingGeometry(bool relativeMode) + { + var flag = false; + flag |= GeometryHelper.EnsureGeometryType(out var geometry, ref CachedGeometry, + () => new PathGeometry()); + flag |= geometry.SetIfDifferent(PathGeometry.FillRuleProperty, FillRule.EvenOdd); + flag |= geometry.Figures.EnsureListCount(2, () => new PathFigure()); + flag |= PathFigureHelper.SyncEllipseFigure(geometry.Figures[0], LogicalBounds, + SweepDirection.Clockwise); + var logicalBounds = LogicalBounds; + var num = logicalBounds.Width / 2.0; + var num2 = logicalBounds.Height / 2.0; + if (relativeMode || MathHelper.AreClose(num, num2)) + { + var bounds = LogicalBounds.Resize(1.0 - _relativeThickness); + return flag | PathFigureHelper.SyncEllipseFigure(geometry.Figures[1], bounds, + SweepDirection.Counterclockwise); + } + + flag |= geometry.Figures[1].SetIfDifferent(PathFigure.IsClosedProperty, true); + flag |= geometry.Figures[1].SetIfDifferent(PathFigure.IsFilledProperty, true); + var firstPoint = new Point(); + var intersect = InnerCurveSelfIntersect(num, num2, _absoluteThickness); + var angles = ComputeAngleRanges(num, num2, intersect, 360.0, 0.0); + flag |= SyncPieceWiseInnerCurves(geometry.Figures[1], 0, ref firstPoint, angles); + return flag | geometry.Figures[1].SetIfDifferent(PathFigure.StartPointProperty, firstPoint); + } + + private bool UpdateOpenArcGeometry(double start, double end) + { + PathFigure figure; + ArcSegment segment; + var flag = false; + var cachedGeometry = CachedGeometry as PathGeometry; + if (cachedGeometry == null || cachedGeometry.Figures.Count != 1 || + (figure = cachedGeometry.Figures[0]).Segments.Count != 1 || + (segment = figure.Segments[0] as ArcSegment) == null) + { + figure = new PathFigure(); + CachedGeometry = new PathGeometry + { + Figures = {figure} + }; + figure.Segments.Add(segment = new ArcSegment()); + figure.IsClosed = false; + segment.SweepDirection = SweepDirection.Clockwise; + flag = true; + } + + flag |= figure.SetIfDifferent(PathFigure.StartPointProperty, + GeometryHelper.GetArcPoint(start, LogicalBounds)); + flag |= figure.SetIfDifferent(PathFigure.IsFilledProperty, false); + flag |= segment.SetIfDifferent(ArcSegment.PointProperty, + GeometryHelper.GetArcPoint(end, LogicalBounds)); + flag |= segment.SetIfDifferent(ArcSegment.SizeProperty, GetArcSize(LogicalBounds)); + return flag | segment.SetIfDifferent(ArcSegment.IsLargeArcProperty, end - start > 180.0); + } + + private bool UpdatePieGeometry(double start, double end) + { + PathFigure figure; + ArcSegment segment; + LineSegment segment2; + var flag = false; + var cachedGeometry = CachedGeometry as PathGeometry; + if (cachedGeometry == null || cachedGeometry.Figures.Count != 1 || + (figure = cachedGeometry.Figures[0]).Segments.Count != 2 || + (segment = figure.Segments[0] as ArcSegment) == null || + (segment2 = figure.Segments[1] as LineSegment) == null) + { + figure = new PathFigure(); + CachedGeometry = new PathGeometry + { + Figures = {figure} + }; + figure.Segments.Add(segment = new ArcSegment()); + figure.Segments.Add(segment2 = new LineSegment()); + figure.IsClosed = true; + segment.SweepDirection = SweepDirection.Clockwise; + flag = true; + } + + flag |= figure.SetIfDifferent(PathFigure.StartPointProperty, + GeometryHelper.GetArcPoint(start, LogicalBounds)); + flag |= segment.SetIfDifferent(ArcSegment.PointProperty, + GeometryHelper.GetArcPoint(end, LogicalBounds)); + flag |= segment.SetIfDifferent(ArcSegment.SizeProperty, GetArcSize(LogicalBounds)); + flag |= segment.SetIfDifferent(ArcSegment.IsLargeArcProperty, end - start > 180.0); + return flag | segment2.SetIfDifferent(LineSegment.PointProperty, LogicalBounds.Center()); + } + + private bool UpdateRingArcGeometry(bool relativeMode, double start, double end) + { + var flag = false; + flag |= GeometryHelper.EnsureGeometryType(out var geometry, ref CachedGeometry, + () => new PathGeometry()); + flag |= geometry.SetIfDifferent(PathGeometry.FillRuleProperty, FillRule.Nonzero); + flag |= geometry.Figures.EnsureListCount(1, () => new PathFigure()); + var dependencyObject = geometry.Figures[0]; + flag |= dependencyObject.SetIfDifferent(PathFigure.IsClosedProperty, true); + flag |= dependencyObject.SetIfDifferent(PathFigure.IsFilledProperty, true); + flag |= dependencyObject.SetIfDifferent(PathFigure.StartPointProperty, + GeometryHelper.GetArcPoint(start, LogicalBounds)); + flag |= dependencyObject.Segments.EnsureListCountAtLeast(3, () => new ArcSegment()); + flag |= GeometryHelper.EnsureSegmentType(out var segment, dependencyObject.Segments, 0, + () => new ArcSegment()); + flag |= segment.SetIfDifferent(ArcSegment.PointProperty, + GeometryHelper.GetArcPoint(end, LogicalBounds)); + flag |= segment.SetIfDifferent(ArcSegment.SizeProperty, + new Size(LogicalBounds.Width / 2.0, LogicalBounds.Height / 2.0)); + flag |= segment.SetIfDifferent(ArcSegment.IsLargeArcProperty, end - start > 180.0); + flag |= segment.SetIfDifferent(ArcSegment.SweepDirectionProperty, SweepDirection.Clockwise); + flag |= GeometryHelper.EnsureSegmentType(out var segment2, dependencyObject.Segments, 1, + () => new LineSegment()); + var logicalBounds = LogicalBounds; + var num = logicalBounds.Width / 2.0; + var num2 = logicalBounds.Height / 2.0; + if (relativeMode || MathHelper.AreClose(num, num2)) + { + var bound = LogicalBounds.Resize(1.0 - _relativeThickness); + flag |= segment2.SetIfDifferent(LineSegment.PointProperty, + GeometryHelper.GetArcPoint(end, bound)); + flag |= dependencyObject.Segments.EnsureListCount(3, () => new ArcSegment()); + flag |= GeometryHelper.EnsureSegmentType(out var segment3, dependencyObject.Segments, 2, + () => new ArcSegment()); + flag |= segment3.SetIfDifferent(ArcSegment.PointProperty, + GeometryHelper.GetArcPoint(start, bound)); + flag |= segment3.SetIfDifferent(ArcSegment.SizeProperty, GetArcSize(bound)); + flag |= segment3.SetIfDifferent(ArcSegment.IsLargeArcProperty, end - start > 180.0); + return flag | segment3.SetIfDifferent(ArcSegment.SweepDirectionProperty, + SweepDirection.Counterclockwise); + } + + var firstPoint = new Point(); + var intersect = InnerCurveSelfIntersect(num, num2, _absoluteThickness); + var angles = ComputeAngleRanges(num, num2, intersect, end, start); + flag |= SyncPieceWiseInnerCurves(dependencyObject, 2, ref firstPoint, angles); + return flag | segment2.SetIfDifferent(LineSegment.PointProperty, firstPoint); + } + + private bool UpdateZeroAngleGeometry(bool relativeMode, double angle) + { + Point point2; + var flag = false; + var arcPoint = GeometryHelper.GetArcPoint(angle, LogicalBounds); + var logicalBounds = LogicalBounds; + var num = logicalBounds.Width / 2.0; + var num2 = logicalBounds.Height / 2.0; + if (relativeMode || MathHelper.AreClose(num, num2)) + { + var bound = LogicalBounds.Resize(1.0 - _relativeThickness); + point2 = GeometryHelper.GetArcPoint(angle, bound); + } + else + { + var intersect = InnerCurveSelfIntersect(num, num2, _absoluteThickness); + var numArray = ComputeAngleRanges(num, num2, intersect, angle, angle); + var a = numArray[0] * 3.1415926535897931 / 180.0; + var vector = new Vector(num2 * Math.Sin(a), -num * Math.Cos(a)); + point2 = GeometryHelper.GetArcPoint(numArray[0], LogicalBounds) - + vector.Normalized() * _absoluteThickness; + } + + flag |= GeometryHelper.EnsureGeometryType(out var geometry, ref CachedGeometry, + () => new LineGeometry()); + flag |= geometry.SetIfDifferent(LineGeometry.StartPointProperty, arcPoint); + return flag | geometry.SetIfDifferent(LineGeometry.EndPointProperty, point2); + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/DrawingPropertyChangedEventArgs.cs b/HandyControl/Expression/Media/DrawingPropertyChangedEventArgs.cs new file mode 100644 index 00000000..7bcb3406 --- /dev/null +++ b/HandyControl/Expression/Media/DrawingPropertyChangedEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace HandyControl.Expression.Media +{ + internal class DrawingPropertyChangedEventArgs : EventArgs + { + public bool IsAnimated { get; set; } + + public DrawingPropertyMetadata Metadata { get; set; } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/DrawingPropertyMetadata.cs b/HandyControl/Expression/Media/DrawingPropertyMetadata.cs new file mode 100644 index 00000000..f3fb8776 --- /dev/null +++ b/HandyControl/Expression/Media/DrawingPropertyMetadata.cs @@ -0,0 +1,77 @@ +using System; +using System.Windows; + +namespace HandyControl.Expression.Media +{ + internal class DrawingPropertyMetadata : FrameworkPropertyMetadata + { + private DrawingPropertyMetadataOptions options; + + private PropertyChangedCallback propertyChangedCallback; + + public static event EventHandler DrawingPropertyChanged; + + static DrawingPropertyMetadata() + { + DrawingPropertyChanged += delegate (object sender, DrawingPropertyChangedEventArgs args) { + IShape shape = sender as IShape; + if ((shape != null) && args.Metadata.AffectsRender) + { + InvalidateGeometryReasons reasons = InvalidateGeometryReasons.PropertyChanged; + if (args.IsAnimated) + { + reasons |= InvalidateGeometryReasons.IsAnimated; + } + shape.InvalidateGeometry(reasons); + } + }; + } + + public DrawingPropertyMetadata(object defaultValue) : this(defaultValue, DrawingPropertyMetadataOptions.None, null) + { + } + + public DrawingPropertyMetadata(PropertyChangedCallback propertyChangedCallback) : this(DependencyProperty.UnsetValue, DrawingPropertyMetadataOptions.None, propertyChangedCallback) + { + } + + private DrawingPropertyMetadata(DrawingPropertyMetadataOptions options, object defaultValue) : base(defaultValue, (FrameworkPropertyMetadataOptions)options) + { + } + + public DrawingPropertyMetadata(object defaultValue, DrawingPropertyMetadataOptions options) : this(defaultValue, options, null) + { + } + + public DrawingPropertyMetadata(object defaultValue, DrawingPropertyMetadataOptions options, PropertyChangedCallback propertyChangedCallback) : base(defaultValue, (FrameworkPropertyMetadataOptions)options, AttachCallback(defaultValue, options, propertyChangedCallback)) + { + } + + private static PropertyChangedCallback AttachCallback(object defaultValue, DrawingPropertyMetadataOptions options, PropertyChangedCallback propertyChangedCallback) + { + DrawingPropertyMetadata metadata = new DrawingPropertyMetadata(options, defaultValue) + { + options = options, + propertyChangedCallback = propertyChangedCallback + }; + return new PropertyChangedCallback(metadata.InternalCallback); + } + + private void InternalCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (DrawingPropertyChanged != null) + { + DrawingPropertyChangedEventArgs args = new DrawingPropertyChangedEventArgs + { + Metadata = this, + IsAnimated = DependencyPropertyHelper.GetValueSource(sender, e.Property).IsAnimated + }; + DrawingPropertyChanged(sender, args); + } + if (this.propertyChangedCallback != null) + { + this.propertyChangedCallback(sender, e); + } + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/DrawingPropertyMetadataOptions.cs b/HandyControl/Expression/Media/DrawingPropertyMetadataOptions.cs new file mode 100644 index 00000000..82114f86 --- /dev/null +++ b/HandyControl/Expression/Media/DrawingPropertyMetadataOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace HandyControl.Expression.Media +{ + [Flags] + internal enum DrawingPropertyMetadataOptions + { + AffectsMeasure = 1, + AffectsRender = 0x10, + None = 0 + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/GeometryEffect.cs b/HandyControl/Expression/Media/GeometryEffect.cs new file mode 100644 index 00000000..84804c13 --- /dev/null +++ b/HandyControl/Expression/Media/GeometryEffect.cs @@ -0,0 +1,172 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Media; +using System.Windows.Threading; + +namespace HandyControl.Expression.Media +{ + [TypeConverter(typeof(GeometryEffectConverter))] + public abstract class GeometryEffect : Freezable + { + // ReSharper disable once InconsistentNaming + private static GeometryEffect defaultGeometryEffect; + + public static readonly DependencyProperty GeometryEffectProperty = + DependencyProperty.RegisterAttached("GeometryEffect", typeof(GeometryEffect), + typeof(GeometryEffect), + new DrawingPropertyMetadata(DefaultGeometryEffect, + DrawingPropertyMetadataOptions.AffectsRender, OnGeometryEffectChanged)); + + protected Geometry CachedGeometry; + + private bool _effectInvalidated; + + static GeometryEffect() + { + DrawingPropertyMetadata.DrawingPropertyChanged += + delegate(object sender, DrawingPropertyChangedEventArgs args) + { + if (sender is GeometryEffect effect && args.Metadata.AffectsRender) + effect.InvalidateGeometry(InvalidateGeometryReasons.PropertyChanged); + }; + } + + public static GeometryEffect DefaultGeometryEffect => + defaultGeometryEffect ?? (defaultGeometryEffect = new NoGeometryEffect()); + + public Geometry OutputGeometry => + CachedGeometry; + + protected internal DependencyObject Parent { get; private set; } + + protected internal virtual void Attach(DependencyObject obj) + { + if (Parent != null) Detach(); + _effectInvalidated = true; + CachedGeometry = null; + if (InvalidateParent(obj)) Parent = obj; + } + + public new GeometryEffect CloneCurrentValue() + { + return (GeometryEffect) base.CloneCurrentValue(); + } + + protected override Freezable CreateInstanceCore() + { + return (Freezable) Activator.CreateInstance(GetType()); + } + + protected abstract GeometryEffect DeepCopy(); + + protected internal virtual void Detach() + { + _effectInvalidated = true; + CachedGeometry = null; + if (Parent != null) + { + InvalidateParent(Parent); + Parent = null; + } + } + + public abstract bool Equals(GeometryEffect geometryEffect); + + public static GeometryEffect GetGeometryEffect(DependencyObject obj) + { + return (GeometryEffect) obj.GetValue(GeometryEffectProperty); + } + + public bool InvalidateGeometry(InvalidateGeometryReasons reasons) + { + if (_effectInvalidated) return false; + _effectInvalidated = true; + if (reasons != InvalidateGeometryReasons.ParentInvalidated) InvalidateParent(Parent); + return true; + } + + private static bool InvalidateParent(DependencyObject parent) + { + if (parent is IShape shape) + { + shape.InvalidateGeometry(InvalidateGeometryReasons.ChildInvalidated); + return true; + } + + if (parent is GeometryEffect effect) + { + effect.InvalidateGeometry(InvalidateGeometryReasons.ChildInvalidated); + return true; + } + + return false; + } + + private static void OnGeometryEffectChanged(DependencyObject obj, + DependencyPropertyChangedEventArgs e) + { + var oldValue = e.OldValue as GeometryEffect; + var newValue = e.NewValue as GeometryEffect; + if (!Equals(oldValue, newValue)) + { + if (oldValue != null && obj.Equals(oldValue.Parent)) oldValue.Detach(); + if (newValue != null) + { + if (newValue.Parent != null) + { + obj.Dispatcher.BeginInvoke(new Action(() => + { + var effect = newValue.CloneCurrentValue(); + obj.SetValue(GeometryEffectProperty, effect); + }), DispatcherPriority.Send, null); + } + else + { + newValue.Attach(obj); + } + } + } + } + + public bool ProcessGeometry(Geometry input) + { + var flag = false; + if (_effectInvalidated) + { + flag |= UpdateCachedGeometry(input); + _effectInvalidated = false; + } + + return flag; + } + + public static void SetGeometryEffect(DependencyObject obj, GeometryEffect value) + { + obj.SetValue(GeometryEffectProperty, value); + } + + protected abstract bool UpdateCachedGeometry(Geometry input); + + + private class NoGeometryEffect : GeometryEffect + { + protected override GeometryEffect DeepCopy() + { + return new NoGeometryEffect(); + } + + public override bool Equals(GeometryEffect geometryEffect) + { + if (geometryEffect != null) return geometryEffect is NoGeometryEffect; + return true; + } + + protected override bool UpdateCachedGeometry(Geometry input) + { + CachedGeometry = input; + return false; + } + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/GeometryEffectConverter.cs b/HandyControl/Expression/Media/GeometryEffectConverter.cs new file mode 100644 index 00000000..6dbfa57a --- /dev/null +++ b/HandyControl/Expression/Media/GeometryEffectConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; + +namespace HandyControl.Expression.Media +{ + public sealed class GeometryEffectConverter : TypeConverter + { + private static readonly Dictionary RegisteredEffects; + + static GeometryEffectConverter() + { + var dictionary = new Dictionary + { + { + "None", + GeometryEffect.DefaultGeometryEffect + }, + { + "Sketch", + new SketchGeometryEffect() + } + }; + RegisteredEffects = dictionary; + } + + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return typeof(string).IsAssignableFrom(sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return typeof(string).IsAssignableFrom(destinationType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string key && RegisteredEffects.TryGetValue(key, out var effect)) + return effect.CloneCurrentValue(); + return null; + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, + Type destinationType) + { + if (typeof(string).IsAssignableFrom(destinationType)) + { + if (value is string) return value; + foreach (var pair in RegisteredEffects) + if (pair.Value?.Equals(value as GeometryEffect) ?? value == null) + return pair.Key; + } + + return null; + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/GeometrySource!1.cs b/HandyControl/Expression/Media/GeometrySource!1.cs new file mode 100644 index 00000000..0edc7fe1 --- /dev/null +++ b/HandyControl/Expression/Media/GeometrySource!1.cs @@ -0,0 +1,88 @@ +using System.Windows; +using System.Windows.Media; +using HandyControl.Expression.Drawing; + +namespace HandyControl.Expression.Media +{ + public abstract class GeometrySource : IGeometrySource + where TParameters : IGeometrySourceParameters + { + private bool _geometryInvalidated; + + protected Geometry CachedGeometry; + + public bool InvalidateGeometry(InvalidateGeometryReasons reasons) + { + if ((reasons & InvalidateGeometryReasons.TemplateChanged) != 0) CachedGeometry = null; + if (!_geometryInvalidated) + { + _geometryInvalidated = true; + return true; + } + + return false; + } + + public bool UpdateGeometry(IGeometrySourceParameters parameters, Rect layoutBounds) + { + var force = false; + if (parameters is TParameters parameters1) + { + var rect = ComputeLogicalBounds(layoutBounds, parameters1); + force |= LayoutBounds != layoutBounds || LogicalBounds != rect; + if (_geometryInvalidated || force) + { + LayoutBounds = layoutBounds; + LogicalBounds = rect; + force |= UpdateCachedGeometry(parameters1); + force |= ApplyGeometryEffect(parameters1, force); + } + } + + _geometryInvalidated = false; + return force; + } + + public Geometry Geometry { get; private set; } + + public Rect LayoutBounds { get; private set; } + + public Rect LogicalBounds { get; private set; } + + private bool ApplyGeometryEffect(IGeometrySourceParameters parameters, bool force) + { + var flag = false; + var cachedGeometry = CachedGeometry; + var geometryEffect = parameters.GetGeometryEffect(); + if (geometryEffect != null) + { + if (force) + { + flag = true; + geometryEffect.InvalidateGeometry(InvalidateGeometryReasons.ParentInvalidated); + } + + if (geometryEffect.ProcessGeometry(CachedGeometry)) + { + flag = true; + cachedGeometry = geometryEffect.OutputGeometry; + } + } + + if (!Equals(Geometry, cachedGeometry)) + { + flag = true; + Geometry = cachedGeometry; + } + + return flag; + } + + protected virtual Rect ComputeLogicalBounds(Rect layoutBounds, IGeometrySourceParameters parameters) + { + return GeometryHelper.Inflate(layoutBounds, -parameters.GetHalfStrokeThickness()); + } + + protected abstract bool UpdateCachedGeometry(TParameters parameters); + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/IArcGeometrySourceParameters.cs b/HandyControl/Expression/Media/IArcGeometrySourceParameters.cs new file mode 100644 index 00000000..f4536306 --- /dev/null +++ b/HandyControl/Expression/Media/IArcGeometrySourceParameters.cs @@ -0,0 +1,13 @@ +namespace HandyControl.Expression.Media +{ + public interface IArcGeometrySourceParameters : IGeometrySourceParameters + { + double ArcThickness { get; } + + UnitType ArcThicknessUnit { get; } + + double EndAngle { get; } + + double StartAngle { get; } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/IGeometrySource.cs b/HandyControl/Expression/Media/IGeometrySource.cs new file mode 100644 index 00000000..faaa3e8d --- /dev/null +++ b/HandyControl/Expression/Media/IGeometrySource.cs @@ -0,0 +1,17 @@ +using System.Windows; + +namespace HandyControl.Expression.Media +{ + public interface IGeometrySource + { + bool InvalidateGeometry(InvalidateGeometryReasons reasons); + + bool UpdateGeometry(IGeometrySourceParameters parameters, Rect layoutBounds); + + System.Windows.Media.Geometry Geometry { get; } + + Rect LayoutBounds { get; } + + Rect LogicalBounds { get; } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/IGeometrySourceExtensions.cs b/HandyControl/Expression/Media/IGeometrySourceExtensions.cs new file mode 100644 index 00000000..1e616211 --- /dev/null +++ b/HandyControl/Expression/Media/IGeometrySourceExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Windows; + +namespace HandyControl.Expression.Media +{ + internal static class GeometrySourceExtensions + { + public static GeometryEffect GetGeometryEffect(this IGeometrySourceParameters parameters) + { + if (parameters is DependencyObject obj2) + { + var geometryEffect = GeometryEffect.GetGeometryEffect(obj2); + if (geometryEffect != null && obj2.Equals(geometryEffect.Parent)) return geometryEffect; + } + + return null; + } + + public static double GetHalfStrokeThickness(this IGeometrySourceParameters parameter) + { + if (parameter.Stroke != null) + { + var strokeThickness = parameter.StrokeThickness; + if (!double.IsNaN(strokeThickness) && !double.IsInfinity(strokeThickness)) + return Math.Abs(strokeThickness) / 2.0; + } + + return 0.0; + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/IGeometrySourceParameters.cs b/HandyControl/Expression/Media/IGeometrySourceParameters.cs new file mode 100644 index 00000000..5354b688 --- /dev/null +++ b/HandyControl/Expression/Media/IGeometrySourceParameters.cs @@ -0,0 +1,13 @@ +using System.Windows.Media; + +namespace HandyControl.Expression.Media +{ + public interface IGeometrySourceParameters + { + Stretch Stretch { get; } + + Brush Stroke { get; } + + double StrokeThickness { get; } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/IShape.cs b/HandyControl/Expression/Media/IShape.cs new file mode 100644 index 00000000..1f0fd37a --- /dev/null +++ b/HandyControl/Expression/Media/IShape.cs @@ -0,0 +1,25 @@ +using System; +using System.Windows; +using System.Windows.Media; + +namespace HandyControl.Expression.Media +{ + public interface IShape + { + event EventHandler RenderedGeometryChanged; + + void InvalidateGeometry(InvalidateGeometryReasons reasons); + + Brush Fill { get; set; } + + Thickness GeometryMargin { get; } + + Geometry RenderedGeometry { get; } + + Stretch Stretch { get; set; } + + Brush Stroke { get; set; } + + double StrokeThickness { get; set; } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/InvalidateGeometryReasons.cs b/HandyControl/Expression/Media/InvalidateGeometryReasons.cs new file mode 100644 index 00000000..32799de5 --- /dev/null +++ b/HandyControl/Expression/Media/InvalidateGeometryReasons.cs @@ -0,0 +1,14 @@ +using System; + +namespace HandyControl.Expression.Media +{ + [Flags] + public enum InvalidateGeometryReasons + { + ChildInvalidated = 4, + IsAnimated = 2, + ParentInvalidated = 8, + PropertyChanged = 1, + TemplateChanged = 0x10 + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/SketchGeometryEffect.cs b/HandyControl/Expression/Media/SketchGeometryEffect.cs new file mode 100644 index 00000000..0969ca1a --- /dev/null +++ b/HandyControl/Expression/Media/SketchGeometryEffect.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Windows; +using System.Windows.Media; +using HandyControl.Expression.Drawing; + +namespace HandyControl.Expression.Media +{ + public sealed class SketchGeometryEffect : GeometryEffect + { + private readonly long _randomSeed = DateTime.Now.Ticks; + + protected override GeometryEffect DeepCopy() + { + return new SketchGeometryEffect(); + } + + private static void DisturbPoints(RandomEngine random, double scale, IList points, + IList normals) + { + var count = points.Count; + for (var i = 1; i < count; i++) + { + var num3 = random.NextGaussian(0.0, 1.0 * scale); + var num4 = random.NextUniform(-0.5, 0.5) * scale; + var point = points[i]; + var vector = normals[i]; + var vector2 = normals[i]; + var point2 = points[i]; + var vector3 = normals[i]; + var vector4 = normals[i]; + points[i] = new Point(point.X + vector.X * num4 - vector2.Y * num3, + point2.Y + vector3.X * num3 + vector4.Y * num4); + } + } + + public override bool Equals(GeometryEffect geometryEffect) + { + return geometryEffect is SketchGeometryEffect; + } + + private IEnumerable GetEffectiveSegments(PathFigure pathFigure) + { + var startPoint = pathFigure.StartPoint; + foreach (var iteratorVariable1 in pathFigure.AllSegments()) + foreach (var iteratorVariable2 in iteratorVariable1.PathSegment.GetSimpleSegments( + iteratorVariable1.StartPoint)) + { + yield return iteratorVariable2; + startPoint = iteratorVariable2.Points.Last(); + } + + if (pathFigure.IsClosed) yield return SimpleSegment.Create(startPoint, pathFigure.StartPoint); + } + + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")] + protected override bool UpdateCachedGeometry(Geometry input) + { + var flag = false; + var inputPath = input.AsPathGeometry(); + if (inputPath != null) return flag | UpdateSketchGeometry(inputPath); + CachedGeometry = input; + return flag; + } + + private bool UpdateSketchGeometry(PathGeometry inputPath) + { + var flag = false; + flag |= GeometryHelper.EnsureGeometryType(out var geometry, ref CachedGeometry, + () => new PathGeometry()); + flag |= geometry.Figures.EnsureListCount(inputPath.Figures.Count, () => new PathFigure()); + var random = new RandomEngine(_randomSeed); + for (var i = 0; i < inputPath.Figures.Count; i++) + { + var pathFigure = inputPath.Figures[i]; + var isClosed = pathFigure.IsClosed; + var isFilled = pathFigure.IsFilled; + if (pathFigure.Segments.Count == 0) + { + flag |= geometry.Figures[i] + .SetIfDifferent(PathFigure.StartPointProperty, pathFigure.StartPoint); + flag |= geometry.Figures[i].Segments.EnsureListCount(0); + } + else + { + var list = new List(pathFigure.Segments.Count * 3); + foreach (var segment in GetEffectiveSegments(pathFigure)) + { + var resultPolyline = new List + { + segment.Points[0] + }; + segment.Flatten(resultPolyline, 0.0, null); + var polyline = new PolylineData(resultPolyline); + if (resultPolyline.Count > 1 && polyline.TotalLength > 4.0) + { + var a = polyline.TotalLength / 8.0; + var sampleCount = (int) Math.Max(2.0, Math.Ceiling(a)); + var interval = polyline.TotalLength / sampleCount; + var scale = interval / 8.0; + var samplePoints = new List(sampleCount); + var sampleNormals = new List(sampleCount); + var sampleIndex = 0; + PolylineHelper.PathMarch(polyline, 0.0, 0.0, delegate(MarchLocation location) + { + if (location.Reason == MarchStopReason.CompletePolyline) return double.NaN; + if (location.Reason != MarchStopReason.CompleteStep) return location.Remain; + if (sampleIndex++ == sampleCount) return double.NaN; + samplePoints.Add(location.GetPoint(polyline.Points)); + sampleNormals.Add(location.GetNormal(polyline)); + return interval; + }); + DisturbPoints(random, scale, samplePoints, sampleNormals); + list.AddRange(samplePoints); + } + else + { + list.AddRange(resultPolyline); + list.RemoveLast(); + } + } + + if (!isClosed) list.Add(pathFigure.Segments.Last().GetLastPoint()); + flag |= PathFigureHelper.SyncPolylineFigure(geometry.Figures[i], list, isClosed, + isFilled); + } + } + + if (flag) CachedGeometry = PathGeometryHelper.FixPathGeometryBoundary(CachedGeometry); + return flag; + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Media/UnitType.cs b/HandyControl/Expression/Media/UnitType.cs new file mode 100644 index 00000000..4aa8d7da --- /dev/null +++ b/HandyControl/Expression/Media/UnitType.cs @@ -0,0 +1,8 @@ +namespace HandyControl.Expression.Media +{ + public enum UnitType + { + Pixel, + Percent + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Shapes/Arc.cs b/HandyControl/Expression/Shapes/Arc.cs new file mode 100644 index 00000000..7f34b945 --- /dev/null +++ b/HandyControl/Expression/Shapes/Arc.cs @@ -0,0 +1,64 @@ +using System.Windows; +using System.Windows.Media; +using HandyControl.Expression.Media; + +namespace HandyControl.Expression.Shapes +{ + public sealed class Arc : PrimitiveShape, IArcGeometrySourceParameters + { + public static readonly DependencyProperty ArcThicknessProperty = + DependencyProperty.Register("ArcThickness", typeof(double), typeof(Arc), + new DrawingPropertyMetadata(0.0, DrawingPropertyMetadataOptions.AffectsRender)); + + public static readonly DependencyProperty ArcThicknessUnitProperty = + DependencyProperty.Register("ArcThicknessUnit", typeof(UnitType), typeof(Arc), + new DrawingPropertyMetadata(UnitType.Pixel, DrawingPropertyMetadataOptions.AffectsRender)); + + public static readonly DependencyProperty EndAngleProperty = DependencyProperty.Register("EndAngle", + typeof(double), typeof(Arc), + new DrawingPropertyMetadata(90.0, DrawingPropertyMetadataOptions.AffectsRender)); + + public static readonly DependencyProperty StartAngleProperty = + DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc), + new DrawingPropertyMetadata(0.0, DrawingPropertyMetadataOptions.AffectsRender)); + + public double ArcThickness + { + get => + (double) GetValue(ArcThicknessProperty); + set => SetValue(ArcThicknessProperty, value); + } + + public UnitType ArcThicknessUnit + { + get => + (UnitType) GetValue(ArcThicknessUnitProperty); + set => SetValue(ArcThicknessUnitProperty, value); + } + + public double EndAngle + { + get => + (double) GetValue(EndAngleProperty); + set => SetValue(EndAngleProperty, value); + } + + Stretch IGeometrySourceParameters.Stretch => Stretch; + + Brush IGeometrySourceParameters.Stroke => Stroke; + + double IGeometrySourceParameters.StrokeThickness => StrokeThickness; + + public double StartAngle + { + get => + (double) GetValue(StartAngleProperty); + set => SetValue(StartAngleProperty, value); + } + + protected override IGeometrySource CreateGeometrySource() + { + return new ArcGeometrySource(); + } + } +} \ No newline at end of file diff --git a/HandyControl/Expression/Shapes/PrimitiveShape.cs b/HandyControl/Expression/Shapes/PrimitiveShape.cs new file mode 100644 index 00000000..fd217485 --- /dev/null +++ b/HandyControl/Expression/Shapes/PrimitiveShape.cs @@ -0,0 +1,79 @@ +using System; +using System.Windows; +using System.Windows.Media; +using System.Windows.Shapes; +using HandyControl.Expression.Drawing; +using HandyControl.Expression.Media; + +namespace HandyControl.Expression.Shapes +{ + public abstract class PrimitiveShape : Shape, IGeometrySourceParameters, IShape + { + private IGeometrySource _geometrySource; + + static PrimitiveShape() + { + StretchProperty.OverrideMetadata(typeof(PrimitiveShape), + new DrawingPropertyMetadata(Stretch.Fill, DrawingPropertyMetadataOptions.AffectsRender)); + StrokeThicknessProperty.OverrideMetadata(typeof(PrimitiveShape), + new DrawingPropertyMetadata(1.0, DrawingPropertyMetadataOptions.AffectsRender)); + } + + protected sealed override Geometry DefiningGeometry => + GeometrySource.Geometry ?? Geometry.Empty; + + private IGeometrySource GeometrySource => _geometrySource ?? (_geometrySource = CreateGeometrySource()); + + Stretch IGeometrySourceParameters.Stretch => Stretch; + + Brush IGeometrySourceParameters.Stroke => Stroke; + + double IGeometrySourceParameters.StrokeThickness => StrokeThickness; + + public event EventHandler RenderedGeometryChanged; + + public void InvalidateGeometry(InvalidateGeometryReasons reasons) + { + if (GeometrySource.InvalidateGeometry(reasons)) InvalidateArrange(); + } + + public Thickness GeometryMargin => GeometrySource.LogicalBounds.Subtract(RenderedGeometry.Bounds); + + Brush IShape.Fill + { + get => Fill; + set => Fill = value; + } + + Stretch IShape.Stretch + { + get => Stretch; + set => Stretch = value; + } + + Brush IShape.Stroke + { + get => Stroke; + set => Stroke = value; + } + + double IShape.StrokeThickness + { + get => StrokeThickness; + set => StrokeThickness = value; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (GeometrySource.UpdateGeometry(this, finalSize.Bounds())) RealizeGeometry(); + base.ArrangeOverride(finalSize); + return finalSize; + } + + protected abstract IGeometrySource CreateGeometrySource(); + + protected override Size MeasureOverride(Size availableSize) => new Size(base.StrokeThickness, base.StrokeThickness); + + private void RealizeGeometry() => RenderedGeometryChanged?.Invoke(this, EventArgs.Empty); + } +} \ No newline at end of file diff --git a/HandyControl/HandyControl.csproj b/HandyControl/HandyControl.csproj index 3a4967d2..38f500c3 100644 --- a/HandyControl/HandyControl.csproj +++ b/HandyControl/HandyControl.csproj @@ -47,6 +47,7 @@ + @@ -61,7 +62,7 @@ - + @@ -71,6 +72,38 @@ True ResourceToken.tt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml b/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml index 5f1655f7..51997587 100644 --- a/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml +++ b/HandyControl/Themes/Styles/Base/ProgressBarBaseStyle.xaml @@ -1,6 +1,11 @@  + xmlns:controls="clr-namespace:HandyControl.Controls" + xmlns:shapes="clr-namespace:HandyControl.Expression.Shapes"> + + + + @@ -18,18 +23,16 @@ - - - - - - - - - - - - + + + + + + + + + + @@ -51,11 +54,9 @@ - + - - - + @@ -68,4 +69,24 @@ + + \ No newline at end of file diff --git a/HandyControl/Themes/Styles/ProgressBar.xaml b/HandyControl/Themes/Styles/ProgressBar.xaml index 3b0c40e5..f6916e85 100644 --- a/HandyControl/Themes/Styles/ProgressBar.xaml +++ b/HandyControl/Themes/Styles/ProgressBar.xaml @@ -1,5 +1,6 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:HandyControl.Controls"> @@ -175,6 +176,26 @@ + + + + + + + + + +