blob: c02062b198e1344a02d16b5f2a21cb1fbb6ac0b5 [file] [log] [blame]
/*
* 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());
}