skia2/tests/PathMeasureTest.cpp
Mike Reed c924a88e33 SkPath::shrinkToFit() needs to perform copy-on-write
When we shrink a path, we might relocate its underlying arrays.
Doing so would invalidate any outstanding Iterators. The caller must
handle this for its path object, but there may be copies elsewhere,
which have just ref'd the underlying arrays. To keep these copys'
iterators alive, we defensively "copy-on-write", so as to not relocate
their buffers.

Incidentally, update SkContourMeasureIter's constructor to clarify that
it is iterating through its copy of the path, and not the original.

Change-Id: I5c9331ab36ac8e156218532478f6d7105fd97cdc
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/326438
Reviewed-by: Florin Malita <fmalita@chromium.org>
Commit-Queue: Mike Reed <reed@google.com>
2020-10-14 20:24:11 +00:00

329 lines
11 KiB
C++

/*
* Copyright 2011 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkPathMeasure.h"
#include "tests/Test.h"
static void test_small_segment3() {
SkPath path;
const SkPoint pts[] = {
{ 0, 0 },
{ 100000000000.0f, 100000000000.0f }, { 0, 0 }, { 10, 10 },
{ 10, 10 }, { 0, 0 }, { 10, 10 }
};
path.moveTo(pts[0]);
for (size_t i = 1; i < SK_ARRAY_COUNT(pts); i += 3) {
path.cubicTo(pts[i], pts[i + 1], pts[i + 2]);
}
SkPathMeasure meas(path, false);
meas.getLength();
}
static void test_small_segment2() {
SkPath path;
const SkPoint pts[] = {
{ 0, 0 },
{ 100000000000.0f, 100000000000.0f }, { 0, 0 },
{ 10, 10 }, { 0, 0 },
};
path.moveTo(pts[0]);
for (size_t i = 1; i < SK_ARRAY_COUNT(pts); i += 2) {
path.quadTo(pts[i], pts[i + 1]);
}
SkPathMeasure meas(path, false);
meas.getLength();
}
static void test_small_segment() {
SkPath path;
const SkPoint pts[] = {
{ 100000, 100000},
// big jump between these points, makes a big segment
{ 1.0005f, 0.9999f },
// tiny (non-zero) jump between these points
{ SK_Scalar1, SK_Scalar1 },
};
path.moveTo(pts[0]);
for (size_t i = 1; i < SK_ARRAY_COUNT(pts); ++i) {
path.lineTo(pts[i]);
}
SkPathMeasure meas(path, false);
/* this would assert (before a fix) because we added a segment with
the same length as the prev segment, due to the follow (bad) pattern
d = distance(pts[0], pts[1]);
distance += d;
seg->fDistance = distance;
SkASSERT(d > 0); // TRUE
SkASSERT(seg->fDistance > prevSeg->fDistance); // FALSE
This 2nd assert failes because (distance += d) didn't affect distance
because distance >>> d.
*/
meas.getLength();
}
DEF_TEST(PathMeasure, reporter) {
SkPath path;
path.moveTo(0, 0);
path.lineTo(SK_Scalar1, 0);
path.lineTo(SK_Scalar1, SK_Scalar1);
path.lineTo(0, SK_Scalar1);
SkPathMeasure meas(path, true);
SkScalar length = meas.getLength();
SkASSERT(length == SK_Scalar1*4);
path.reset();
path.moveTo(0, 0);
path.lineTo(SK_Scalar1*3, SK_Scalar1*4);
meas.setPath(&path, false);
length = meas.getLength();
REPORTER_ASSERT(reporter, length == SK_Scalar1*5);
path.reset();
path.addCircle(0, 0, SK_Scalar1);
meas.setPath(&path, true);
length = meas.getLength();
// SkDebugf("circle arc-length = %g\n", length);
// Test the behavior following a close not followed by a move.
path.reset();
path.lineTo(SK_Scalar1, 0);
path.lineTo(SK_Scalar1, SK_Scalar1);
path.lineTo(0, SK_Scalar1);
path.close();
path.lineTo(-SK_Scalar1, 0);
meas.setPath(&path, false);
length = meas.getLength();
REPORTER_ASSERT(reporter, length == SK_Scalar1 * 4);
meas.nextContour();
length = meas.getLength();
REPORTER_ASSERT(reporter, length == SK_Scalar1);
SkPoint position;
SkVector tangent;
REPORTER_ASSERT(reporter, meas.getPosTan(SK_ScalarHalf, &position, &tangent));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fX,
-SK_ScalarHalf,
0.0001f));
REPORTER_ASSERT(reporter, position.fY == 0);
REPORTER_ASSERT(reporter, tangent.fX == -SK_Scalar1);
REPORTER_ASSERT(reporter, tangent.fY == 0);
// Test degenerate paths
path.reset();
path.moveTo(0, 0);
path.lineTo(0, 0);
path.lineTo(SK_Scalar1, 0);
path.quadTo(SK_Scalar1, 0, SK_Scalar1, 0);
path.quadTo(SK_Scalar1, SK_Scalar1, SK_Scalar1, SK_Scalar1 * 2);
path.cubicTo(SK_Scalar1, SK_Scalar1 * 2,
SK_Scalar1, SK_Scalar1 * 2,
SK_Scalar1, SK_Scalar1 * 2);
path.cubicTo(SK_Scalar1*2, SK_Scalar1 * 2,
SK_Scalar1*3, SK_Scalar1 * 2,
SK_Scalar1*4, SK_Scalar1 * 2);
meas.setPath(&path, false);
length = meas.getLength();
REPORTER_ASSERT(reporter, length == SK_Scalar1 * 6);
REPORTER_ASSERT(reporter, meas.getPosTan(SK_ScalarHalf, &position, &tangent));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fX,
SK_ScalarHalf,
0.0001f));
REPORTER_ASSERT(reporter, position.fY == 0);
REPORTER_ASSERT(reporter, tangent.fX == SK_Scalar1);
REPORTER_ASSERT(reporter, tangent.fY == 0);
REPORTER_ASSERT(reporter, meas.getPosTan(2.5f, &position, &tangent));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fX, SK_Scalar1, 0.0001f));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fY, 1.5f));
REPORTER_ASSERT(reporter, tangent.fX == 0);
REPORTER_ASSERT(reporter, tangent.fY == SK_Scalar1);
REPORTER_ASSERT(reporter, meas.getPosTan(4.5f, &position, &tangent));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fX,
2.5f,
0.0001f));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fY,
2.0f,
0.0001f));
REPORTER_ASSERT(reporter, tangent.fX == SK_Scalar1);
REPORTER_ASSERT(reporter, tangent.fY == 0);
path.reset();
path.moveTo(0, 0);
path.lineTo(SK_Scalar1, 0);
path.moveTo(SK_Scalar1, SK_Scalar1);
path.moveTo(SK_Scalar1 * 2, SK_Scalar1 * 2);
path.lineTo(SK_Scalar1, SK_Scalar1 * 2);
meas.setPath(&path, false);
length = meas.getLength();
REPORTER_ASSERT(reporter, length == SK_Scalar1);
REPORTER_ASSERT(reporter, meas.getPosTan(SK_ScalarHalf, &position, &tangent));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fX,
SK_ScalarHalf,
0.0001f));
REPORTER_ASSERT(reporter, position.fY == 0);
REPORTER_ASSERT(reporter, tangent.fX == SK_Scalar1);
REPORTER_ASSERT(reporter, tangent.fY == 0);
meas.nextContour();
length = meas.getLength();
REPORTER_ASSERT(reporter, length == SK_Scalar1);
REPORTER_ASSERT(reporter, meas.getPosTan(SK_ScalarHalf, &position, &tangent));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fX,
1.5f,
0.0001f));
REPORTER_ASSERT(reporter,
SkScalarNearlyEqual(position.fY,
2.0f,
0.0001f));
REPORTER_ASSERT(reporter, tangent.fX == -SK_Scalar1);
REPORTER_ASSERT(reporter, tangent.fY == 0);
test_small_segment();
test_small_segment2();
test_small_segment3();
}
DEF_TEST(PathMeasureConic, reporter) {
SkPoint stdP, hiP, pts[] = {{0,0}, {100,0}, {100,0}};
SkPath p;
p.moveTo(0, 0);
p.conicTo(pts[1], pts[2], 1);
SkPathMeasure stdm(p, false);
REPORTER_ASSERT(reporter, stdm.getPosTan(20, &stdP, nullptr));
p.reset();
p.moveTo(0, 0);
p.conicTo(pts[1], pts[2], 10);
stdm.setPath(&p, false);
REPORTER_ASSERT(reporter, stdm.getPosTan(20, &hiP, nullptr));
REPORTER_ASSERT(reporter, 19.5f < stdP.fX && stdP.fX < 20.5f);
REPORTER_ASSERT(reporter, 19.5f < hiP.fX && hiP.fX < 20.5f);
}
// Regression test for b/26425223
DEF_TEST(PathMeasure_nextctr, reporter) {
SkPath path;
path.moveTo(0, 0); path.lineTo(100, 0);
SkPathMeasure meas(path, false);
// only expect 1 contour, even if we didn't explicitly call getLength() ourselves
REPORTER_ASSERT(reporter, !meas.nextContour());
}
#include "include/core/SkContourMeasure.h"
static void test_90_degrees(sk_sp<SkContourMeasure> cm, SkScalar radius,
skiatest::Reporter* reporter) {
SkPoint pos;
SkVector tan;
SkScalar distance = cm->length() / 4;
bool success = cm->getPosTan(distance, &pos, &tan);
REPORTER_ASSERT(reporter, success);
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(pos.fX, 0));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(pos.fY, radius));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(tan.fX, -1));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(tan.fY, 0));
}
static void test_empty_contours(skiatest::Reporter* reporter) {
SkPath path;
path.moveTo(0, 0).lineTo(100, 100).lineTo(200, 100);
path.moveTo(2, 2).moveTo(3, 3); // zero-length(s)
path.moveTo(4, 4).close().close().close(); // zero-length
path.moveTo(5, 5).lineTo(5, 5); // zero-length
path.moveTo(5, 5).lineTo(5, 5).close(); // zero-length
path.moveTo(5, 5).lineTo(5, 5).close().close(); // zero-length
path.moveTo(6, 6).lineTo(7, 7);
path.moveTo(10, 10); // zero-length
SkContourMeasureIter fact(path, false);
// given the above construction, we expect only 2 contours (the rest are "empty")
REPORTER_ASSERT(reporter, fact.next());
REPORTER_ASSERT(reporter, fact.next());
REPORTER_ASSERT(reporter, !fact.next());
}
static void test_MLM_contours(skiatest::Reporter* reporter) {
SkPath path;
// This odd sequence (with a trailing moveTo) used to return a 2nd contour, which is
// wrong, since the contract for a measure is to only return non-zero length contours.
path.moveTo(10, 10).lineTo(20, 20).moveTo(30, 30);
for (bool forceClosed : {false, true}) {
SkContourMeasureIter fact(path, forceClosed);
REPORTER_ASSERT(reporter, fact.next());
REPORTER_ASSERT(reporter, !fact.next());
}
}
static void test_shrink(skiatest::Reporter* reporter) {
SkPath path;
path.addRect({1, 2, 3, 4});
path.incReserve(100); // give shrinkToFit() something to do
SkContourMeasureIter iter(path, false);
// shrinks the allocation, possibly relocating the underlying arrays.
// The contouremasureiter needs to have safely copied path, to be unaffected by this
// change to "path".
path.shrinkToFit();
// Note, this failed (before the fix) on an ASAN build, which notices that we were
// using an internal iterator of the passed-in path, not our copy.
while (iter.next())
;
}
DEF_TEST(contour_measure, reporter) {
SkPath path;
path.addCircle(0, 0, 100);
path.addCircle(0, 0, 10);
SkContourMeasureIter fact(path, false);
path.reset(); // we should not need the path avert we created the factory
auto cm0 = fact.next();
auto cm1 = fact.next();
REPORTER_ASSERT(reporter, cm0->isClosed());
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(cm0->length(), 200 * SK_ScalarPI, 1.5f));
test_90_degrees(cm0, 100, reporter);
REPORTER_ASSERT(reporter, cm1->isClosed());
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(cm1->length(), 20 * SK_ScalarPI, 0.5f));
test_90_degrees(cm1, 10, reporter);
auto cm2 = fact.next();
REPORTER_ASSERT(reporter, !cm2);
test_empty_contours(reporter);
test_MLM_contours(reporter);
test_shrink(reporter);
}