diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 75d58f45d5..0664f22dcb 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")] [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.UnitTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index a1a2d6c3d0..7949a62949 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -8,6 +8,11 @@ namespace Avalonia.Utilities /// public static class MathUtilities { + // smallest such that 1.0+DoubleEpsilon != 1.0 + private const double DoubleEpsilon = 2.2204460492503131e-016; + + private const float FloatEpsilon = 1.192092896e-07F; + /// /// AreClose - Returns whether or not two doubles are "close". That is, whether or /// not they are within epsilon of each other. @@ -18,11 +23,26 @@ namespace Avalonia.Utilities { //in case they are Infinities (then epsilon check does not work) if (value1 == value2) return true; - double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon; double delta = value1 - value2; return (-eps < delta) && (eps > delta); } + /// + /// AreClose - Returns whether or not two floats are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first float to compare. + /// The second float to compare. + public static bool AreClose(float value1, float value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatEpsilon; + float delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + /// /// LessThan - Returns whether or not the first double is less than the second double. /// That is, whether or not the first is strictly less than *and* not within epsilon of @@ -35,6 +55,18 @@ namespace Avalonia.Utilities return (value1 < value2) && !AreClose(value1, value2); } + /// + /// LessThan - Returns whether or not the first float is less than the second float. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. + /// + /// The first single float to compare. + /// The second single float to compare. + public static bool LessThan(float value1, float value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + /// /// GreaterThan - Returns whether or not the first double is greater than the second double. /// That is, whether or not the first is strictly greater than *and* not within epsilon of @@ -47,6 +79,18 @@ namespace Avalonia.Utilities return (value1 > value2) && !AreClose(value1, value2); } + /// + /// GreaterThan - Returns whether or not the first float is greater than the second float. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool GreaterThan(float value1, float value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + /// /// LessThanOrClose - Returns whether or not the first double is less than or close to /// the second double. That is, whether or not the first is strictly less than or within @@ -59,6 +103,18 @@ namespace Avalonia.Utilities return (value1 < value2) || AreClose(value1, value2); } + /// + /// LessThanOrClose - Returns whether or not the first float is less than or close to + /// the second float. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool LessThanOrClose(float value1, float value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + /// /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to /// the second double. That is, whether or not the first is strictly greater than or within @@ -71,6 +127,18 @@ namespace Avalonia.Utilities return (value1 > value2) || AreClose(value1, value2); } + /// + /// GreaterThanOrClose - Returns whether or not the first float is greater than or close to + /// the second float. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. + /// + /// The first float to compare. + /// The second float to compare. + public static bool GreaterThanOrClose(float value1, float value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + /// /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), /// but this is faster. @@ -78,7 +146,17 @@ namespace Avalonia.Utilities /// The double to compare to 1. public static bool IsOne(double value) { - return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + return Math.Abs(value - 1.0) < 10.0 * DoubleEpsilon; + } + + /// + /// IsOne - Returns whether or not the float is "close" to 1. Same as AreClose(float, 1), + /// but this is faster. + /// + /// The float to compare to 1. + public static bool IsOne(float value) + { + return Math.Abs(value - 1.0f) < 10.0f * FloatEpsilon; } /// @@ -88,7 +166,17 @@ namespace Avalonia.Utilities /// The double to compare to 0. public static bool IsZero(double value) { - return Math.Abs(value) < 10.0 * double.Epsilon; + return Math.Abs(value) < 10.0 * DoubleEpsilon; + } + + /// + /// IsZero - Returns whether or not the float is "close" to 0. Same as AreClose(float, 0), + /// but this is faster. + /// + /// The float to compare to 0. + public static bool IsZero(float value) + { + return Math.Abs(value) < 10.0f * FloatEpsilon; } /// diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 1781067abb..e10d78917e 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1228,7 +1228,7 @@ namespace Avalonia.Controls Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count); // avoid processing when asked to distribute "0" - if (!_IsZero(requestedSize)) + if (!MathUtilities.IsZero(requestedSize)) { DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting int end = start + count; @@ -1306,7 +1306,7 @@ namespace Avalonia.Controls } // sanity check: requested size must all be distributed - Debug.Assert(_IsZero(sizeToDistribute)); + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); } else if (requestedSize <= rangeMaxSize) { @@ -1346,7 +1346,7 @@ namespace Avalonia.Controls } // sanity check: requested size must all be distributed - Debug.Assert(_IsZero(sizeToDistribute)); + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); } else { @@ -1358,7 +1358,7 @@ namespace Avalonia.Controls double equalSize = requestedSize / count; if (equalSize < maxMaxSize - && !_AreClose(equalSize, maxMaxSize)) + && !MathUtilities.AreClose(equalSize, maxMaxSize)) { // equi-size is less than maximum of maxSizes. // in this case distribute so that smaller definitions grow faster than @@ -2151,7 +2151,7 @@ namespace Avalonia.Controls // and precision of floating-point computation. (However, the resulting // display is subject to anti-aliasing problems. TANSTAAFL.) - if (!_AreClose(roundedTakenSize, finalSize)) + if (!MathUtilities.AreClose(roundedTakenSize, finalSize)) { // Compute deltas for (int i = 0; i < definitions.Count; ++i) @@ -2168,7 +2168,7 @@ namespace Avalonia.Controls if (roundedTakenSize > finalSize) { int i = definitions.Count - 1; - while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) + while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0) { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache - dpiIncrement; @@ -2184,7 +2184,7 @@ namespace Avalonia.Controls else if (roundedTakenSize < finalSize) { int i = 0; - while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Count) + while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Count) { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache + dpiIncrement; @@ -2595,27 +2595,6 @@ namespace Avalonia.Controls set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } } - /// - /// fp version of d == 0. - /// - /// Value to check. - /// true if d == 0. - private static bool _IsZero(double d) - { - return (Math.Abs(d) < double.Epsilon); - } - - /// - /// fp version of d1 == d2 - /// - /// First value to compare - /// Second value to compare - /// true if d1 == d2 - private static bool _AreClose(double d1, double d2) - { - return (Math.Abs(d1 - d2) < double.Epsilon); - } - /// /// Returns reference to extended data bag. /// diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index ec23bfa396..64378a4eb2 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -193,7 +193,8 @@ namespace Avalonia.Controls var orient = Orientation == Orientation.Horizontal; var pointDen = orient ? _track.Bounds.Width : _track.Bounds.Height; - pointDen += double.Epsilon; // Just add epsilon to avoid divide by zero exceptions. + // Just add epsilon to avoid NaN in case 0/0 + pointDen += double.Epsilon; var pointNum = orient ? x.Position.X : x.Position.Y; var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d); diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 51eb6edbea..438cbc8b27 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Controls.Utils { @@ -119,7 +120,7 @@ namespace Avalonia.Controls.Utils } var rect = new Rect(_size); - if (Math.Abs(borderThickness) > double.Epsilon) + if (!MathUtilities.IsZero(borderThickness)) rect = rect.Deflate(borderThickness * 0.5); var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, _cornerRadius.BottomRight, _cornerRadius.BottomLeft); diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 6fdcd9631b..b1cf1aecc9 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -4,6 +4,7 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; +using Avalonia.Utilities; using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media @@ -154,12 +155,12 @@ namespace Avalonia.Media return; } - if (Math.Abs(radiusX) > double.Epsilon) + if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); } - if (Math.Abs(radiusY) > double.Epsilon) + if (!MathUtilities.IsZero(radiusY)) { radiusY = Math.Min(radiusY, rect.Height / 2); } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 720185a3ad..0292398782 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; +using Avalonia.Utilities; using Avalonia.Utility; namespace Avalonia.Media.TextFormatting @@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting /// private void UpdateLayout() { - if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon) + if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = CreateEmptyTextLine(0); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index bbb45cf64c..9b7ba4844a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X))); var radiusY = Math.Max(rrect.RadiiTopLeft.Y, Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y))); - var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; + var isRounded = !MathUtilities.IsZero(radiusX) || !MathUtilities.IsZero(radiusY); if (brush != null) { diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs new file mode 100644 index 0000000000..0378a5b017 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -0,0 +1,119 @@ +using System; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities +{ + public class MathUtilitiesTests + { + private const double AnyValue = 42.42; + private readonly double _calculatedAnyValue; + private readonly double _one; + private readonly double _zero; + + public MathUtilitiesTests() + { + _calculatedAnyValue = 0.0; + _one = 0.0; + _zero = 1.0; + + const int N = 10; + var dxAny = AnyValue / N; + var dxOne = 1.0 / N; + var dxZero = _zero / N; + + for (var i = 0; i < N; ++i) + { + _calculatedAnyValue += dxAny; + _one += dxOne; + _zero -= dxZero; + } + } + + [Fact] + public void Two_Equivalent_Double_Values_Are_Close() + { + var actual = MathUtilities.AreClose(AnyValue, _calculatedAnyValue); + + Assert.True(actual); + Assert.Equal(AnyValue, Math.Round(_calculatedAnyValue, 14)); + } + + [Fact] + public void Two_Equivalent_Single_Values_Are_Close() + { + var expectedValue = (float)AnyValue; + var actualValue = (float)_calculatedAnyValue; + + var actual = MathUtilities.AreClose(expectedValue, actualValue); + + Assert.True(actual); + Assert.Equal((float) Math.Round(expectedValue, 5), (float) Math.Round(actualValue, 4)); + } + + [Fact] + public void Calculated_Double_One_Is_One() + { + var actual = MathUtilities.IsOne(_one); + + Assert.True(actual); + Assert.Equal(1.0, Math.Round(_one, 15)); + } + + [Fact] + public void Calculated_Single_One_Is_One() + { + var actualValue = (float)_one; + + var actual = MathUtilities.IsOne(actualValue); + + Assert.True(actual); + Assert.Equal(1.0f, (float) Math.Round(actualValue, 7)); + } + + [Fact] + public void Calculated_Double_Zero_Is_Zero() + { + var actual = MathUtilities.IsZero(_zero); + + Assert.True(actual); + Assert.Equal(0.0, Math.Round(_zero, 15)); + } + + [Fact] + public void Calculated_Single_Zero_Is_Zero() + { + var actualValue = (float)_zero; + + var actual = MathUtilities.IsZero(actualValue); + + Assert.True(actual); + Assert.Equal(0.0f, (float) Math.Round(actualValue, 7)); + } + + [Fact] + public void Clamp_Input_NaN_Return_NaN() + { + var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0); + Assert.True(double.IsNaN(clamp)); + } + + [Fact] + public void Clamp_Input_NegativeInfinity_Return_Min() + { + const double min = 0.0; + const double max = 1.0; + var actual = MathUtilities.Clamp(double.NegativeInfinity, min, max); + Assert.Equal(min, actual); + } + + [Fact] + public void Clamp_Input_PositiveInfinity_Return_Max() + { + const double min = 0.0; + const double max = 1.0; + var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max); + Assert.Equal(max, actual); + } + } +}