diff --git a/tests/YGRoundingFunctionTest.cpp b/tests/YGRoundingFunctionTest.cpp index 82eba44d7c..b96fdb9630 100644 --- a/tests/YGRoundingFunctionTest.cpp +++ b/tests/YGRoundingFunctionTest.cpp @@ -18,6 +18,14 @@ TEST(YogaTest, rounding_value) { ASSERT_FLOAT_EQ(6.0, YGRoundValueToPixelGrid(5.999999, 2.0, true, false)); ASSERT_FLOAT_EQ(6.0, YGRoundValueToPixelGrid(5.999999, 2.0, false, true)); + // Test with negative numbers + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.000001, 2.0, false, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.000001, 2.0, true, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.000001, 2.0, false, true)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-5.999999, 2.0, false, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-5.999999, 2.0, true, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-5.999999, 2.0, false, true)); + // Test that numbers with fraction are rounded correctly accounting for ceil/floor flags ASSERT_FLOAT_EQ(6.0, YGRoundValueToPixelGrid(6.01, 2.0, false, false)); ASSERT_FLOAT_EQ(6.5, YGRoundValueToPixelGrid(6.01, 2.0, true, false)); @@ -25,4 +33,45 @@ TEST(YogaTest, rounding_value) { ASSERT_FLOAT_EQ(6.0, YGRoundValueToPixelGrid(5.99, 2.0, false, false)); ASSERT_FLOAT_EQ(6.0, YGRoundValueToPixelGrid(5.99, 2.0, true, false)); ASSERT_FLOAT_EQ(5.5, YGRoundValueToPixelGrid(5.99, 2.0, false, true)); + ASSERT_FLOAT_EQ(6.0, YGRoundValueToPixelGrid(6.49, 1.0, false, false)); + ASSERT_FLOAT_EQ(7.0, YGRoundValueToPixelGrid(6.50, 1.0, false, false)); + ASSERT_FLOAT_EQ(7.0, YGRoundValueToPixelGrid(6.51, 1.0, false, false)); + ASSERT_FLOAT_EQ(7.0, YGRoundValueToPixelGrid(6.50, 1.0, true, false)); + ASSERT_FLOAT_EQ(6.0, YGRoundValueToPixelGrid(6.50, 1.0, false, true)); + + // Test with negative numbers + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.01, 2.0, false, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.01, 2.0, true, false)); + ASSERT_FLOAT_EQ(-6.5, YGRoundValueToPixelGrid(-6.01, 2.0, false, true)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-5.99, 2.0, false, false)); + ASSERT_FLOAT_EQ(-5.5, YGRoundValueToPixelGrid(-5.99, 2.0, true, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-5.99, 2.0, false, true)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.49, 1.0, false, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.50, 1.0, false, false)); + ASSERT_FLOAT_EQ(-7.0, YGRoundValueToPixelGrid(-6.51, 1.0, false, false)); + ASSERT_FLOAT_EQ(-6.0, YGRoundValueToPixelGrid(-6.50, 1.0, true, false)); + ASSERT_FLOAT_EQ(-7.0, YGRoundValueToPixelGrid(-6.50, 1.0, false, true)); + + // Do a simple translation test to ensure that a distance between 2 values + // stays consistent during an animation, even after rounding them. + const int LAPS = 3; + const int LAP_CLOSEST = 0; + const int LAP_CEIL = 1; + const int LAP_FLOOR = 2; + for (int currentLap = LAP_CLOSEST; currentLap < LAPS; ++currentLap) + { + float left = -2.0f; + float right = 2.0f; + const float distance = right-left; + float totalDistance = 1.1f; + const float step = 0.01f; + while (totalDistance >= 0.0f) { + left += step; + right += step; + const float snappedLeft = YGRoundValueToPixelGrid(left, 1.0, currentLap==LAP_CEIL, currentLap==LAP_FLOOR); + const float snappedRight = YGRoundValueToPixelGrid(right, 1.0, currentLap==LAP_CEIL, currentLap==LAP_FLOOR); + ASSERT_FLOAT_EQ(distance, (snappedRight-snappedLeft)); + totalDistance -= step; + } + } } diff --git a/yoga/Yoga.cpp b/yoga/Yoga.cpp index 41f89e877e..df50e8affe 100644 --- a/yoga/Yoga.cpp +++ b/yoga/Yoga.cpp @@ -3180,24 +3180,41 @@ float YGRoundValueToPixelGrid(const float value, const bool forceCeil, const bool forceFloor) { float scaledValue = value * pointScaleFactor; - float fractial = fmodf(scaledValue, 1.0); - if (YGFloatsEqual(fractial, 0)) { - // First we check if the value is already rounded - scaledValue = scaledValue - fractial; - } else if (YGFloatsEqual(fractial, 1.0)) { - scaledValue = scaledValue - fractial + 1.0; + const float fractial = fmodf(scaledValue, 1.0f); + const float absoluteFractial = fabs(fractial); + + // 1. Remove any remainder from the scaledValue + scaledValue = scaledValue - fractial; + + // 2. Figure out rounding + // Note: It is important that the following rounding algorithm + // ensures that both positive and negative values are treated exactly the same. + if (YGFloatsEqual(absoluteFractial, 0.0f)) { + // Already whole (or close enough), skip rounding + } else if (YGFloatsEqual(absoluteFractial, 1.0f)) { + // Already whole (or close enough), skip rounding + scaledValue += (fractial < 0.0f) ? -1.0f : 1.0f; } else if (forceCeil) { - // Next we check if we need to use forced rounding - scaledValue = scaledValue - fractial + 1.0f; + // Force round to upper whole value + if (fractial > 0.0f) { + scaledValue += 1.0f; + } } else if (forceFloor) { - scaledValue = scaledValue - fractial; + // Force round to lower whole value + if (fractial < 0.0f) { + scaledValue -= 1.0f; + } } else { - // Finally we just round the value - scaledValue = scaledValue - fractial + - (!YGFloatIsUndefined(fractial) && - (fractial > 0.5f || YGFloatsEqual(fractial, 0.5f)) - ? 1.0f - : 0.0f); + // Round to closest whole value + if (fractial > 0.0f) { + if (absoluteFractial > 0.5f || YGFloatsEqual(absoluteFractial, 0.5f)) { + scaledValue += 1.0f; + } + } else { + if (absoluteFractial > 0.5f && !YGFloatsEqual(absoluteFractial, 0.5f)) { + scaledValue -= 1.0f; + } + } } return (YGFloatIsUndefined(scaledValue) || YGFloatIsUndefined(pointScaleFactor))