| /* |
| * 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()); |
| } |