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/SkPathBuilder.h" 9#include "include/core/SkPathTypes.h" 10#include "src/core/SkPathPriv.h" 11#include "tests/Test.h" 12 13static void is_empty(skiatest::Reporter* reporter, const SkPath& p) { 14 REPORTER_ASSERT(reporter, p.getBounds().isEmpty()); 15 REPORTER_ASSERT(reporter, p.countPoints() == 0); 16} 17 18DEF_TEST(pathbuilder, reporter) { 19 SkPathBuilder b; 20 21 is_empty(reporter, b.snapshot()); 22 is_empty(reporter, b.detach()); 23 24 b.moveTo(10, 10).lineTo(20, 20).quadTo(30, 10, 10, 20); 25 26 SkPath p0 = b.snapshot(); 27 SkPath p1 = b.snapshot(); 28 SkPath p2 = b.detach(); 29 30 // Builders should always precompute the path's bounds, so there is no race condition later 31 REPORTER_ASSERT(reporter, SkPathPriv::HasComputedBounds(p0)); 32 REPORTER_ASSERT(reporter, SkPathPriv::HasComputedBounds(p1)); 33 REPORTER_ASSERT(reporter, SkPathPriv::HasComputedBounds(p2)); 34 35 REPORTER_ASSERT(reporter, p0.getBounds() == SkRect::MakeLTRB(10, 10, 30, 20)); 36 REPORTER_ASSERT(reporter, p0.countPoints() == 4); 37 38 REPORTER_ASSERT(reporter, p0 == p1); 39 REPORTER_ASSERT(reporter, p0 == p2); 40 41 is_empty(reporter, b.snapshot()); 42 is_empty(reporter, b.detach()); 43} 44 45DEF_TEST(pathbuilder_filltype, reporter) { 46 for (auto fillType : { SkPathFillType::kWinding, 47 SkPathFillType::kEvenOdd, 48 SkPathFillType::kInverseWinding, 49 SkPathFillType::kInverseEvenOdd }) { 50 SkPathBuilder b(fillType); 51 52 REPORTER_ASSERT(reporter, b.fillType() == fillType); 53 54 for (const SkPath& path : { b.snapshot(), b.detach() }) { 55 REPORTER_ASSERT(reporter, path.getFillType() == fillType); 56 is_empty(reporter, path); 57 } 58 } 59} 60 61static bool check_points(const SkPath& path, const SkPoint expected[], size_t count) { 62 std::vector<SkPoint> iter_pts; 63 64 for (auto [v, p, w] : SkPathPriv::Iterate(path)) { 65 switch (v) { 66 case SkPathVerb::kMove: 67 iter_pts.push_back(p[0]); 68 break; 69 case SkPathVerb::kLine: 70 iter_pts.push_back(p[1]); 71 break; 72 case SkPathVerb::kQuad: 73 case SkPathVerb::kConic: 74 iter_pts.push_back(p[1]); 75 iter_pts.push_back(p[2]); 76 break; 77 case SkPathVerb::kCubic: 78 iter_pts.push_back(p[1]); 79 iter_pts.push_back(p[2]); 80 iter_pts.push_back(p[3]); 81 break; 82 case SkPathVerb::kClose: 83 break; 84 } 85 } 86 if (iter_pts.size() != count) { 87 return false; 88 } 89 for (size_t i = 0; i < count; ++i) { 90 if (iter_pts[i] != expected[i]) { 91 return false; 92 } 93 } 94 return true; 95} 96 97DEF_TEST(pathbuilder_missing_move, reporter) { 98 SkPathBuilder b; 99 100 b.lineTo(10, 10).lineTo(20, 30); 101 const SkPoint pts0[] = { 102 {0, 0}, {10, 10}, {20, 30}, 103 }; 104 REPORTER_ASSERT(reporter, check_points(b.snapshot(), pts0, SK_ARRAY_COUNT(pts0))); 105 106 b.reset().moveTo(20, 20).lineTo(10, 10).lineTo(20, 30).close().lineTo(60, 60); 107 const SkPoint pts1[] = { 108 {20, 20}, {10, 10}, {20, 30}, 109 {20, 20}, {60, 60}, 110 }; 111 REPORTER_ASSERT(reporter, check_points(b.snapshot(), pts1, SK_ARRAY_COUNT(pts1))); 112} 113 114DEF_TEST(pathbuilder_addRect, reporter) { 115 const SkRect r = { 10, 20, 30, 40 }; 116 117 for (int i = 0; i < 4; ++i) { 118 for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { 119 SkPathBuilder b; 120 b.addRect(r, dir, i); 121 auto bp = b.detach(); 122 123 SkRect r2; 124 bool closed = false; 125 SkPathDirection dir2; 126 REPORTER_ASSERT(reporter, bp.isRect(&r2, &closed, &dir2)); 127 REPORTER_ASSERT(reporter, r2 == r); 128 REPORTER_ASSERT(reporter, closed); 129 REPORTER_ASSERT(reporter, dir == dir2); 130 131 SkPath p; 132 p.addRect(r, dir, i); 133 REPORTER_ASSERT(reporter, p == bp); 134 } 135 } 136} 137 138static bool is_eq(const SkPath& a, const SkPath& b) { 139 if (a != b) { 140 return false; 141 } 142 143 { 144 SkRect ra, rb; 145 bool is_a = a.isOval(&ra); 146 bool is_b = b.isOval(&rb); 147 if (is_a != is_b) { 148 return false; 149 } 150 if (is_a && (ra != rb)) { 151 return false; 152 } 153 } 154 155 { 156 SkRRect rra, rrb; 157 bool is_a = a.isRRect(&rra); 158 bool is_b = b.isRRect(&rrb); 159 if (is_a != is_b) { 160 return false; 161 } 162 if (is_a && (rra != rrb)) { 163 return false; 164 } 165 } 166 167 // getConvextity() should be sufficient to test, but internally we sometimes don't want 168 // to trigger computing it, so this is the stronger test for equality. 169 { 170 SkPathConvexity ca = SkPathPriv::GetConvexityOrUnknown(a), 171 cb = SkPathPriv::GetConvexityOrUnknown(b); 172 if (ca != cb) { 173 return false; 174 } 175 } 176 177 return true; 178} 179 180DEF_TEST(pathbuilder_addOval, reporter) { 181 const SkRect r = { 10, 20, 30, 40 }; 182 SkRect tmp; 183 184 for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { 185 for (int i = 0; i < 4; ++i) { 186 auto bp = SkPathBuilder().addOval(r, dir, i).detach(); 187 SkPath p; 188 p.addOval(r, dir, i); 189 REPORTER_ASSERT(reporter, is_eq(p, bp)); 190 } 191 auto bp = SkPathBuilder().addOval(r, dir).detach(); 192 SkPath p; 193 p.addOval(r, dir); 194 REPORTER_ASSERT(reporter, is_eq(p, bp)); 195 196 // test negative case -- can't have any other segments 197 bp = SkPathBuilder().addOval(r, dir).lineTo(10, 10).detach(); 198 REPORTER_ASSERT(reporter, !bp.isOval(&tmp)); 199 bp = SkPathBuilder().lineTo(10, 10).addOval(r, dir).detach(); 200 REPORTER_ASSERT(reporter, !bp.isOval(&tmp)); 201 } 202} 203 204DEF_TEST(pathbuilder_addRRect, reporter) { 205 const SkRRect rr = SkRRect::MakeRectXY({ 10, 20, 30, 40 }, 5, 6); 206 207 for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) { 208 for (int i = 0; i < 4; ++i) { 209 SkPathBuilder b; 210 b.addRRect(rr, dir, i); 211 auto bp = b.detach(); 212 213 SkPath p; 214 p.addRRect(rr, dir, i); 215 REPORTER_ASSERT(reporter, is_eq(p, bp)); 216 } 217 auto bp = SkPathBuilder().addRRect(rr, dir).detach(); 218 SkPath p; 219 p.addRRect(rr, dir); 220 REPORTER_ASSERT(reporter, is_eq(p, bp)); 221 222 // test negative case -- can't have any other segments 223 SkRRect tmp; 224 bp = SkPathBuilder().addRRect(rr, dir).lineTo(10, 10).detach(); 225 REPORTER_ASSERT(reporter, !bp.isRRect(&tmp)); 226 bp = SkPathBuilder().lineTo(10, 10).addRRect(rr, dir).detach(); 227 REPORTER_ASSERT(reporter, !bp.isRRect(&tmp)); 228 } 229} 230 231#include "include/utils/SkRandom.h" 232 233DEF_TEST(pathbuilder_make, reporter) { 234 constexpr int N = 100; 235 uint8_t vbs[N]; 236 SkPoint pts[N]; 237 238 SkRandom rand; 239 SkPathBuilder b; 240 b.moveTo(0, 0); 241 pts[0] = {0, 0}; vbs[0] = (uint8_t)SkPathVerb::kMove; 242 for (int i = 1; i < N; ++i) { 243 float x = rand.nextF(); 244 float y = rand.nextF(); 245 b.lineTo(x, y); 246 pts[i] = {x, y}; vbs[i] = (uint8_t)SkPathVerb::kLine; 247 } 248 auto p0 = b.detach(); 249 auto p1 = SkPath::Make(pts, N, vbs, N, nullptr, 0, p0.getFillType()); 250 REPORTER_ASSERT(reporter, p0 == p1); 251} 252 253DEF_TEST(pathbuilder_genid, r) { 254 SkPathBuilder builder; 255 256 builder.lineTo(10, 10); 257 auto p1 = builder.snapshot(); 258 259 builder.lineTo(10, 20); 260 auto p2 = builder.snapshot(); 261 262 REPORTER_ASSERT(r, p1.getGenerationID() != p2.getGenerationID()); 263} 264 265DEF_TEST(pathbuilder_addPolygon, reporter) { 266 SkPoint pts[] = {{1, 2}, {3, 4}, {5, 6}, {7, 8}}; 267 268 auto addpoly = [](const SkPoint pts[], int count, bool isClosed) { 269 SkPathBuilder builder; 270 if (count > 0) { 271 builder.moveTo(pts[0]); 272 for (int i = 1; i < count; ++i) { 273 builder.lineTo(pts[i]); 274 } 275 if (isClosed) { 276 builder.close(); 277 } 278 } 279 return builder.detach(); 280 }; 281 282 for (bool isClosed : {false, true}) { 283 for (size_t i = 0; i <= SK_ARRAY_COUNT(pts); ++i) { 284 auto path0 = SkPathBuilder().addPolygon(pts, i, isClosed).detach(); 285 auto path1 = addpoly(pts, i, isClosed); 286 REPORTER_ASSERT(reporter, path0 == path1); 287 } 288 } 289} 290 291DEF_TEST(pathbuilder_shrinkToFit, reporter) { 292 // SkPathBuilder::snapshot() creates copies of its arrays for perfectly sized paths, 293 // where SkPathBuilder::detach() moves its larger scratch arrays for speed. 294 bool any_smaller = false; 295 for (int pts = 0; pts < 10; pts++) { 296 297 SkPathBuilder b; 298 for (int i = 0; i < pts; i++) { 299 b.lineTo(i,i); 300 } 301 b.close(); 302 303 SkPath s = b.snapshot(), 304 d = b.detach(); 305 REPORTER_ASSERT(reporter, s.approximateBytesUsed() <= d.approximateBytesUsed()); 306 any_smaller |= s.approximateBytesUsed() < d.approximateBytesUsed(); 307 } 308 REPORTER_ASSERT(reporter, any_smaller); 309} 310 311DEF_TEST(pathbuilder_addPath, reporter) { 312 const auto p = SkPath() 313 .moveTo(10, 10) 314 .lineTo(100, 10) 315 .quadTo(200, 100, 100, 200) 316 .close() 317 .moveTo(200, 200) 318 .cubicTo(210, 200, 210, 300, 200, 300) 319 .conicTo(150, 250, 100, 200, 1.4f); 320 321 REPORTER_ASSERT(reporter, p == SkPathBuilder().addPath(p).detach()); 322} 323 324/* 325 * If paths were immutable, we would not have to track this, but until that day, we need 326 * to ensure that paths are built correctly/consistently with this field, regardless of 327 * either the classic mutable apis, or via SkPathBuilder (SkPath::Polygon uses builder). 328 */ 329DEF_TEST(pathbuilder_lastmoveindex, reporter) { 330 const SkPoint pts[] = { 331 {0, 1}, {2, 3}, {4, 5}, 332 }; 333 constexpr int N = (int)SK_ARRAY_COUNT(pts); 334 335 for (int ctrCount = 1; ctrCount < 4; ++ctrCount) { 336 const int lastMoveToIndex = (ctrCount - 1) * N; 337 338 for (bool isClosed : {false, true}) { 339 SkPath a, b; 340 341 SkPathBuilder builder; 342 for (int i = 0; i < ctrCount; ++i) { 343 builder.addPolygon(pts, N, isClosed); // new-school way 344 b.addPoly(pts, N, isClosed); // old-school way 345 } 346 a = builder.detach(); 347 348 // We track the last moveTo verb index, and we invert it if the last verb was a close 349 const int expected = isClosed ? ~lastMoveToIndex : lastMoveToIndex; 350 const int a_last = SkPathPriv::LastMoveToIndex(a); 351 const int b_last = SkPathPriv::LastMoveToIndex(b); 352 353 REPORTER_ASSERT(reporter, a_last == expected); 354 REPORTER_ASSERT(reporter, b_last == expected); 355 } 356 } 357} 358