OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright 2013 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 #include "PathOpsTestCommon.h" |
| 8 #include "SkIntersections.h" |
| 9 #include "SkOpSegment.h" |
| 10 #include "SkPathOpsTriangle.h" |
| 11 #include "SkRandom.h" |
| 12 #include "SkTArray.h" |
| 13 #include "SkTSort.h" |
| 14 #include "Test.h" |
| 15 |
| 16 static bool gPathOpsAngleIdeasVerbose = false; |
| 17 static bool gPathOpsAngleIdeasEnableBruteCheck = false; |
| 18 |
| 19 class PathOpsAngleTester { |
| 20 public: |
| 21 static int ConvexHullOverlaps(const SkOpAngle& lh, const SkOpAngle& rh) { |
| 22 return lh.convexHullOverlaps(rh); |
| 23 } |
| 24 |
| 25 static int EndsIntersect(const SkOpAngle& lh, const SkOpAngle& rh) { |
| 26 return lh.endsIntersect(rh); |
| 27 } |
| 28 }; |
| 29 |
| 30 struct TRange { |
| 31 double tMin1; |
| 32 double tMin2; |
| 33 double t1; |
| 34 double t2; |
| 35 double tMin; |
| 36 double a1; |
| 37 double a2; |
| 38 bool ccw; |
| 39 }; |
| 40 |
| 41 static double testArc(skiatest::Reporter* reporter, const SkDQuad& quad, const S
kDQuad& arcRef, |
| 42 int octant) { |
| 43 SkDQuad arc = arcRef; |
| 44 SkDVector offset = {quad[0].fX, quad[0].fY}; |
| 45 arc[0] += offset; |
| 46 arc[1] += offset; |
| 47 arc[2] += offset; |
| 48 SkIntersections i; |
| 49 i.intersect(arc, quad); |
| 50 if (i.used() == 0) { |
| 51 return -1; |
| 52 } |
| 53 int smallest = -1; |
| 54 double t = 2; |
| 55 for (int idx = 0; idx < i.used(); ++idx) { |
| 56 if (i[0][idx] > 1 || i[0][idx] < 0) { |
| 57 i.reset(); |
| 58 i.intersect(arc, quad); |
| 59 } |
| 60 if (i[1][idx] > 1 || i[1][idx] < 0) { |
| 61 i.reset(); |
| 62 i.intersect(arc, quad); |
| 63 } |
| 64 if (t > i[1][idx]) { |
| 65 smallest = idx; |
| 66 t = i[1][idx]; |
| 67 } |
| 68 } |
| 69 REPORTER_ASSERT(reporter, smallest >= 0); |
| 70 REPORTER_ASSERT(reporter, t >= 0 && t <= 1); |
| 71 return i[1][smallest]; |
| 72 } |
| 73 |
| 74 static void orderQuads(skiatest::Reporter* reporter, const SkDQuad& quad, double
radius, |
| 75 SkTArray<double, false>* tArray) { |
| 76 double r = radius; |
| 77 double s = r * SK_ScalarTanPIOver8; |
| 78 double m = r * SK_ScalarRoot2Over2; |
| 79 // construct circle from quads |
| 80 const SkDQuad circle[8] = {{{{ r, 0}, { r, -s}, { m, -m}}}, |
| 81 {{{ m, -m}, { s, -r}, { 0, -r}}}, |
| 82 {{{ 0, -r}, {-s, -r}, {-m, -m}}}, |
| 83 {{{-m, -m}, {-r, -s}, {-r, 0}}}, |
| 84 {{{-r, 0}, {-r, s}, {-m, m}}}, |
| 85 {{{-m, m}, {-s, r}, { 0, r}}}, |
| 86 {{{ 0, r}, { s, r}, { m, m}}}, |
| 87 {{{ m, m}, { r, s}, { r, 0}}}}; |
| 88 for (int octant = 0; octant < 8; ++octant) { |
| 89 double t = testArc(reporter, quad, circle[octant], octant); |
| 90 if (t < 0) { |
| 91 continue; |
| 92 } |
| 93 for (int index = 0; index < tArray->count(); ++index) { |
| 94 double matchT = (*tArray)[index]; |
| 95 if (approximately_equal(t, matchT)) { |
| 96 goto next; |
| 97 } |
| 98 } |
| 99 tArray->push_back(t); |
| 100 next: ; |
| 101 } |
| 102 } |
| 103 |
| 104 static double quadAngle(skiatest::Reporter* reporter, const SkDQuad& quad, doubl
e t) { |
| 105 const SkDVector& pt = quad.ptAtT(t) - quad[0]; |
| 106 double angle = (atan2(pt.fY, pt.fX) + SK_ScalarPI) * 8 / (SK_ScalarPI * 2); |
| 107 REPORTER_ASSERT(reporter, angle >= 0 && angle <= 8); |
| 108 return angle; |
| 109 } |
| 110 |
| 111 static bool angleDirection(double a1, double a2) { |
| 112 double delta = a1 - a2; |
| 113 return (delta < 4 && delta > 0) || delta < -4; |
| 114 } |
| 115 |
| 116 static void setQuadHullSweep(const SkDQuad& quad, SkDVector sweep[2]) { |
| 117 sweep[0] = quad[1] - quad[0]; |
| 118 sweep[1] = quad[2] - quad[0]; |
| 119 } |
| 120 |
| 121 static double distEndRatio(double dist, const SkDQuad& quad) { |
| 122 SkDVector v[] = {quad[2] - quad[0], quad[1] - quad[0], quad[2] - quad[1]}; |
| 123 double longest = SkTMax(v[0].length(), SkTMax(v[1].length(), v[2].length()))
; |
| 124 return longest / dist; |
| 125 } |
| 126 |
| 127 static bool checkParallel(skiatest::Reporter* reporter, const SkDQuad& quad1, co
nst SkDQuad& quad2) { |
| 128 SkDVector sweep[2], tweep[2]; |
| 129 setQuadHullSweep(quad1, sweep); |
| 130 setQuadHullSweep(quad2, tweep); |
| 131 // if the ctrl tangents are not nearly parallel, use them |
| 132 // solve for opposite direction displacement scale factor == m |
| 133 // initial dir = v1.cross(v2) == v2.x * v1.y - v2.y * v1.x |
| 134 // displacement of q1[1] : dq1 = { -m * v1.y, m * v1.x } + q1[1] |
| 135 // straight angle when : v2.x * (dq1.y - q1[0].y) == v2.y * (dq1.x - q1[0].x
) |
| 136 // v2.x * (m * v1.x + v1.y) == v2.y * (-m * v1.y + v1.
x) |
| 137 // - m * (v2.x * v1.x + v2.y * v1.y) == v2.x * v1.y - v2.y * v1.x |
| 138 // m = (v2.y * v1.x - v2.x * v1.y) / (v2.x * v1.x + v2.y * v1.y) |
| 139 // m = v1.cross(v2) / v1.dot(v2) |
| 140 double s0dt0 = sweep[0].dot(tweep[0]); |
| 141 REPORTER_ASSERT(reporter, s0dt0 != 0); |
| 142 double s0xt0 = sweep[0].crossCheck(tweep[0]); |
| 143 double m = s0xt0 / s0dt0; |
| 144 double sDist = sweep[0].length() * m; |
| 145 double tDist = tweep[0].length() * m; |
| 146 bool useS = fabs(sDist) < fabs(tDist); |
| 147 double mFactor = fabs(useS ? distEndRatio(sDist, quad1) : distEndRatio(tDist
, quad2)); |
| 148 if (mFactor < 5000) { // empirically found limit |
| 149 return s0xt0 < 0; |
| 150 } |
| 151 SkDVector m0 = quad1.ptAtT(0.5) - quad1[0]; |
| 152 SkDVector m1 = quad2.ptAtT(0.5) - quad2[0]; |
| 153 return m0.crossCheck(m1) < 0; |
| 154 } |
| 155 |
| 156 /* returns |
| 157 -1 if overlaps |
| 158 0 if no overlap cw |
| 159 1 if no overlap ccw |
| 160 */ |
| 161 static int quadHullsOverlap(skiatest::Reporter* reporter, const SkDQuad& quad1, |
| 162 const SkDQuad& quad2) { |
| 163 SkDVector sweep[2], tweep[2]; |
| 164 setQuadHullSweep(quad1, sweep); |
| 165 setQuadHullSweep(quad2, tweep); |
| 166 double s0xs1 = sweep[0].crossCheck(sweep[1]); |
| 167 double s0xt0 = sweep[0].crossCheck(tweep[0]); |
| 168 double s1xt0 = sweep[1].crossCheck(tweep[0]); |
| 169 bool tBetweenS = s0xs1 > 0 ? s0xt0 > 0 && s1xt0 < 0 : s0xt0 < 0 && s1xt0 > 0
; |
| 170 double s0xt1 = sweep[0].crossCheck(tweep[1]); |
| 171 double s1xt1 = sweep[1].crossCheck(tweep[1]); |
| 172 tBetweenS |= s0xs1 > 0 ? s0xt1 > 0 && s1xt1 < 0 : s0xt1 < 0 && s1xt1 > 0; |
| 173 double t0xt1 = tweep[0].crossCheck(tweep[1]); |
| 174 if (tBetweenS) { |
| 175 return -1; |
| 176 } |
| 177 if ((s0xt0 == 0 && s1xt1 == 0) || (s1xt0 == 0 && s0xt1 == 0)) { // s0 to s1
equals t0 to t1 |
| 178 return -1; |
| 179 } |
| 180 bool sBetweenT = t0xt1 > 0 ? s0xt0 < 0 && s0xt1 > 0 : s0xt0 > 0 && s0xt1 < 0
; |
| 181 sBetweenT |= t0xt1 > 0 ? s1xt0 < 0 && s1xt1 > 0 : s1xt0 > 0 && s1xt1 < 0; |
| 182 if (sBetweenT) { |
| 183 return -1; |
| 184 } |
| 185 // if all of the sweeps are in the same half plane, then the order of any pa
ir is enough |
| 186 if (s0xt0 >= 0 && s0xt1 >= 0 && s1xt0 >= 0 && s1xt1 >= 0) { |
| 187 return 0; |
| 188 } |
| 189 if (s0xt0 <= 0 && s0xt1 <= 0 && s1xt0 <= 0 && s1xt1 <= 0) { |
| 190 return 1; |
| 191 } |
| 192 // if the outside sweeps are greater than 180 degress: |
| 193 // first assume the inital tangents are the ordering |
| 194 // if the midpoint direction matches the inital order, that is enough |
| 195 SkDVector m0 = quad1.ptAtT(0.5) - quad1[0]; |
| 196 SkDVector m1 = quad2.ptAtT(0.5) - quad2[0]; |
| 197 double m0xm1 = m0.crossCheck(m1); |
| 198 if (s0xt0 > 0 && m0xm1 > 0) { |
| 199 return 0; |
| 200 } |
| 201 if (s0xt0 < 0 && m0xm1 < 0) { |
| 202 return 1; |
| 203 } |
| 204 REPORTER_ASSERT(reporter, s0xt0 != 0); |
| 205 return checkParallel(reporter, quad1, quad2); |
| 206 } |
| 207 |
| 208 static double radianSweep(double start, double end) { |
| 209 double sweep = end - start; |
| 210 if (sweep > SK_ScalarPI) { |
| 211 sweep -= 2 * SK_ScalarPI; |
| 212 } else if (sweep < -SK_ScalarPI) { |
| 213 sweep += 2 * SK_ScalarPI; |
| 214 } |
| 215 return sweep; |
| 216 } |
| 217 |
| 218 static bool radianBetween(double start, double test, double end) { |
| 219 double startToEnd = radianSweep(start, end); |
| 220 double startToTest = radianSweep(start, test); |
| 221 double testToEnd = radianSweep(test, end); |
| 222 return (startToTest <= 0 && testToEnd <= 0 && startToTest >= startToEnd) || |
| 223 (startToTest >= 0 && testToEnd >= 0 && startToTest <= startToEnd); |
| 224 } |
| 225 |
| 226 static bool orderTRange(skiatest::Reporter* reporter, const SkDQuad& quad1, cons
t SkDQuad& quad2, |
| 227 double r, TRange* result) { |
| 228 SkTArray<double, false> t1Array, t2Array; |
| 229 orderQuads(reporter, quad1, r, &t1Array); |
| 230 orderQuads(reporter,quad2, r, &t2Array); |
| 231 if (!t1Array.count() || !t2Array.count()) { |
| 232 return false; |
| 233 } |
| 234 SkTQSort<double>(t1Array.begin(), t1Array.end() - 1); |
| 235 SkTQSort<double>(t2Array.begin(), t2Array.end() - 1); |
| 236 double t1 = result->tMin1 = t1Array[0]; |
| 237 double t2 = result->tMin2 = t2Array[0]; |
| 238 double a1 = quadAngle(reporter,quad1, t1); |
| 239 double a2 = quadAngle(reporter,quad2, t2); |
| 240 if (approximately_equal(a1, a2)) { |
| 241 return false; |
| 242 } |
| 243 bool refCCW = angleDirection(a1, a2); |
| 244 result->t1 = t1; |
| 245 result->t2 = t2; |
| 246 result->tMin = SkTMin(t1, t2); |
| 247 result->a1 = a1; |
| 248 result->a2 = a2; |
| 249 result->ccw = refCCW; |
| 250 return true; |
| 251 } |
| 252 |
| 253 static bool equalPoints(const SkDPoint& pt1, const SkDPoint& pt2, double max) { |
| 254 return approximately_zero_when_compared_to(pt1.fX - pt2.fX, max) |
| 255 && approximately_zero_when_compared_to(pt1.fY - pt2.fY, max); |
| 256 } |
| 257 |
| 258 static double maxDist(const SkDQuad& quad) { |
| 259 SkDRect bounds; |
| 260 bounds.setBounds(quad); |
| 261 SkDVector corner[4] = { |
| 262 { bounds.fLeft - quad[0].fX, bounds.fTop - quad[0].fY }, |
| 263 { bounds.fRight - quad[0].fX, bounds.fTop - quad[0].fY }, |
| 264 { bounds.fLeft - quad[0].fX, bounds.fBottom - quad[0].fY }, |
| 265 { bounds.fRight - quad[0].fX, bounds.fBottom - quad[0].fY } |
| 266 }; |
| 267 double max = 0; |
| 268 for (unsigned index = 0; index < SK_ARRAY_COUNT(corner); ++index) { |
| 269 max = SkTMax(max, corner[index].length()); |
| 270 } |
| 271 return max; |
| 272 } |
| 273 |
| 274 static double maxQuad(const SkDQuad& quad) { |
| 275 double max = 0; |
| 276 for (int index = 0; index < 2; ++index) { |
| 277 max = SkTMax(max, fabs(quad[index].fX)); |
| 278 max = SkTMax(max, fabs(quad[index].fY)); |
| 279 } |
| 280 return max; |
| 281 } |
| 282 |
| 283 static bool bruteMinT(skiatest::Reporter* reporter, const SkDQuad& quad1, const
SkDQuad& quad2, |
| 284 TRange* lowerRange, TRange* upperRange) { |
| 285 double maxRadius = SkTMin(maxDist(quad1), maxDist(quad2)); |
| 286 double maxQuads = SkTMax(maxQuad(quad1), maxQuad(quad2)); |
| 287 double r = maxRadius / 2; |
| 288 double rStep = r / 2; |
| 289 SkDPoint best1 = {SK_ScalarInfinity, SK_ScalarInfinity}; |
| 290 SkDPoint best2 = {SK_ScalarInfinity, SK_ScalarInfinity}; |
| 291 int bestCCW = -1; |
| 292 double bestR = maxRadius; |
| 293 upperRange->tMin = 0; |
| 294 lowerRange->tMin = 1; |
| 295 do { |
| 296 do { // find upper bounds of single result |
| 297 TRange tRange; |
| 298 bool stepUp = orderTRange(reporter, quad1, quad2, r, &tRange); |
| 299 if (stepUp) { |
| 300 SkDPoint pt1 = quad1.ptAtT(tRange.t1); |
| 301 if (equalPoints(pt1, best1, maxQuads)) { |
| 302 break; |
| 303 } |
| 304 best1 = pt1; |
| 305 SkDPoint pt2 = quad2.ptAtT(tRange.t2); |
| 306 if (equalPoints(pt2, best2, maxQuads)) { |
| 307 break; |
| 308 } |
| 309 best2 = pt2; |
| 310 if (gPathOpsAngleIdeasVerbose) { |
| 311 SkDebugf("u bestCCW=%d ccw=%d bestMin=%1.9g:%1.9g r=%1.9g tM
in=%1.9g\n", |
| 312 bestCCW, tRange.ccw, lowerRange->tMin, upperRange->t
Min, r, |
| 313 tRange.tMin); |
| 314 } |
| 315 if (bestCCW >= 0 && bestCCW != (int) tRange.ccw) { |
| 316 if (tRange.tMin < upperRange->tMin) { |
| 317 upperRange->tMin = 0; |
| 318 } else { |
| 319 stepUp = false; |
| 320 } |
| 321 } |
| 322 if (upperRange->tMin < tRange.tMin) { |
| 323 bestCCW = tRange.ccw; |
| 324 bestR = r; |
| 325 *upperRange = tRange; |
| 326 } |
| 327 if (lowerRange->tMin > tRange.tMin) { |
| 328 *lowerRange = tRange; |
| 329 } |
| 330 } |
| 331 r += stepUp ? rStep : -rStep; |
| 332 rStep /= 2; |
| 333 } while (rStep > FLT_EPSILON); |
| 334 if (bestCCW < 0) { |
| 335 REPORTER_ASSERT(reporter, bestR < maxRadius); |
| 336 return false; |
| 337 } |
| 338 double lastHighR = bestR; |
| 339 r = bestR / 2; |
| 340 rStep = r / 2; |
| 341 do { // find lower bounds of single result |
| 342 TRange tRange; |
| 343 bool success = orderTRange(reporter, quad1, quad2, r, &tRange); |
| 344 if (success) { |
| 345 if (gPathOpsAngleIdeasVerbose) { |
| 346 SkDebugf("l bestCCW=%d ccw=%d bestMin=%1.9g:%1.9g r=%1.9g tM
in=%1.9g\n", |
| 347 bestCCW, tRange.ccw, lowerRange->tMin, upperRange->t
Min, r, |
| 348 tRange.tMin); |
| 349 } |
| 350 if (bestCCW != (int) tRange.ccw || upperRange->tMin < tRange.tMi
n) { |
| 351 bestCCW = tRange.ccw; |
| 352 *upperRange = tRange; |
| 353 bestR = lastHighR; |
| 354 break; // need to establish a new upper bounds |
| 355 } |
| 356 SkDPoint pt1 = quad1.ptAtT(tRange.t1); |
| 357 SkDPoint pt2 = quad2.ptAtT(tRange.t2); |
| 358 if (equalPoints(pt1, best1, maxQuads)) { |
| 359 goto breakOut; |
| 360 } |
| 361 best1 = pt1; |
| 362 if (equalPoints(pt2, best2, maxQuads)) { |
| 363 goto breakOut; |
| 364 } |
| 365 best2 = pt2; |
| 366 if (equalPoints(pt1, pt2, maxQuads)) { |
| 367 success = false; |
| 368 } else { |
| 369 if (upperRange->tMin < tRange.tMin) { |
| 370 *upperRange = tRange; |
| 371 } |
| 372 if (lowerRange->tMin > tRange.tMin) { |
| 373 *lowerRange = tRange; |
| 374 } |
| 375 } |
| 376 lastHighR = SkTMin(r, lastHighR); |
| 377 } |
| 378 r += success ? -rStep : rStep; |
| 379 rStep /= 2; |
| 380 } while (rStep > FLT_EPSILON); |
| 381 } while (rStep > FLT_EPSILON); |
| 382 breakOut: |
| 383 if (gPathOpsAngleIdeasVerbose) { |
| 384 SkDebugf("l a2-a1==%1.9g\n", lowerRange->a2 - lowerRange->a1); |
| 385 } |
| 386 return true; |
| 387 } |
| 388 |
| 389 static void bruteForce(skiatest::Reporter* reporter, const SkDQuad& quad1, const
SkDQuad& quad2, |
| 390 bool ccw) { |
| 391 if (!gPathOpsAngleIdeasEnableBruteCheck) { |
| 392 return; |
| 393 } |
| 394 TRange lowerRange, upperRange; |
| 395 bool result = bruteMinT(reporter, quad1, quad2, &lowerRange, &upperRange); |
| 396 REPORTER_ASSERT(reporter, result); |
| 397 double angle = fabs(lowerRange.a2 - lowerRange.a1); |
| 398 REPORTER_ASSERT(reporter, angle > 3.998 || ccw == upperRange.ccw); |
| 399 } |
| 400 |
| 401 static bool bruteForceCheck(skiatest::Reporter* reporter, const SkDQuad& quad1, |
| 402 const SkDQuad& quad2, bool ccw) { |
| 403 TRange lowerRange, upperRange; |
| 404 bool result = bruteMinT(reporter, quad1, quad2, &lowerRange, &upperRange); |
| 405 REPORTER_ASSERT(reporter, result); |
| 406 return ccw == upperRange.ccw; |
| 407 } |
| 408 |
| 409 class PathOpsSegmentTester { |
| 410 public: |
| 411 static void ConstructQuad(SkOpSegment* segment, SkPoint shortQuad[3]) { |
| 412 segment->debugConstructQuad(shortQuad); |
| 413 } |
| 414 }; |
| 415 |
| 416 static void makeSegment(const SkDQuad& quad, SkPoint shortQuad[3], SkOpSegment*
result) { |
| 417 shortQuad[0] = quad[0].asSkPoint(); |
| 418 shortQuad[1] = quad[1].asSkPoint(); |
| 419 shortQuad[2] = quad[2].asSkPoint(); |
| 420 PathOpsSegmentTester::ConstructQuad(result, shortQuad); |
| 421 } |
| 422 |
| 423 static void testQuadAngles(skiatest::Reporter* reporter, const SkDQuad& quad1, c
onst SkDQuad& quad2, |
| 424 int testNo) { |
| 425 SkPoint shortQuads[2][3]; |
| 426 SkOpSegment seg[2]; |
| 427 makeSegment(quad1, shortQuads[0], &seg[0]); |
| 428 makeSegment(quad2, shortQuads[1], &seg[1]); |
| 429 int realOverlap = PathOpsAngleTester::ConvexHullOverlaps(seg[0].angle(0), se
g[1].angle(0)); |
| 430 const SkDPoint& origin = quad1[0]; |
| 431 REPORTER_ASSERT(reporter, origin == quad2[0]); |
| 432 double a1s = atan2(origin.fY - quad1[1].fY, quad1[1].fX - origin.fX); |
| 433 double a1e = atan2(origin.fY - quad1[2].fY, quad1[2].fX - origin.fX); |
| 434 double a2s = atan2(origin.fY - quad2[1].fY, quad2[1].fX - origin.fX); |
| 435 double a2e = atan2(origin.fY - quad2[2].fY, quad2[2].fX - origin.fX); |
| 436 bool oldSchoolOverlap = radianBetween(a1s, a2s, a1e) |
| 437 || radianBetween(a1s, a2e, a1e) || radianBetween(a2s, a1s, a2e) |
| 438 || radianBetween(a2s, a1e, a2e); |
| 439 int overlap = quadHullsOverlap(reporter, quad1, quad2); |
| 440 bool realMatchesOverlap = realOverlap == overlap || SK_ScalarPI - fabs(a2s -
a1s) < 0.002; |
| 441 if (realOverlap != overlap) { |
| 442 SkDebugf("\nSK_ScalarPI - fabs(a2s - a1s) = %1.9g\n", SK_ScalarPI - fabs
(a2s - a1s)); |
| 443 } |
| 444 if (!realMatchesOverlap) { |
| 445 DumpQ(quad1, quad2, testNo); |
| 446 } |
| 447 REPORTER_ASSERT(reporter, realMatchesOverlap); |
| 448 if (oldSchoolOverlap != (overlap < 0)) { |
| 449 overlap = quadHullsOverlap(reporter, quad1, quad2); // set a breakpoint
and debug if assert fires |
| 450 REPORTER_ASSERT(reporter, oldSchoolOverlap == (overlap < 0)); |
| 451 } |
| 452 SkDVector v1s = quad1[1] - quad1[0]; |
| 453 SkDVector v1e = quad1[2] - quad1[0]; |
| 454 SkDVector v2s = quad2[1] - quad2[0]; |
| 455 SkDVector v2e = quad2[2] - quad2[0]; |
| 456 double vDir[2] = { v1s.cross(v1e), v2s.cross(v2e) }; |
| 457 bool ray1In2 = v1s.cross(v2s) * vDir[1] <= 0 && v1s.cross(v2e) * vDir[1] >=
0; |
| 458 bool ray2In1 = v2s.cross(v1s) * vDir[0] <= 0 && v2s.cross(v1e) * vDir[0] >=
0; |
| 459 if (overlap >= 0) { |
| 460 // verify that hulls really don't overlap |
| 461 REPORTER_ASSERT(reporter, !ray1In2); |
| 462 REPORTER_ASSERT(reporter, !ray2In1); |
| 463 bool ctrl1In2 = v1e.cross(v2s) * vDir[1] <= 0 && v1e.cross(v2e) * vDir[1
] >= 0; |
| 464 REPORTER_ASSERT(reporter, !ctrl1In2); |
| 465 bool ctrl2In1 = v2e.cross(v1s) * vDir[0] <= 0 && v2e.cross(v1e) * vDir[0
] >= 0; |
| 466 REPORTER_ASSERT(reporter, !ctrl2In1); |
| 467 // check answer against reference |
| 468 bruteForce(reporter, quad1, quad2, overlap > 0); |
| 469 } |
| 470 // continue end point rays and see if they intersect the opposite curve |
| 471 SkDLine rays[] = {{{origin, quad2[2]}}, {{origin, quad1[2]}}}; |
| 472 const SkDQuad* quads[] = {&quad1, &quad2}; |
| 473 SkDVector midSpokes[2]; |
| 474 SkIntersections intersect[2]; |
| 475 double minX, minY, maxX, maxY; |
| 476 minX = minY = SK_ScalarInfinity; |
| 477 maxX = maxY = -SK_ScalarInfinity; |
| 478 double maxWidth = 0; |
| 479 bool useIntersect = false; |
| 480 double smallestTs[] = {1, 1}; |
| 481 for (unsigned index = 0; index < SK_ARRAY_COUNT(quads); ++index) { |
| 482 const SkDQuad& q = *quads[index]; |
| 483 midSpokes[index] = q.ptAtT(0.5) - origin; |
| 484 minX = SkTMin(SkTMin(SkTMin(minX, origin.fX), q[1].fX), q[2].fX); |
| 485 minY = SkTMin(SkTMin(SkTMin(minY, origin.fY), q[1].fY), q[2].fY); |
| 486 maxX = SkTMax(SkTMax(SkTMax(maxX, origin.fX), q[1].fX), q[2].fX); |
| 487 maxY = SkTMax(SkTMax(SkTMax(maxY, origin.fY), q[1].fY), q[2].fY); |
| 488 maxWidth = SkTMax(maxWidth, SkTMax(maxX - minX, maxY - minY)); |
| 489 intersect[index].intersectRay(q, rays[index]); |
| 490 const SkIntersections& i = intersect[index]; |
| 491 REPORTER_ASSERT(reporter, i.used() >= 1); |
| 492 bool foundZero = false; |
| 493 double smallT = 1; |
| 494 for (int idx2 = 0; idx2 < i.used(); ++idx2) { |
| 495 double t = i[0][idx2]; |
| 496 if (t == 0) { |
| 497 foundZero = true; |
| 498 continue; |
| 499 } |
| 500 if (smallT > t) { |
| 501 smallT = t; |
| 502 } |
| 503 } |
| 504 REPORTER_ASSERT(reporter, foundZero == true); |
| 505 if (smallT == 1) { |
| 506 continue; |
| 507 } |
| 508 SkDVector ray = q.ptAtT(smallT) - origin; |
| 509 SkDVector end = rays[index][1] - origin; |
| 510 if (ray.fX * end.fX < 0 || ray.fY * end.fY < 0) { |
| 511 continue; |
| 512 } |
| 513 double rayDist = ray.length(); |
| 514 double endDist = end.length(); |
| 515 double delta = fabs(rayDist - endDist) / maxWidth; |
| 516 if (delta > 1e-4) { |
| 517 useIntersect ^= true; |
| 518 } |
| 519 smallestTs[index] = smallT; |
| 520 } |
| 521 bool firstInside; |
| 522 if (useIntersect) { |
| 523 int sIndex = (int) (smallestTs[1] < 1); |
| 524 REPORTER_ASSERT(reporter, smallestTs[sIndex ^ 1] == 1); |
| 525 double t = smallestTs[sIndex]; |
| 526 const SkDQuad& q = *quads[sIndex]; |
| 527 SkDVector ray = q.ptAtT(t) - origin; |
| 528 SkDVector end = rays[sIndex][1] - origin; |
| 529 double rayDist = ray.length(); |
| 530 double endDist = end.length(); |
| 531 SkDVector mid = q.ptAtT(t / 2) - origin; |
| 532 double midXray = mid.crossCheck(ray); |
| 533 if (gPathOpsAngleIdeasVerbose) { |
| 534 SkDebugf("rayDist>endDist:%d sIndex==0:%d vDir[sIndex]<0:%d midXray<
0:%d\n", |
| 535 rayDist > endDist, sIndex == 0, vDir[sIndex] < 0, midXray <
0); |
| 536 } |
| 537 SkASSERT(SkScalarSignAsInt(SkDoubleToScalar(midXray)) |
| 538 == SkScalarSignAsInt(SkDoubleToScalar(vDir[sIndex]))); |
| 539 firstInside = (rayDist > endDist) ^ (sIndex == 0) ^ (vDir[sIndex] < 0); |
| 540 } else if (overlap >= 0) { |
| 541 return; // answer has already been determined |
| 542 } else { |
| 543 firstInside = checkParallel(reporter, quad1, quad2); |
| 544 } |
| 545 if (overlap < 0) { |
| 546 SkDEBUGCODE(int realEnds =) |
| 547 PathOpsAngleTester::EndsIntersect(seg[0].angle(0), seg[1].angle(
0)); |
| 548 SkASSERT(realEnds == (firstInside ? 1 : 0)); |
| 549 } |
| 550 bruteForce(reporter, quad1, quad2, firstInside); |
| 551 } |
| 552 |
| 553 DEF_TEST(PathOpsAngleOverlapHullsOne, reporter) { |
| 554 // gPathOpsAngleIdeasVerbose = true; |
| 555 const SkDQuad quads[] = { |
| 556 {{{939.4808349609375, 914.355224609375}, {-357.7921142578125, 590.842529296875},
{736.8936767578125, -350.717529296875}}}, |
| 557 {{{939.4808349609375, 914.355224609375}, {-182.85418701171875, 634.4552001953125
}, {-509.62615966796875, 576.1182861328125}}} |
| 558 }; |
| 559 for (int index = 0; index < (int) SK_ARRAY_COUNT(quads); index += 2) { |
| 560 testQuadAngles(reporter, quads[index], quads[index + 1], 0); |
| 561 } |
| 562 } |
| 563 |
| 564 DEF_TEST(PathOpsAngleOverlapHulls, reporter) { |
| 565 if (!gPathOpsAngleIdeasVerbose) { // takes a while to run -- so exclude it
by default |
| 566 return; |
| 567 } |
| 568 SkRandom ran; |
| 569 for (int index = 0; index < 100000; ++index) { |
| 570 if (index % 1000 == 999) SkDebugf("."); |
| 571 SkDPoint origin = {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-1000, 10
00)}; |
| 572 SkDQuad quad1 = {{origin, {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-
1000, 1000)}, |
| 573 {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-1000, 1000)}}}; |
| 574 if (quad1[0] == quad1[2]) { |
| 575 continue; |
| 576 } |
| 577 SkDQuad quad2 = {{origin, {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-
1000, 1000)}, |
| 578 {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-1000, 1000)}}}; |
| 579 if (quad2[0] == quad2[2]) { |
| 580 continue; |
| 581 } |
| 582 SkIntersections i; |
| 583 i.intersect(quad1, quad2); |
| 584 REPORTER_ASSERT(reporter, i.used() >= 1); |
| 585 if (i.used() > 1) { |
| 586 continue; |
| 587 } |
| 588 testQuadAngles(reporter, quad1, quad2, index); |
| 589 } |
| 590 } |
| 591 |
| 592 DEF_TEST(PathOpsAngleBruteT, reporter) { |
| 593 if (!gPathOpsAngleIdeasVerbose) { // takes a while to run -- so exclude it
by default |
| 594 return; |
| 595 } |
| 596 SkRandom ran; |
| 597 double smaller = SK_Scalar1; |
| 598 SkDQuad small[2]; |
| 599 SkDEBUGCODE(int smallIndex); |
| 600 for (int index = 0; index < 100000; ++index) { |
| 601 SkDPoint origin = {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-1000, 10
00)}; |
| 602 SkDQuad quad1 = {{origin, {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-
1000, 1000)}, |
| 603 {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-1000, 1000)}}}; |
| 604 if (quad1[0] == quad1[2]) { |
| 605 continue; |
| 606 } |
| 607 SkDQuad quad2 = {{origin, {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-
1000, 1000)}, |
| 608 {ran.nextRangeF(-1000, 1000), ran.nextRangeF(-1000, 1000)}}}; |
| 609 if (quad2[0] == quad2[2]) { |
| 610 continue; |
| 611 } |
| 612 SkIntersections i; |
| 613 i.intersect(quad1, quad2); |
| 614 REPORTER_ASSERT(reporter, i.used() >= 1); |
| 615 if (i.used() > 1) { |
| 616 continue; |
| 617 } |
| 618 TRange lowerRange, upperRange; |
| 619 bool result = bruteMinT(reporter, quad1, quad2, &lowerRange, &upperRange
); |
| 620 REPORTER_ASSERT(reporter, result); |
| 621 double min = SkTMin(upperRange.t1, upperRange.t2); |
| 622 if (smaller > min) { |
| 623 small[0] = quad1; |
| 624 small[1] = quad2; |
| 625 SkDEBUGCODE(smallIndex = index); |
| 626 smaller = min; |
| 627 } |
| 628 } |
| 629 #ifdef SK_DEBUG |
| 630 DumpQ(small[0], small[1], smallIndex); |
| 631 #endif |
| 632 } |
| 633 |
| 634 DEF_TEST(PathOpsAngleBruteTOne, reporter) { |
| 635 // gPathOpsAngleIdeasVerbose = true; |
| 636 const SkDQuad quads[] = { |
| 637 {{{-770.8492431640625, 948.2369384765625}, {-853.37066650390625, 972.03015136718
75}, {-200.62042236328125, -26.7174072265625}}}, |
| 638 {{{-770.8492431640625, 948.2369384765625}, {513.602783203125, 578.8681640625}, {
960.641357421875, -813.69757080078125}}}, |
| 639 {{{563.8267822265625, -107.4566650390625}, {-44.67724609375, -136.57452392578125
}, {492.3856201171875, -268.79644775390625}}}, |
| 640 {{{563.8267822265625, -107.4566650390625}, {708.049072265625, -100.7778930664062
5}, {-48.88226318359375, 967.9022216796875}}}, |
| 641 {{{598.857421875, 846.345458984375}, {-644.095703125, -316.12921142578125}, {-97
.64599609375, 20.6158447265625}}}, |
| 642 {{{598.857421875, 846.345458984375}, {715.7142333984375, 955.3599853515625}, {-9
19.9478759765625, 691.611328125}}}, |
| 643 }; |
| 644 TRange lowerRange, upperRange; |
| 645 bruteMinT(reporter, quads[0], quads[1], &lowerRange, &upperRange); |
| 646 bruteMinT(reporter, quads[2], quads[3], &lowerRange, &upperRange); |
| 647 bruteMinT(reporter, quads[4], quads[5], &lowerRange, &upperRange); |
| 648 } |
| 649 |
| 650 /* |
| 651 The sorting problem happens when the inital tangents are not a true indicator of
the curve direction |
| 652 Nearly always, the initial tangents do give the right answer, |
| 653 so the trick is to figure out when the initial tangent cannot be trusted. |
| 654 If the convex hulls of both curves are in the same half plane, and not overlappi
ng, sorting the |
| 655 hulls is enough. |
| 656 If the hulls overlap, and have the same general direction, then intersect the sh
orter end point ray |
| 657 with the opposing curve, and see on which side of the shorter curve the opposing
intersection lies. |
| 658 Otherwise, if the control vector is extremely short, likely the point on curve m
ust be computed |
| 659 If moving the control point slightly can change the sign of the cross product, e
ither answer could |
| 660 be "right". |
| 661 We need to determine how short is extremely short. Move the control point a set
percentage of |
| 662 the largest length to determine how stable the curve is vis-a-vis the initial ta
ngent. |
| 663 */ |
| 664 |
| 665 static const SkDQuad extremeTests[][2] = { |
| 666 { |
| 667 {{{-708.0077926931004,-154.61669472244046}, |
| 668 {-707.9234268635319,-154.30459999551294}, |
| 669 {505.58447265625,-504.9130859375}}}, |
| 670 {{{-708.0077926931004,-154.61669472244046}, |
| 671 {-711.127526325141,-163.9446090624656}, |
| 672 {-32.39227294921875,-906.3277587890625}}}, |
| 673 }, { |
| 674 {{{-708.0077926931004,-154.61669472244046}, |
| 675 {-708.2875337527566,-154.36676458635623}, |
| 676 {505.58447265625,-504.9130859375}}}, |
| 677 {{{-708.0077926931004,-154.61669472244046}, |
| 678 {-708.4111557216864,-154.5366642875255}, |
| 679 {-32.39227294921875,-906.3277587890625}}}, |
| 680 }, { |
| 681 {{{-609.0230951752058,-267.5435593490574}, |
| 682 {-594.1120809906336,-136.08492475411555}, |
| 683 {505.58447265625,-504.9130859375}}}, |
| 684 {{{-609.0230951752058,-267.5435593490574}, |
| 685 {-693.7467719138988,-341.3259237831895}, |
| 686 {-32.39227294921875,-906.3277587890625}}} |
| 687 }, { |
| 688 {{{-708.0077926931004,-154.61669472244046}, |
| 689 {-707.9994640658723,-154.58588461064852}, |
| 690 {505.58447265625,-504.9130859375}}}, |
| 691 {{{-708.0077926931004,-154.61669472244046}, |
| 692 {-708.0239418990758,-154.6403553507124}, |
| 693 {-32.39227294921875,-906.3277587890625}}} |
| 694 }, { |
| 695 {{{-708.0077926931004,-154.61669472244046}, |
| 696 {-707.9993222215099,-154.55999389855003}, |
| 697 {68.88981098017803,296.9273945411635}}}, |
| 698 {{{-708.0077926931004,-154.61669472244046}, |
| 699 {-708.0509091919608,-154.64675214697067}, |
| 700 {-777.4871194247767,-995.1470120113145}}} |
| 701 }, { |
| 702 {{{-708.0077926931004,-154.61669472244046}, |
| 703 {-708.0060491116379,-154.60889321524968}, |
| 704 {229.97088707895057,-430.0569357467175}}}, |
| 705 {{{-708.0077926931004,-154.61669472244046}, |
| 706 {-708.013911296257,-154.6219143988058}, |
| 707 {138.13162892614037,-573.3689311737394}}} |
| 708 }, { |
| 709 {{{-543.2570545751013,-237.29243831075053}, |
| 710 {-452.4119186056987,-143.47223056267802}, |
| 711 {229.97088707895057,-430.0569357467175}}}, |
| 712 {{{-543.2570545751013,-237.29243831075053}, |
| 713 {-660.5330371214436,-362.0016148388}, |
| 714 {138.13162892614037,-573.3689311737394}}}, |
| 715 }, |
| 716 }; |
| 717 |
| 718 static double endCtrlRatio(const SkDQuad quad) { |
| 719 SkDVector longEdge = quad[2] - quad[0]; |
| 720 double longLen = longEdge.length(); |
| 721 SkDVector shortEdge = quad[1] - quad[0]; |
| 722 double shortLen = shortEdge.length(); |
| 723 return longLen / shortLen; |
| 724 } |
| 725 |
| 726 static void computeMV(const SkDQuad& quad, const SkDVector& v, double m, SkDVect
or mV[2]) { |
| 727 SkDPoint mPta = {quad[1].fX - m * v.fY, quad[1].fY + m * v.fX}; |
| 728 SkDPoint mPtb = {quad[1].fX + m * v.fY, quad[1].fY - m * v.fX}; |
| 729 mV[0] = mPta - quad[0]; |
| 730 mV[1] = mPtb - quad[0]; |
| 731 } |
| 732 |
| 733 static double mDistance(skiatest::Reporter* reporter, bool agrees, const SkDQuad
& q1, |
| 734 const SkDQuad& q2) { |
| 735 if (1 && agrees) { |
| 736 return SK_ScalarMax; |
| 737 } |
| 738 // how close is the angle from inflecting in the opposite direction? |
| 739 SkDVector v1 = q1[1] - q1[0]; |
| 740 SkDVector v2 = q2[1] - q2[0]; |
| 741 double dir = v1.crossCheck(v2); |
| 742 REPORTER_ASSERT(reporter, dir != 0); |
| 743 // solve for opposite direction displacement scale factor == m |
| 744 // initial dir = v1.cross(v2) == v2.x * v1.y - v2.y * v1.x |
| 745 // displacement of q1[1] : dq1 = { -m * v1.y, m * v1.x } + q1[1] |
| 746 // straight angle when : v2.x * (dq1.y - q1[0].y) == v2.y * (dq1.x - q1[0].x
) |
| 747 // v2.x * (m * v1.x + v1.y) == v2.y * (-m * v1.y + v1.
x) |
| 748 // - m * (v2.x * v1.x + v2.y * v1.y) == v2.x * v1.y - v2.y * v1.x |
| 749 // m = (v2.y * v1.x - v2.x * v1.y) / (v2.x * v1.x + v2.y * v1.y) |
| 750 // m = v1.cross(v2) / v1.dot(v2) |
| 751 double div = v1.dot(v2); |
| 752 REPORTER_ASSERT(reporter, div != 0); |
| 753 double m = dir / div; |
| 754 SkDVector mV1[2], mV2[2]; |
| 755 computeMV(q1, v1, m, mV1); |
| 756 computeMV(q2, v2, m, mV2); |
| 757 double dist1 = v1.length() * m; |
| 758 double dist2 = v2.length() * m; |
| 759 if (gPathOpsAngleIdeasVerbose) { |
| 760 SkDebugf("%c r1=%1.9g r2=%1.9g m=%1.9g dist1=%1.9g dist2=%1.9g " |
| 761 " dir%c 1a=%1.9g 1b=%1.9g 2a=%1.9g 2b=%1.9g\n", agrees ? 'T' : '
F', |
| 762 endCtrlRatio(q1), endCtrlRatio(q2), m, dist1, dist2, dir > 0 ? '
+' : '-', |
| 763 mV1[0].crossCheck(v2), mV1[1].crossCheck(v2), |
| 764 mV2[0].crossCheck(v1), mV2[1].crossCheck(v1)); |
| 765 } |
| 766 if (1) { |
| 767 bool use1 = fabs(dist1) < fabs(dist2); |
| 768 if (gPathOpsAngleIdeasVerbose) { |
| 769 SkDebugf("%c dist=%1.9g r=%1.9g\n", agrees ? 'T' : 'F', use1 ? dist1
: dist2, |
| 770 use1 ? distEndRatio(dist1, q1) : distEndRatio(dist2, q2)); |
| 771 } |
| 772 return fabs(use1 ? distEndRatio(dist1, q1) : distEndRatio(dist2, q2)); |
| 773 } |
| 774 return SK_ScalarMax; |
| 775 } |
| 776 |
| 777 static void midPointAgrees(skiatest::Reporter* reporter, const SkDQuad& q1, cons
t SkDQuad& q2, |
| 778 bool ccw) { |
| 779 SkDPoint mid1 = q1.ptAtT(0.5); |
| 780 SkDVector m1 = mid1 - q1[0]; |
| 781 SkDPoint mid2 = q2.ptAtT(0.5); |
| 782 SkDVector m2 = mid2 - q2[0]; |
| 783 REPORTER_ASSERT(reporter, ccw ? m1.crossCheck(m2) < 0 : m1.crossCheck(m2) >
0); |
| 784 } |
| 785 |
| 786 DEF_TEST(PathOpsAngleExtreme, reporter) { |
| 787 if (!gPathOpsAngleIdeasVerbose) { // takes a while to run -- so exclude it
by default |
| 788 return; |
| 789 } |
| 790 double maxR = SK_ScalarMax; |
| 791 for (int index = 0; index < (int) SK_ARRAY_COUNT(extremeTests); ++index) { |
| 792 const SkDQuad& quad1 = extremeTests[index][0]; |
| 793 const SkDQuad& quad2 = extremeTests[index][1]; |
| 794 if (gPathOpsAngleIdeasVerbose) { |
| 795 SkDebugf("%s %d\n", __FUNCTION__, index); |
| 796 } |
| 797 REPORTER_ASSERT(reporter, quad1[0] == quad2[0]); |
| 798 SkIntersections i; |
| 799 i.intersect(quad1, quad2); |
| 800 REPORTER_ASSERT(reporter, i.used() == 1); |
| 801 REPORTER_ASSERT(reporter, i.pt(0) == quad1[0]); |
| 802 int overlap = quadHullsOverlap(reporter, quad1, quad2); |
| 803 REPORTER_ASSERT(reporter, overlap >= 0); |
| 804 SkDVector sweep[2], tweep[2]; |
| 805 setQuadHullSweep(quad1, sweep); |
| 806 setQuadHullSweep(quad2, tweep); |
| 807 double s0xt0 = sweep[0].crossCheck(tweep[0]); |
| 808 REPORTER_ASSERT(reporter, s0xt0 != 0); |
| 809 bool ccw = s0xt0 < 0; |
| 810 bool agrees = bruteForceCheck(reporter, quad1, quad2, ccw); |
| 811 maxR = SkTMin(maxR, mDistance(reporter, agrees, quad1, quad2)); |
| 812 if (agrees) { |
| 813 continue; |
| 814 } |
| 815 midPointAgrees(reporter, quad1, quad2, !ccw); |
| 816 SkDQuad q1 = quad1; |
| 817 SkDQuad q2 = quad2; |
| 818 double loFail = 1; |
| 819 double hiPass = 2; |
| 820 // double vectors until t passes |
| 821 do { |
| 822 q1[1].fX = quad1[0].fX * (1 - hiPass) + quad1[1].fX * hiPass; |
| 823 q1[1].fY = quad1[0].fY * (1 - hiPass) + quad1[1].fY * hiPass; |
| 824 q2[1].fX = quad2[0].fX * (1 - hiPass) + quad2[1].fX * hiPass; |
| 825 q2[1].fY = quad2[0].fY * (1 - hiPass) + quad2[1].fY * hiPass; |
| 826 agrees = bruteForceCheck(reporter, q1, q2, ccw); |
| 827 maxR = SkTMin(maxR, mDistance(reporter, agrees, q1, q2)); |
| 828 if (agrees) { |
| 829 break; |
| 830 } |
| 831 midPointAgrees(reporter, quad1, quad2, !ccw); |
| 832 loFail = hiPass; |
| 833 hiPass *= 2; |
| 834 } while (true); |
| 835 // binary search to find minimum pass |
| 836 double midTest = (loFail + hiPass) / 2; |
| 837 double step = (hiPass - loFail) / 4; |
| 838 while (step > FLT_EPSILON) { |
| 839 q1[1].fX = quad1[0].fX * (1 - midTest) + quad1[1].fX * midTest; |
| 840 q1[1].fY = quad1[0].fY * (1 - midTest) + quad1[1].fY * midTest; |
| 841 q2[1].fX = quad2[0].fX * (1 - midTest) + quad2[1].fX * midTest; |
| 842 q2[1].fY = quad2[0].fY * (1 - midTest) + quad2[1].fY * midTest; |
| 843 agrees = bruteForceCheck(reporter, q1, q2, ccw); |
| 844 maxR = SkTMin(maxR, mDistance(reporter, agrees, q1, q2)); |
| 845 if (!agrees) { |
| 846 midPointAgrees(reporter, quad1, quad2, !ccw); |
| 847 } |
| 848 midTest += agrees ? -step : step; |
| 849 step /= 2; |
| 850 } |
| 851 #ifdef SK_DEBUG |
| 852 // DumpQ(q1, q2, 999); |
| 853 #endif |
| 854 } |
| 855 if (gPathOpsAngleIdeasVerbose) { |
| 856 SkDebugf("maxR=%1.9g\n", maxR); |
| 857 } |
| 858 } |
OLD | NEW |