xref: /aosp_15_r20/external/skia/tests/PathBuilderTest.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2020 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "include/core/SkPath.h"
9 #include "include/core/SkPathBuilder.h"
10 #include "include/core/SkPathTypes.h"
11 #include "include/core/SkPoint.h"
12 #include "include/core/SkRRect.h"
13 #include "include/core/SkRect.h"
14 #include "include/core/SkScalar.h"
15 #include "src/base/SkRandom.h"
16 #include "src/core/SkPathPriv.h"
17 #include "tests/Test.h"
18 
19 #include <cstddef>
20 #include <cstdint>
21 #include <initializer_list>
22 #include <string>
23 #include <vector>
24 
25 enum class SkPathConvexity;
26 
is_empty(skiatest::Reporter * reporter,const SkPath & p)27 static void is_empty(skiatest::Reporter* reporter, const SkPath& p) {
28     REPORTER_ASSERT(reporter, p.getBounds().isEmpty());
29     REPORTER_ASSERT(reporter, p.countPoints() == 0);
30 }
31 
DEF_TEST(pathbuilder,reporter)32 DEF_TEST(pathbuilder, reporter) {
33     SkPathBuilder b;
34 
35     is_empty(reporter, b.snapshot());
36     is_empty(reporter, b.detach());
37 
38     b.moveTo(10, 10).lineTo(20, 20).quadTo(30, 10, 10, 20);
39 
40     SkPath p0 = b.snapshot();
41     SkPath p1 = b.snapshot();
42     SkPath p2 = b.detach();
43 
44     // Builders should always precompute the path's bounds, so there is no race condition later
45     REPORTER_ASSERT(reporter, SkPathPriv::HasComputedBounds(p0));
46     REPORTER_ASSERT(reporter, SkPathPriv::HasComputedBounds(p1));
47     REPORTER_ASSERT(reporter, SkPathPriv::HasComputedBounds(p2));
48 
49     REPORTER_ASSERT(reporter, p0.getBounds() == SkRect::MakeLTRB(10, 10, 30, 20));
50     REPORTER_ASSERT(reporter, p0.countPoints() == 4);
51 
52     REPORTER_ASSERT(reporter, p0 == p1);
53     REPORTER_ASSERT(reporter, p0 == p2);
54 
55     is_empty(reporter, b.snapshot());
56     is_empty(reporter, b.detach());
57 }
58 
DEF_TEST(pathbuilder_filltype,reporter)59 DEF_TEST(pathbuilder_filltype, reporter) {
60     for (auto fillType : { SkPathFillType::kWinding,
61                            SkPathFillType::kEvenOdd,
62                            SkPathFillType::kInverseWinding,
63                            SkPathFillType::kInverseEvenOdd }) {
64         SkPathBuilder b(fillType);
65 
66         REPORTER_ASSERT(reporter, b.fillType() == fillType);
67 
68         for (const SkPath& path : { b.snapshot(), b.detach() }) {
69             REPORTER_ASSERT(reporter, path.getFillType() == fillType);
70             is_empty(reporter, path);
71         }
72     }
73 }
74 
check_points(const SkPath & path,const SkPoint expected[],size_t count)75 static bool check_points(const SkPath& path, const SkPoint expected[], size_t count) {
76     std::vector<SkPoint> iter_pts;
77 
78     for (auto [v, p, w] : SkPathPriv::Iterate(path)) {
79         switch (v) {
80             case SkPathVerb::kMove:
81                 iter_pts.push_back(p[0]);
82                 break;
83             case SkPathVerb::kLine:
84                 iter_pts.push_back(p[1]);
85                 break;
86             case SkPathVerb::kQuad:
87             case SkPathVerb::kConic:
88                 iter_pts.push_back(p[1]);
89                 iter_pts.push_back(p[2]);
90                 break;
91             case SkPathVerb::kCubic:
92                 iter_pts.push_back(p[1]);
93                 iter_pts.push_back(p[2]);
94                 iter_pts.push_back(p[3]);
95                 break;
96             case SkPathVerb::kClose:
97                 break;
98         }
99     }
100     if (iter_pts.size() != count) {
101         return false;
102     }
103     for (size_t i = 0; i < count; ++i) {
104         if (iter_pts[i] != expected[i]) {
105             return false;
106         }
107     }
108     return true;
109 }
110 
DEF_TEST(pathbuilder_missing_move,reporter)111 DEF_TEST(pathbuilder_missing_move, reporter) {
112     SkPathBuilder b;
113 
114     b.lineTo(10, 10).lineTo(20, 30);
115     const SkPoint pts0[] = {
116         {0, 0}, {10, 10}, {20, 30},
117     };
118     REPORTER_ASSERT(reporter, check_points(b.snapshot(), pts0, std::size(pts0)));
119 
120     b.reset().moveTo(20, 20).lineTo(10, 10).lineTo(20, 30).close().lineTo(60, 60);
121     const SkPoint pts1[] = {
122         {20, 20}, {10, 10}, {20, 30},
123         {20, 20}, {60, 60},
124     };
125     REPORTER_ASSERT(reporter, check_points(b.snapshot(), pts1, std::size(pts1)));
126 }
127 
DEF_TEST(pathbuilder_addRect,reporter)128 DEF_TEST(pathbuilder_addRect, reporter) {
129     const SkRect r = { 10, 20, 30, 40 };
130 
131     for (int i = 0; i < 4; ++i) {
132         for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
133             SkPathBuilder b;
134             b.addRect(r, dir, i);
135             auto bp = b.detach();
136 
137             SkRect r2;
138             bool   closed = false;
139             SkPathDirection dir2;
140             REPORTER_ASSERT(reporter, bp.isRect(&r2, &closed, &dir2));
141             REPORTER_ASSERT(reporter, r2 == r);
142             REPORTER_ASSERT(reporter, closed);
143             REPORTER_ASSERT(reporter, dir == dir2);
144 
145             SkPath p;
146             p.addRect(r, dir, i);
147             REPORTER_ASSERT(reporter, p == bp);
148         }
149     }
150 }
151 
is_eq(const SkPath & a,const SkPath & b)152 static bool is_eq(const SkPath& a, const SkPath& b) {
153     if (a != b) {
154         return false;
155     }
156 
157     {
158         SkRect ra, rb;
159         bool is_a = a.isOval(&ra);
160         bool is_b = b.isOval(&rb);
161         if (is_a != is_b) {
162             return false;
163         }
164         if (is_a && (ra != rb)) {
165             return false;
166         }
167     }
168 
169     {
170         SkRRect rra, rrb;
171         bool is_a = a.isRRect(&rra);
172         bool is_b = b.isRRect(&rrb);
173         if (is_a != is_b) {
174             return false;
175         }
176         if (is_a && (rra != rrb)) {
177             return false;
178         }
179     }
180 
181     // getConvextity() should be sufficient to test, but internally we sometimes don't want
182     // to trigger computing it, so this is the stronger test for equality.
183     {
184         SkPathConvexity ca = SkPathPriv::GetConvexityOrUnknown(a),
185                         cb = SkPathPriv::GetConvexityOrUnknown(b);
186         if (ca != cb) {
187             return false;
188         }
189     }
190 
191     return true;
192 }
193 
DEF_TEST(pathbuilder_addOval,reporter)194 DEF_TEST(pathbuilder_addOval, reporter) {
195     const SkRect r = { 10, 20, 30, 40 };
196     SkRect tmp;
197 
198     for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
199         for (int i = 0; i < 4; ++i) {
200             auto bp = SkPathBuilder().addOval(r, dir, i).detach();
201             SkPath p;
202             p.addOval(r, dir, i);
203             REPORTER_ASSERT(reporter, is_eq(p, bp));
204         }
205         auto bp = SkPathBuilder().addOval(r, dir).detach();
206         SkPath p;
207         p.addOval(r, dir);
208         REPORTER_ASSERT(reporter, is_eq(p, bp));
209 
210         // test negative case -- can't have any other segments
211         bp = SkPathBuilder().addOval(r, dir).lineTo(10, 10).detach();
212         REPORTER_ASSERT(reporter, !bp.isOval(&tmp));
213         bp = SkPathBuilder().lineTo(10, 10).addOval(r, dir).detach();
214         REPORTER_ASSERT(reporter, !bp.isOval(&tmp));
215     }
216 }
217 
DEF_TEST(pathbuilder_addRRect,reporter)218 DEF_TEST(pathbuilder_addRRect, reporter) {
219     const SkRRect rr = SkRRect::MakeRectXY({ 10, 20, 30, 40 }, 5, 6);
220 
221     for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
222         for (int i = 0; i < 4; ++i) {
223             SkPathBuilder b;
224             b.addRRect(rr, dir, i);
225             auto bp = b.detach();
226 
227             SkPath p;
228             p.addRRect(rr, dir, i);
229             REPORTER_ASSERT(reporter, is_eq(p, bp));
230         }
231         auto bp = SkPathBuilder().addRRect(rr, dir).detach();
232         SkPath p;
233         p.addRRect(rr, dir);
234         REPORTER_ASSERT(reporter, is_eq(p, bp));
235 
236         // test negative case -- can't have any other segments
237         SkRRect tmp;
238         bp = SkPathBuilder().addRRect(rr, dir).lineTo(10, 10).detach();
239         REPORTER_ASSERT(reporter, !bp.isRRect(&tmp));
240         bp = SkPathBuilder().lineTo(10, 10).addRRect(rr, dir).detach();
241         REPORTER_ASSERT(reporter, !bp.isRRect(&tmp));
242     }
243 }
244 
DEF_TEST(pathbuilder_make,reporter)245 DEF_TEST(pathbuilder_make, reporter) {
246     constexpr int N = 100;
247     uint8_t vbs[N];
248     SkPoint pts[N];
249 
250     SkRandom rand;
251     SkPathBuilder b;
252     b.moveTo(0, 0);
253     pts[0] = {0, 0}; vbs[0] = (uint8_t)SkPathVerb::kMove;
254     for (int i = 1; i < N; ++i) {
255         float x = rand.nextF();
256         float y = rand.nextF();
257         b.lineTo(x, y);
258         pts[i] = {x, y}; vbs[i] = (uint8_t)SkPathVerb::kLine;
259     }
260     auto p0 = b.detach();
261     auto p1 = SkPath::Make(pts, N, vbs, N, nullptr, 0, p0.getFillType());
262     REPORTER_ASSERT(reporter, p0 == p1);
263 }
264 
DEF_TEST(pathbuilder_genid,r)265 DEF_TEST(pathbuilder_genid, r) {
266     SkPathBuilder builder;
267 
268     builder.lineTo(10, 10);
269     auto p1 = builder.snapshot();
270 
271     builder.lineTo(10, 20);
272     auto p2 = builder.snapshot();
273 
274     REPORTER_ASSERT(r, p1.getGenerationID() != p2.getGenerationID());
275 }
276 
DEF_TEST(pathbuilder_addPolygon,reporter)277 DEF_TEST(pathbuilder_addPolygon, reporter) {
278     SkPoint pts[] = {{1, 2}, {3, 4}, {5, 6}, {7, 8}};
279 
280     auto addpoly = [](const SkPoint pts[], int count, bool isClosed) {
281         SkPathBuilder builder;
282         if (count > 0) {
283             builder.moveTo(pts[0]);
284             for (int i = 1; i < count; ++i) {
285                 builder.lineTo(pts[i]);
286             }
287             if (isClosed) {
288                 builder.close();
289             }
290         }
291         return builder.detach();
292     };
293 
294     for (bool isClosed : {false, true}) {
295         for (size_t i = 0; i <= std::size(pts); ++i) {
296             auto path0 = SkPathBuilder().addPolygon(pts, i, isClosed).detach();
297             auto path1 = addpoly(pts, i, isClosed);
298             REPORTER_ASSERT(reporter, path0 == path1);
299         }
300     }
301 }
302 
DEF_TEST(pathbuilder_addPath,reporter)303 DEF_TEST(pathbuilder_addPath, reporter) {
304     const auto p = SkPath()
305         .moveTo(10, 10)
306         .lineTo(100, 10)
307         .quadTo(200, 100, 100, 200)
308         .close()
309         .moveTo(200, 200)
310         .cubicTo(210, 200, 210, 300, 200, 300)
311         .conicTo(150, 250, 100, 200, 1.4f);
312 
313     REPORTER_ASSERT(reporter, p == SkPathBuilder().addPath(p).detach());
314 }
315 
316 /*
317  *  If paths were immutable, we would not have to track this, but until that day, we need
318  *  to ensure that paths are built correctly/consistently with this field, regardless of
319  *  either the classic mutable apis, or via SkPathBuilder (SkPath::Polygon uses builder).
320  */
DEF_TEST(pathbuilder_lastmoveindex,reporter)321 DEF_TEST(pathbuilder_lastmoveindex, reporter) {
322     const SkPoint pts[] = {
323         {0, 1}, {2, 3}, {4, 5},
324     };
325     constexpr int N = (int)std::size(pts);
326 
327     for (int ctrCount = 1; ctrCount < 4; ++ctrCount) {
328         const int lastMoveToIndex = (ctrCount - 1) * N;
329 
330         for (bool isClosed : {false, true}) {
331             SkPath a, b;
332 
333             SkPathBuilder builder;
334             for (int i = 0; i < ctrCount; ++i) {
335                 builder.addPolygon(pts, N, isClosed);  // new-school way
336                 b.addPoly(pts, N, isClosed);        // old-school way
337             }
338             a = builder.detach();
339 
340             // We track the last moveTo verb index, and we invert it if the last verb was a close
341             const int expected = isClosed ? ~lastMoveToIndex : lastMoveToIndex;
342             const int a_last = SkPathPriv::LastMoveToIndex(a);
343             const int b_last = SkPathPriv::LastMoveToIndex(b);
344 
345             REPORTER_ASSERT(reporter, a_last == expected);
346             REPORTER_ASSERT(reporter, b_last == expected);
347         }
348     }
349 }
350 
assertIsMoveTo(skiatest::Reporter * reporter,SkPathPriv::RangeIter * iter,SkScalar x0,SkScalar y0)351 static void assertIsMoveTo(skiatest::Reporter* reporter, SkPathPriv::RangeIter* iter,
352                            SkScalar x0, SkScalar y0) {
353     auto [v, pts, w] = *(*iter)++;
354     REPORTER_ASSERT(reporter, v == SkPathVerb::kMove, "%d != %d (move)",
355                     (int)v, (int)SkPathVerb::kMove);
356     REPORTER_ASSERT(reporter, pts[0].fX == x0, "X mismatch %f != %f", pts[0].fX, x0);
357     REPORTER_ASSERT(reporter, pts[0].fY == y0, "Y mismatch %f != %f", pts[0].fY, y0);
358 }
359 
assertIsLineTo(skiatest::Reporter * reporter,SkPathPriv::RangeIter * iter,SkScalar x1,SkScalar y1)360 static void assertIsLineTo(skiatest::Reporter* reporter, SkPathPriv::RangeIter* iter,
361                            SkScalar x1, SkScalar y1) {
362     auto [v, pts, w] = *(*iter)++;
363     REPORTER_ASSERT(reporter, v == SkPathVerb::kLine, "%d != %d (line)",
364                     (int)v, (int)SkPathVerb::kLine);
365     // pts[0] is the moveTo before this line. See pts_backset_for_verb in SkPath::RangeIter
366     REPORTER_ASSERT(reporter, pts[1].fX == x1, "X mismatch %f != %f", pts[1].fX, x1);
367     REPORTER_ASSERT(reporter, pts[1].fY == y1, "Y mismatch %f != %f", pts[1].fY, y1);
368 }
369 
assertIsDone(skiatest::Reporter * reporter,SkPathPriv::RangeIter * iter,SkPath * p)370 static void assertIsDone(skiatest::Reporter* reporter, SkPathPriv::RangeIter* iter, SkPath* p) {
371     REPORTER_ASSERT(reporter, *iter == SkPathPriv::Iterate(*p).end(), "Iterator is not done yet");
372 }
373 
DEF_TEST(SkPathBuilder_lineToMoveTo,reporter)374 DEF_TEST(SkPathBuilder_lineToMoveTo, reporter) {
375     SkPathBuilder pb;
376     pb.moveTo(5, -1);
377     pb.moveTo(20, 3);
378     pb.lineTo(7, 11);
379     pb.lineTo(8, 12);
380     pb.moveTo(2, 3);
381     pb.lineTo(20, 30);
382 
383     SkPath result = pb.detach();
384 
385     auto iter = SkPathPriv::Iterate(result).begin();
386     assertIsMoveTo(reporter, &iter, 5, -1);
387     assertIsMoveTo(reporter, &iter, 20, 3);
388     assertIsLineTo(reporter, &iter, 7, 11);
389     assertIsLineTo(reporter, &iter, 8, 12);
390     assertIsMoveTo(reporter, &iter, 2, 3);
391     assertIsLineTo(reporter, &iter, 20, 30);
392     assertIsDone(reporter, &iter, &result);
393 }
394 
DEF_TEST(SkPathBuilder_arcToPtPtRad_invalidInputsResultInALine,reporter)395 DEF_TEST(SkPathBuilder_arcToPtPtRad_invalidInputsResultInALine, reporter) {
396     auto test = [&](const std::string& name, SkPoint start, SkPoint end, SkScalar radius,
397                     SkPoint expectedLineTo) {
398         SkPathBuilder pb;
399         // Remember there is an implicit moveTo(0, 0) if arcTo is the first command called.
400         pb.arcTo(start, end, radius);
401         SkPath result = pb.detach();
402 
403         reporter->push(name);
404         auto iter = SkPathPriv::Iterate(result).begin();
405         assertIsMoveTo(reporter, &iter, 0, 0);
406         assertIsLineTo(reporter, &iter, expectedLineTo.fX, expectedLineTo.fY);
407         assertIsDone(reporter, &iter, &result);
408         reporter->pop();
409     };
410     // From SkPathBuilder docs:
411     //   Arc is contained by tangent from last SkPath point to p1, and tangent from p1 to p2. Arc
412     //   is part of circle sized to radius, positioned so it touches both tangent lines.
413     // If the values cannot construct an arc, a line to the first point is constructed instead.
414     test("first point equals previous point", {0, 0}, {1, 2}, 1, {0, 0});
415     test("two points equal", {5, 7}, {5, 7}, 1, {5, 7});
416     test("radius is zero", {-3, 5}, {-7, 11}, 0, {-3, 5});
417     test("second point equals previous point", {5, 4}, {0, 0}, 1, {5, 4});
418 }
419