| /* | 
 |  * Copyright 2025 Google LLC | 
 |  * | 
 |  * Use of this source code is governed by a BSD-style license that can be | 
 |  * found in the LICENSE file. | 
 |  */ | 
 |  | 
 | #include "include/core/SkPathBuilder.h" | 
 | #include "include/core/SkPoint.h" | 
 | #include "include/core/SkTypes.h" | 
 | #include "include/private/base/SkFixed.h" | 
 | #include "src/core/SkEdge.h" | 
 | #include "src/core/SkEdgeBuilder.h" | 
 | #include "src/core/SkGeometry.h" | 
 | #include "tests/Test.h" | 
 |  | 
 | #include <cstddef> | 
 |  | 
 | DEF_TEST(SkEdge_CWLineWithoutClip_PublicMembersMatchMathematics, r) { | 
 |     SkEdge e; | 
 |     e.setLine({0, 4}, {10, 20}); | 
 |  | 
 |     REPORTER_ASSERT(r, e.fWinding == SkEdge::Winding::kCW); | 
 |     // The slope of X with respect to Y of this line segment is (10 - 0) / (20 - 4) or 0.625 | 
 |     REPORTER_ASSERT(r, e.fDxDy == SkFloatToFixed(0.625f)); | 
 |     // The Y coordinate conceptually starts at 0.5, so make sure the X coordinate matches is | 
 |     // 0.5 * 0.625 (the slope) | 
 |     REPORTER_ASSERT(r, e.fX == SkFloatToFixed(0.3125f)); | 
 |     // Even though these are stored as integers, they conceptually represent the midpoint of | 
 |     // the line (4.5 and 19.5). | 
 |     REPORTER_ASSERT(r, e.fFirstY == 4); | 
 |     REPORTER_ASSERT(r, e.fLastY == 19); | 
 |     // Lines are "approximated" in one shot and have no need for follow-up approximations. | 
 |     REPORTER_ASSERT(r, !e.hasNextSegment()); | 
 | } | 
 |  | 
 | DEF_TEST(SkEdge_CWLineNullClip_PublicMembersMatchMathematics, r) { | 
 |     SkEdge e; | 
 |     e.setLine({0, 4}, {10, 20}, nullptr); | 
 |  | 
 |     REPORTER_ASSERT(r, e.fWinding == SkEdge::Winding::kCW); | 
 |     // The slope of X with respect to Y of this line segment is (10 - 0) / (20 - 4) or 0.625 | 
 |     REPORTER_ASSERT(r, e.fDxDy == SkFloatToFixed(0.625f)); | 
 |     // The Y coordinate conceptually starts at 0.5, so make sure the X coordinate matches is | 
 |     // 0.5 * 0.625 (the slope) | 
 |     REPORTER_ASSERT(r, e.fX == SkFloatToFixed(0.3125f)); | 
 |     // Even though these are stored as integers, they conceptually represent the midpoint of | 
 |     // the line (4.5 and 19.5). | 
 |     REPORTER_ASSERT(r, e.fFirstY == 4); | 
 |     REPORTER_ASSERT(r, e.fLastY == 19); | 
 |     // Lines are "approximated" in one shot and have no need for follow-up approximations. | 
 |     REPORTER_ASSERT(r, !e.hasNextSegment()); | 
 | } | 
 |  | 
 | DEF_TEST(SkEdge_CCWLineWithoutClip_PublicMembersMatchMathematics, r) { | 
 |     SkEdge e; | 
 |     e.setLine({0, -4}, {10, -20}); | 
 |     REPORTER_ASSERT(r, e.fWinding == SkEdge::Winding::kCCW); | 
 |     // The slope of X with respect to Y of this line segment is (10 - 0) / (-20 - -4) or -0.625 | 
 |     REPORTER_ASSERT(r, e.fDxDy == SkFloatToFixed(-0.625f)); | 
 |     // The Y coordinate conceptually starts at 0.5, so make sure the X coordinate matches is | 
 |     // 10 + 0.5 * -0.625 (the slope) | 
 |     REPORTER_ASSERT(r, e.fX == SkFloatToFixed(9.6875f)); | 
 |     // Even though these are stored as integers, they conceptually represent the midpoint of | 
 |     // the line (-19.5 and -4.5). | 
 |     REPORTER_ASSERT(r, e.fFirstY == -20); | 
 |     REPORTER_ASSERT(r, e.fLastY == -5); | 
 |     REPORTER_ASSERT(r, !e.hasNextSegment()); | 
 | } | 
 |  | 
 | static float quad_slope_at(const SkPoint pts[3], float t) { | 
 |     SkASSERT(t >= 0.0f && t <= 1.0f); | 
 |     // A quadratic Bezier curve can be written in polynomial form as | 
 |     //   Q(t) = At^2 + Bt + C | 
 |     // Where A = p0 - 2p1 + p2, B = 2(p1 - p0), C = p0 | 
 |     // and p0 is the start point, p2 is the end point and p1 is the control point. | 
 |     // With some derivatives and algebra, the slope of X with respect to Y is | 
 |     // ((2*x0 - 4*x1 + 2*x2)t + (2*x1 - 2*x0)) / (2*y0 - 4*y1 + 2*y2)*t + (2*y1 - 2*y0) | 
 |     const float x0 = pts[0].fX, x1 = pts[1].fX, x2 = pts[2].fX; | 
 |     const float dxdt = (2 * x0 - 4 * x1 + 2 * x2) * t + (2 * x1 - 2 * x0); | 
 |     const float y0 = pts[0].fY, y1 = pts[1].fY, y2 = pts[2].fY; | 
 |     const float dydt = (2 * y0 - 4 * y1 + 2 * y2) * t + (2 * y1 - 2 * y0); | 
 |     return dxdt / dydt; | 
 | } | 
 |  | 
 | static float cubic_slope_at(const SkPoint pts[4], float t) { | 
 |     SkASSERT(t >= 0.0f && t <= 1.0f); | 
 |     // A cubic Bezier curve can be written in polynomial form as | 
 |     //   C(t) = At^3 + Bt^2 + Ct + D | 
 |     // Where A = -p0 + 3p1 + -3p2 + p3 | 
 |     //       B = 3p0 - 6p1 + 3p2 | 
 |     //       C = -3p0 + 3p1 | 
 |     //       D = p0 | 
 |     // and p0 is the start point, p3 is the end point and p1/p2 are the control points. | 
 |     // With some derivatives and algebra, the slope of X with respect to Y is | 
 |     //  ((-3*x0 + 9*x1 + -9*x2 + 3*x3)*t*t + (6*x0 - 12*x1 + 6*x2)*t + (-3*x0 + 3*x1)) / | 
 |     //  ((-3*y0 + 9*y1 + -9*y2 + 3*y3)*t*t + (6*y0 - 12*y1 + 6*y2)*t + (-3*y0 + 3*y1)) | 
 |     // | 
 |     const float x0 = pts[0].fX, x1 = pts[1].fX, x2 = pts[2].fX, x3 = pts[3].fX; | 
 |     const float dxdt = (-3 * x0 + 9 * x1 + -9 * x2 + 3 * x3) * t * t + | 
 |                        (6 * x0 - 12 * x1 + 6 * x2) * t + (-3 * x0 + 3 * x1); | 
 |     const float y0 = pts[0].fY, y1 = pts[1].fY, y2 = pts[2].fY, y3 = pts[3].fY; | 
 |     const float dydt = (-3 * y0 + 9 * y1 + -9 * y2 + 3 * y3) * t * t + | 
 |                        (6 * y0 - 12 * y1 + 6 * y2) * t + (-3 * y0 + 3 * y1); | 
 |     return dxdt / dydt; | 
 | } | 
 |  | 
 | static void assert_within_n_percent(skiatest::Reporter* r, | 
 |                                     float actual, | 
 |                                     float expected, | 
 |                                     float delta) { | 
 |     // Make sure we are passing in something like 5% and not 0.05 | 
 |     SkASSERT(delta >= 1 && delta <= 25); | 
 |     float percentDiff = (std::abs(actual - expected) / expected); | 
 |     REPORTER_ASSERT(r, percentDiff < (delta / 100.f), | 
 |                     "%g was not within %g%% of %g", | 
 |                     actual, delta, expected); | 
 | } | 
 |  | 
 | static float eval_x_at(const SkQuadCoeff& quad, float t) { return quad.eval({t, t})[0]; } | 
 |  | 
 | static float eval_y_at(const SkQuadCoeff& quad, float t) { return quad.eval({t, t})[1]; } | 
 |  | 
 | static float eval_x_at(const SkCubicCoeff& cubic, float t) { return cubic.eval({t, t})[0]; } | 
 |  | 
 | static float eval_y_at(const SkCubicCoeff& cubic, float t) { return cubic.eval({t, t})[1]; } | 
 |  | 
 | DEF_TEST(SkEdge_Quad_ApproximatedWithMultipleLineSegments, r) { | 
 |     SkQuadraticEdge e; | 
 |     const SkPoint points[3] = {{0.f, 0.f}, {2.5f, 15.f}, {10.f, 20.f}}; | 
 |     REPORTER_ASSERT(r, e.setQuadratic(points)); | 
 |  | 
 |     const SkQuadCoeff exact = SkQuadCoeff(points); | 
 |     auto assert_x_on_curve_between = [&](SkFixed x, float tLower, float tUpper) { | 
 |         const float lower = eval_x_at(exact, tLower); | 
 |         const float upper = eval_x_at(exact, tUpper); | 
 |         REPORTER_ASSERT(r, SkFixedToFloat(x) >= lower && SkFixedToFloat(x) < upper, | 
 |                         "%g was not in [%g, %g]", | 
 |                         SkFixedToFloat(x), | 
 |                         lower, | 
 |                         upper); | 
 |     }; | 
 |     auto assert_y_starts_at = [&](int32_t y, float atT) { | 
 |         const float start = sk_float_round2int(eval_y_at(exact, atT)); | 
 |         REPORTER_ASSERT(r, y == start, "%d was not %g", y, start); | 
 |     }; | 
 |     auto assert_y_ends_at = [&](int32_t y, float atT) { | 
 |         const float end = sk_float_round2int(eval_y_at(exact, atT)) - 1;  // this range is exclusive | 
 |         REPORTER_ASSERT(r, y == end, "%d was not %g", y, end); | 
 |     }; | 
 |  | 
 |     // SkQuadraticEdge will approximate a quadratic Bezier curve with N line segments. | 
 |     // There are some heuristics to determine how many segments to use. For this curve, there are | 
 |     // 4 total segments - the current segment followed by 3 more. | 
 |     const int kNumSegments = 4; | 
 |     const float kSegmentWidth = 1.0f / kNumSegments; | 
 |     float t = 0.0f; | 
 |     for (int segments = kNumSegments - 1; segments >= 0; segments--) { | 
 |         skiatest::ReporterContext ctx(r, | 
 |                                       SkStringPrintf("Segment %d, t is [%g, %g]", | 
 |                                                      kNumSegments - segments - 1, | 
 |                                                      t, | 
 |                                                      t + kSegmentWidth)); | 
 |         REPORTER_ASSERT(r, e.fWinding == SkEdge::Winding::kCW); | 
 |         REPORTER_ASSERT(r, e.segmentsLeft() == segments); | 
 |  | 
 |         // The slope for a line segment should be close to the curve at the midpoint between this | 
 |         // section of curve. e.g. for the segment [0.0, 0.25], it should be the slope at t=0.125 | 
 |         const float segment1Slope = quad_slope_at(points, kSegmentWidth / 2 + t); | 
 |         assert_within_n_percent(r, SkFixedToFloat(e.fDxDy), segment1Slope, 1); | 
 |  | 
 |         // The x value could vary a bit depending on implementation and rounding | 
 |         assert_x_on_curve_between(e.fX, t, t + kSegmentWidth); | 
 |  | 
 |         // The start and stopping y values should match the actual curve. | 
 |         assert_y_starts_at(e.fFirstY, t); | 
 |         assert_y_ends_at(e.fLastY, t + kSegmentWidth); | 
 |  | 
 |         if (e.hasNextSegment()) { | 
 |             e.nextSegment(); | 
 |             t += kSegmentWidth; | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | DEF_TEST(SkEdge_MostlyFlatQuad_SkipsZeroHeightSegments, r) { | 
 |     SkQuadraticEdge e; | 
 |     const SkPoint points[3] = {{4.f, 19.f}, {5.5f, 18.2f}, {10.f, 18.f}}; | 
 |  | 
 |     REPORTER_ASSERT(r, e.setQuadratic(points)); | 
 |     REPORTER_ASSERT(r, e.fWinding == SkEdge::Winding::kCCW); | 
 |  | 
 |     REPORTER_ASSERT(r, e.fFirstY == 18); | 
 |     REPORTER_ASSERT(r, e.fLastY == 18); | 
 |  | 
 |     // The x coordinate should be somewhere between B_x(0.25) and B_x(0.75). The exact value is | 
 |     // subject to the implementation's choice of dealing with segments that cover 0 height. | 
 |     SkQuadCoeff exact = SkQuadCoeff(points); | 
 |     const float kOneQuarterT = exact.eval({0.25f})[0];    // about 4.9375 | 
 |     const float kThreeQuarterT = exact.eval({0.75f})[0];  // about 7.9375 | 
 |     REPORTER_ASSERT(r, | 
 |                     e.fX > SkFloatToFixed(kOneQuarterT) && e.fX < SkFloatToFixed(kThreeQuarterT)); | 
 |  | 
 |     // The slope should be negative, but precisely how much again depends the implementation. | 
 |     // The exact value doesn't matter much for this segment because it's 1) the last segment | 
 |     // and 2) only of height 1. | 
 |     REPORTER_ASSERT(r, e.fDxDy < 0); | 
 |  | 
 |     // This edge has covered the entire scanline, so there shouldn't be any more segments | 
 |     REPORTER_ASSERT(r, !e.hasNextSegment()); | 
 | } | 
 |  | 
 | DEF_TEST(SkEdge_HorizontalishQuad_ReturnsFalse, r) { | 
 |     SkQuadraticEdge e; | 
 |     const SkPoint points[3] = {{4.f, 18.f}, {5.5f, 18.2f}, {10.f, 18.4f}}; | 
 |  | 
 |     REPORTER_ASSERT(r, !e.setQuadratic(points)); | 
 | } | 
 |  | 
 | DEF_TEST(SkEdge_Cubic_ApproximatedWithMultipleLineSegments, r) { | 
 |     SkCubicEdge e; | 
 |     const SkPoint points[4] = {{0.f, 0.f}, {2.f, 13.f}, {8.f, 6.f}, {10.f, 20.f}}; | 
 |  | 
 |     REPORTER_ASSERT(r, e.setCubic(points)); | 
 |  | 
 |     const SkCubicCoeff exact = SkCubicCoeff(points); | 
 |     auto assert_x_on_curve_between = [&](SkFixed x, float tLower, float tUpper) { | 
 |         const float lower = eval_x_at(exact, tLower); | 
 |         const float upper = eval_x_at(exact, tUpper); | 
 |         REPORTER_ASSERT(r, SkFixedToFloat(x) >= lower && SkFixedToFloat(x) < upper, | 
 |                         "%g was not in [%g, %g]", | 
 |                         SkFixedToFloat(x), lower, upper); | 
 |     }; | 
 |     auto assert_y_starts_at = [&](int32_t y, float atT) { | 
 |         const float start = sk_float_round2int(eval_y_at(exact, atT)); | 
 |         REPORTER_ASSERT(r, y == start, "%d was not %g", y, start); | 
 |     }; | 
 |     auto assert_y_ends_at = [&](int32_t y, float atT) { | 
 |         const float end = sk_float_round2int(eval_y_at(exact, atT)) - 1;  // this range is exclusive | 
 |         REPORTER_ASSERT(r, y == end, "%d was not %g", y, end); | 
 |     }; | 
 |  | 
 |     // SkCubicEdge will approximate a cubic Bezier curve with N line segments. | 
 |     // There are some heuristics to determine how many segments to use. For this curve, there are | 
 |     // 8 total segments - the current segment followed by 7 more. | 
 |     const int kNumSegments = 8; | 
 |     const float kSegmentWidth = 1.0f / kNumSegments; | 
 |     float t = 0.0f; | 
 |     for (int segments = kNumSegments - 1; segments >= 0; segments--) { | 
 |         skiatest::ReporterContext ctx(r, | 
 |                                       SkStringPrintf("Segment %d, t is [%g, %g]", | 
 |                                                      kNumSegments - segments - 1, | 
 |                                                      t, | 
 |                                                      t + kSegmentWidth)); | 
 |         REPORTER_ASSERT(r, e.fWinding == SkEdge::Winding::kCW); | 
 |         REPORTER_ASSERT(r, e.segmentsLeft() == segments); | 
 |  | 
 |         // The slope for a line segment should be close to the curve at the midpoint between this | 
 |         // section of curve. e.g. for the segment [0.0, 0.25], it should be the slope at t=0.125 | 
 |         const float segment1Slope = cubic_slope_at(points, kSegmentWidth / 2 + t); | 
 |         assert_within_n_percent(r, SkFixedToFloat(e.fDxDy), segment1Slope, 3); | 
 |  | 
 |         // The x value could vary a bit depending on implementation and rounding | 
 |         assert_x_on_curve_between(e.fX, t, t + kSegmentWidth); | 
 |  | 
 |         // The start and stopping y values should match the actual curve. | 
 |         assert_y_starts_at(e.fFirstY, t); | 
 |         assert_y_ends_at(e.fLastY, t + kSegmentWidth); | 
 |  | 
 |         if (e.hasNextSegment()) { | 
 |             e.nextSegment(); | 
 |             t += kSegmentWidth; | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | DEF_TEST(SkBasicEdgeBuilder_TrapezoidNoClip_HasTwoEdges, r) { | 
 |     SkPathBuilder pb; | 
 |     SkPath path = pb.moveTo(5, 0) | 
 |                     .lineTo(0, 20) | 
 |                     .lineTo(20, 20) | 
 |                     .lineTo(10, 0) | 
 |                     .close() | 
 |                     .detach(); | 
 |  | 
 |     SkBasicEdgeBuilder eb; | 
 |     int numEdges = eb.buildEdges(path, nullptr); | 
 |     REPORTER_ASSERT(r, numEdges == 2); | 
 |  | 
 |     SkEdge** edges = eb.edgeList(); | 
 |     SkEdge* left = edges[0]; | 
 |     SkEdge* right = edges[1]; | 
 |  | 
 |     if (left->fX > right->fX) { // An implementation could return these edges in either order | 
 |         std::swap(left, right); | 
 |     } | 
 |  | 
 |     // left edge should go from (5,0) -> (0,20) (clockwise) which is a X/Y slope of -5/20 | 
 |     REPORTER_ASSERT(r, left->fWinding == SkEdge::Winding::kCW); | 
 |     const float kLeftSlope = -0.25f; | 
 |     REPORTER_ASSERT(r, left->fDxDy == SkFloatToFixed(kLeftSlope)); | 
 |     // fX will be slightly less than 5 because it's evaluated at y=0.5 | 
 |     const float kLeftX = 5.f + kLeftSlope * 0.5f; // 4.875 | 
 |     REPORTER_ASSERT(r, left->fX == SkFloatToFixed(kLeftX)); | 
 |     REPORTER_ASSERT(r, left->fFirstY == 0); | 
 |     REPORTER_ASSERT(r, left->fLastY == 19); // this represents 19.5 and is inclusive | 
 |  | 
 |     // right edge should go from (20,20) -> (10,0) (counter clockwise) which is a X/Y slope of 10/20 | 
 |     REPORTER_ASSERT(r, right->fWinding == SkEdge::Winding::kCCW); | 
 |     const float kRightSlope = 0.5f; | 
 |     REPORTER_ASSERT(r, right->fDxDy == SkFloatToFixed(kRightSlope)); | 
 |     // fX will be slightly more than 10 because it's evaluated at y=0.5 | 
 |     const float kRightX = 10.f + kRightSlope * 0.5f; // 10.25 | 
 |     REPORTER_ASSERT(r, right->fX == SkFloatToFixed(kRightX)); | 
 |     REPORTER_ASSERT(r, right->fFirstY == 0); | 
 |     REPORTER_ASSERT(r, right->fLastY == 19); // this represents 19.5 and is inclusive | 
 |  | 
 |     // Lines are "approximated" in one shot and have no need for follow-up approximations. | 
 |     REPORTER_ASSERT(r, !left->hasNextSegment()); | 
 |     REPORTER_ASSERT(r, !right->hasNextSegment()); | 
 | } |