skia2/tests/PathOpsConicIntersectionTest.cpp
caryclark ef784fb7f5 More conic-specific tests revealed a few conic-specific bugs. Because javascript / canvas make visualizing conics tricky, new native tools are required.
The utility SubsetPath removes parts of a potentially very large path to isolate a minimal test case. SubsetPath is very useful for debugging path ops, but is not path ops specific.

PathOpsBuilderConicTest compares the output of the Path Ops Builder, sequential calls to Simplify, and SkRegions for some number of rotated ovals.

Some tests caused path ops to hang. It was caught adding a loop of curves because the head was not found by the tail. Even though the root cause has been fixed, SkSegment::addCurveTo callers now abort the path op if the same curve was added twice.

The subdivided conic weight was been computed anew. Fortunately, it's a simpler computation that the one it replaces.

Some Simplify() subroutines returned false to signal that the results needed assembling. Change these to abort the current operation instead.

Coincident curve intersection triggered two small bugs; one where no perpendicular could be found for coincident curves, and one where no coincident curves remain after looping.

The SixtyOvals test can be run through multiple processes instead of multiple threads. This strategy allows a 48 core machine to saturate all cores at 100%.

The DEBUG_VISUALIZE_CONICS code in PathOpsConicIntersectionTest acknowleges that it is easier to visualize conics with Skia than with script and html canvas. This test also verifies that path ops subdivision matches geometry chopping.

TBR=reed@google.com

Review URL: https://codereview.chromium.org/1405383004
2015-10-30 12:03:06 -07:00

351 lines
14 KiB
C++

/*
* Copyright 2015 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "PathOpsTestCommon.h"
#include "SkGeometry.h"
#include "SkIntersections.h"
#include "Test.h"
/*
manually compute the intersection of a pair of circles and see if the conic intersection matches
given two circles
construct a line connecting their centers
*/
static const SkDConic testSet[] = {
{{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
{{{{5.1114602088928223, 628.77813720703125},
{10.834027290344238, 988.964111328125},
{163.40835571289062, 988.964111328125}}}, 0.72944212f},
{{{{163.40835571289062, 988.964111328125},
{5, 988.964111328125},
{5, 614.7423095703125}}}, 0.707106769f},
{{{{11.17222976684570312, -8.103978157043457031},
{22.91432571411132812, -10.37866020202636719},
{23.7764129638671875, -7.725424289703369141}}}, 1.00862849f},
{{{{-1.545085430145263672, -4.755282402038574219},
{22.23132705688476562, -12.48070907592773438},
{23.7764129638671875, -7.725427150726318359}}}, 0.707106769f},
{{{{-4,1}, {-4,5}, {0,5}}}, 0.707106769f},
{{{{-3,4}, {-3,1}, {0,1}}}, 0.707106769f},
{{{{0, 0}, {0, 1}, {1, 1}}}, 0.5f},
{{{{1, 0}, {0, 0}, {0, 1}}}, 0.5f},
};
const int testSetCount = (int) SK_ARRAY_COUNT(testSet);
static void chopCompare(const SkConic chopped[2], const SkDConic dChopped[2]) {
SkASSERT(roughly_equal(chopped[0].fW, dChopped[0].fWeight));
SkASSERT(roughly_equal(chopped[1].fW, dChopped[1].fWeight));
for (int cIndex = 0; cIndex < 2; ++cIndex) {
for (int pIndex = 0; pIndex < 3; ++pIndex) {
SkDPoint up;
up.set(chopped[cIndex].fPts[pIndex]);
SkASSERT(dChopped[cIndex].fPts[pIndex].approximatelyEqual(up));
}
}
#if DEBUG_VISUALIZE_CONICS
dChopped[0].dump();
dChopped[1].dump();
#endif
}
#include "SkBitmap.h"
#include "SkCanvas.h"
#include "SkImageEncoder.h"
#include "SkPathOpsRect.h"
#include "SkPaint.h"
#include "SkString.h"
#define DEBUG_VISUALIZE_CONICS 0
#if DEBUG_VISUALIZE_CONICS
static void writePng(const SkConic& c, const SkConic ch[2], const char* name) {
const int scale = 10;
SkConic conic, chopped[2];
for (int index = 0; index < 3; ++index) {
conic.fPts[index].fX = c.fPts[index].fX * scale;
conic.fPts[index].fY = c.fPts[index].fY * scale;
for (int chIndex = 0; chIndex < 2; ++chIndex) {
chopped[chIndex].fPts[index].fX = ch[chIndex].fPts[index].fX * scale;
chopped[chIndex].fPts[index].fY = ch[chIndex].fPts[index].fY * scale;
}
}
conic.fW = c.fW;
chopped[0].fW = ch[0].fW;
chopped[1].fW = ch[1].fW;
SkBitmap bitmap;
SkRect bounds;
conic.computeTightBounds(&bounds);
bounds.outset(10, 10);
bitmap.tryAllocPixels(SkImageInfo::MakeN32Premul(
SkScalarRoundToInt(bounds.width()), SkScalarRoundToInt(bounds.height())));
SkCanvas canvas(bitmap);
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style);
canvas.translate(-bounds.fLeft, -bounds.fTop);
canvas.drawColor(SK_ColorWHITE);
SkPath path;
path.moveTo(conic.fPts[0]);
path.conicTo(conic.fPts[1], conic.fPts[2], conic.fW);
paint.setARGB(0x80, 0xFF, 0, 0);
canvas.drawPath(path, paint);
path.reset();
path.moveTo(chopped[0].fPts[0]);
path.conicTo(chopped[0].fPts[1], chopped[0].fPts[2], chopped[0].fW);
path.moveTo(chopped[1].fPts[0]);
path.conicTo(chopped[1].fPts[1], chopped[1].fPts[2], chopped[1].fW);
paint.setARGB(0x80, 0, 0, 0xFF);
canvas.drawPath(path, paint);
SkString filename("c:\\Users\\caryclark\\Documents\\");
filename.appendf("%s.png", name);
SkImageEncoder::EncodeFile(filename.c_str(), bitmap,
SkImageEncoder::kPNG_Type, 100);
}
static void writeDPng(const SkDConic& dC, const char* name) {
const int scale = 5;
SkDConic dConic = {{{ {dC.fPts[0].fX * scale, dC.fPts[0].fY * scale },
{dC.fPts[1].fX * scale, dC.fPts[1].fY * scale },
{dC.fPts[2].fX * scale, dC.fPts[2].fY * scale }}}, dC.fWeight };
SkBitmap bitmap;
SkDRect bounds;
bounds.setBounds(dConic);
bounds.fLeft -= 10;
bounds.fTop -= 10;
bounds.fRight += 10;
bounds.fBottom += 10;
bitmap.tryAllocPixels(SkImageInfo::MakeN32Premul(
SkScalarRoundToInt(SkDoubleToScalar(bounds.width())),
SkScalarRoundToInt(SkDoubleToScalar(bounds.height()))));
SkCanvas canvas(bitmap);
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style);
canvas.translate(SkDoubleToScalar(-bounds.fLeft), SkDoubleToScalar(-bounds.fTop));
canvas.drawColor(SK_ColorWHITE);
SkPath path;
path.moveTo(dConic.fPts[0].asSkPoint());
path.conicTo(dConic.fPts[1].asSkPoint(), dConic.fPts[2].asSkPoint(), dConic.fWeight);
paint.setARGB(0x80, 0xFF, 0, 0);
canvas.drawPath(path, paint);
path.reset();
const int chops = 2;
for (int tIndex = 0; tIndex < chops; ++tIndex) {
SkDConic chopped = dConic.subDivide(tIndex / (double) chops,
(tIndex + 1) / (double) chops);
path.moveTo(chopped.fPts[0].asSkPoint());
path.conicTo(chopped.fPts[1].asSkPoint(), chopped.fPts[2].asSkPoint(), chopped.fWeight);
}
paint.setARGB(0x80, 0, 0, 0xFF);
canvas.drawPath(path, paint);
SkString filename("c:\\Users\\caryclark\\Documents\\");
filename.appendf("%s.png", name);
SkImageEncoder::EncodeFile(filename.c_str(), bitmap,
SkImageEncoder::kPNG_Type, 100);
}
#endif
static void chopBothWays(const SkDConic& dConic, double t, const char* name) {
SkConic conic;
for (int index = 0; index < 3; ++index) {
conic.fPts[index] = dConic.fPts[index].asSkPoint();
}
conic.fW = dConic.fWeight;
SkConic chopped[2];
SkDConic dChopped[2];
conic.chopAt(SkDoubleToScalar(t), chopped);
dChopped[0] = dConic.subDivide(0, t);
dChopped[1] = dConic.subDivide(t, 1);
#if DEBUG_VISUALIZE_CONICS
dConic.dump();
#endif
chopCompare(chopped, dChopped);
#if DEBUG_VISUALIZE_CONICS
writePng(conic, chopped, name);
#endif
}
#if DEBUG_VISUALIZE_CONICS
const SkDConic frame0[] = {
{{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
};
const SkDConic frame1[] = {
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
{{{{306.58801299999999, -227.983994}, {212.46499600000001, -262.24200400000001}, {95.551200899999998, 58.976398500000002}}}, 0.707107008f},
{{{{377.21899400000001, -141.98100299999999}, {237.77799285476553, -166.56830755921084}, {134.08399674208422, -155.06258330544892}}}, 0.788580656f},
{{{{134.08399674208422, -155.06258330544892}, {30.390000629402859, -143.55685905168704}, {23.185499199999999, -102.697998}}}, 0.923879623f},
};
const SkDConic frame2[] = {
{{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
{{{{205.78973252799028, -158.12538713371103}, {143.97848953841861, -74.076645245042371}, {95.551200899999998, 58.976398500000002}}}, 0.923879623f},
{{{{377.21899400000001, -141.98100299999999}, {237.77799285476553, -166.56830755921084}, {134.08399674208422, -155.06258330544892}}}, 0.788580656f},
};
const SkDConic frame3[] = {
{{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
{{{{205.78973252799028, -158.12538713371103}, {143.97848953841861, -74.076645245042371}, {95.551200899999998, 58.976398500000002}}}, 0.923879623f},
{{{{252.08225670812539, -156.90491625851064}, {185.93099479842493, -160.81544543232982}, {134.08399674208422, -155.06258330544892}}}, 0.835816324f},
};
const SkDConic frame4[] = {
{{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
{{{{205.78973252799028, -158.12538713371103}, {174.88411103320448, -116.10101618937664}, {145.19509369736275, -56.857102571363754}}}, 0.871667147f},
{{{{252.08225670812539, -156.90491625851064}, {185.93099479842493, -160.81544543232982}, {134.08399674208422, -155.06258330544892}}}, 0.835816324f},
};
const SkDConic frame5[] = {
{{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
{{{{205.78973252799028, -158.12538713371103}, {174.88411103320448, -116.10101618937664}, {145.19509369736275, -56.857102571363754}}}, 0.871667147f},
{{{{252.08225670812539, -156.90491625851064}, {219.70109133058406, -158.81912754088933}, {190.17095392508796, -158.38373974664466}}}, 0.858306944f},
};
const SkDConic frame6[] = {
{{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
{{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
{{{{205.78973252799028, -158.12538713371103}, {190.33692178059735, -137.11320166154385}, {174.87004877564593, -111.2132534799228}}}, 0.858117759f},
{{{{252.08225670812539, -156.90491625851064}, {219.70109133058406, -158.81912754088933}, {190.17095392508796, -158.38373974664466}}}, 0.858306944f},
};
const SkDConic* frames[] = {
frame0, frame1, frame2, frame3, frame4, frame5, frame6
};
const int frameSizes[] = { (int) SK_ARRAY_COUNT(frame0), (int) SK_ARRAY_COUNT(frame1),
(int) SK_ARRAY_COUNT(frame2), (int) SK_ARRAY_COUNT(frame3),
(int) SK_ARRAY_COUNT(frame4), (int) SK_ARRAY_COUNT(frame5),
(int) SK_ARRAY_COUNT(frame6),
};
static void writeFrames() {
const int scale = 5;
for (int index = 0; index < (int) SK_ARRAY_COUNT(frameSizes); ++index) {
SkDRect bounds;
bool boundsSet = false;
int frameSize = frameSizes[index];
for (int fIndex = 0; fIndex < frameSize; ++fIndex) {
const SkDConic& dC = frames[index][fIndex];
SkDConic dConic = {{{ {dC.fPts[0].fX * scale, dC.fPts[0].fY * scale },
{dC.fPts[1].fX * scale, dC.fPts[1].fY * scale },
{dC.fPts[2].fX * scale, dC.fPts[2].fY * scale }}}, dC.fWeight };
SkDRect dBounds;
dBounds.setBounds(dConic);
if (!boundsSet) {
bounds = dBounds;
boundsSet = true;
} else {
bounds.add((SkDPoint&) dBounds.fLeft);
bounds.add((SkDPoint&) dBounds.fRight);
}
}
bounds.fLeft -= 10;
bounds.fTop -= 10;
bounds.fRight += 10;
bounds.fBottom += 10;
SkBitmap bitmap;
bitmap.tryAllocPixels(SkImageInfo::MakeN32Premul(
SkScalarRoundToInt(SkDoubleToScalar(bounds.width())),
SkScalarRoundToInt(SkDoubleToScalar(bounds.height()))));
SkCanvas canvas(bitmap);
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style);
canvas.translate(SkDoubleToScalar(-bounds.fLeft), SkDoubleToScalar(-bounds.fTop));
canvas.drawColor(SK_ColorWHITE);
for (int fIndex = 0; fIndex < frameSize; ++fIndex) {
const SkDConic& dC = frames[index][fIndex];
SkDConic dConic = {{{ {dC.fPts[0].fX * scale, dC.fPts[0].fY * scale },
{dC.fPts[1].fX * scale, dC.fPts[1].fY * scale },
{dC.fPts[2].fX * scale, dC.fPts[2].fY * scale }}}, dC.fWeight };
SkPath path;
path.moveTo(dConic.fPts[0].asSkPoint());
path.conicTo(dConic.fPts[1].asSkPoint(), dConic.fPts[2].asSkPoint(), dConic.fWeight);
if (fIndex < 2) {
paint.setARGB(0x80, 0xFF, 0, 0);
} else {
paint.setARGB(0x80, 0, 0, 0xFF);
}
canvas.drawPath(path, paint);
}
SkString filename("c:\\Users\\caryclark\\Documents\\");
filename.appendf("f%d.png", index);
SkImageEncoder::EncodeFile(filename.c_str(), bitmap, SkImageEncoder::kPNG_Type, 100);
}
}
#endif
static void oneOff(skiatest::Reporter* reporter, const SkDConic& c1, const SkDConic& c2,
bool coin) {
#if DEBUG_VISUALIZE_CONICS
writeFrames();
#endif
chopBothWays(c1, 0.5, "c1");
chopBothWays(c2, 0.5, "c2");
#if DEBUG_VISUALIZE_CONICS
writeDPng(c1, "d1");
writeDPng(c2, "d2");
#endif
SkASSERT(ValidConic(c1));
SkASSERT(ValidConic(c2));
SkIntersections intersections;
intersections.intersect(c1, c2);
if (coin && intersections.used() != 2) {
SkDebugf("");
}
REPORTER_ASSERT(reporter, !coin || intersections.used() == 2);
double tt1, tt2;
SkDPoint xy1, xy2;
for (int pt3 = 0; pt3 < intersections.used(); ++pt3) {
tt1 = intersections[0][pt3];
xy1 = c1.ptAtT(tt1);
tt2 = intersections[1][pt3];
xy2 = c2.ptAtT(tt2);
const SkDPoint& iPt = intersections.pt(pt3);
REPORTER_ASSERT(reporter, xy1.approximatelyEqual(iPt));
REPORTER_ASSERT(reporter, xy2.approximatelyEqual(iPt));
REPORTER_ASSERT(reporter, xy1.approximatelyEqual(xy2));
}
reporter->bumpTestCount();
}
static void oneOff(skiatest::Reporter* reporter, int outer, int inner) {
const SkDConic& c1 = testSet[outer];
const SkDConic& c2 = testSet[inner];
oneOff(reporter, c1, c2, false);
}
static void oneOffTests(skiatest::Reporter* reporter) {
for (int outer = 0; outer < testSetCount - 1; ++outer) {
for (int inner = outer + 1; inner < testSetCount; ++inner) {
oneOff(reporter, outer, inner);
}
}
}
DEF_TEST(PathOpsConicIntersectionOneOff, reporter) {
oneOff(reporter, 0, 1);
}
DEF_TEST(PathOpsConicIntersection, reporter) {
oneOffTests(reporter);
}