diff options
author | Cary Clark <caryclark@skia.org> | 2018-08-16 11:53:54 -0400 |
---|---|---|
committer | Skia Commit-Bot <skia-commit-bot@chromium.org> | 2018-08-17 14:01:51 +0000 |
commit | 7d06e2642b5592c926c2e508aa6c3cd64c9009cd (patch) | |
tree | 84761874df9e14436ff9d2384b46470dc5c5b545 /src | |
parent | 773151fec905581c968f222f9b4a45aebbfb772d (diff) | |
download | skqp-7d06e2642b5592c926c2e508aa6c3cd64c9009cd.tar.gz |
fixup winding contours
Add AsWinding to convert SkPath with even odd fill to winding fill.
This basic implementation works for simple non-intersecting paths.
It may fail if contours in paths touch, specifically when the leftmost
point in a contour is shared with another contour.
The incomplete parts are marked with TODO in the code.
If this interface and implementation look promising, I will continue to
tackle the more difficult cases.
R=reed@google.com,bungeman@google.com
Bug: skia:7682
Change-Id: I479fba60072eb1391b451fcb819504245da2e2a9
Reviewed-on: https://skia-review.googlesource.com/147044
Commit-Queue: Cary Clark <caryclark@google.com>
Reviewed-by: Mike Reed <reed@google.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/pathops/SkPathOpsAsWinding.cpp | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/src/pathops/SkPathOpsAsWinding.cpp b/src/pathops/SkPathOpsAsWinding.cpp new file mode 100644 index 0000000000..7f173eed6d --- /dev/null +++ b/src/pathops/SkPathOpsAsWinding.cpp @@ -0,0 +1,421 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#include "SkOpEdgeBuilder.h" +#include "SkPathOpsCommon.h" +#include "SkRect.h" +#include <algorithm> +#include <vector> + +using std::vector; + +struct Contour { + enum class Direction { // SkPath::Direction doesn't have 'none' state + kCCW = -1, + kNone, + kCW, + }; + + Contour(const SkRect& bounds, int lastStart, int verbStart) + : fBounds(bounds) + , fVerbStart(lastStart) + , fVerbEnd(verbStart) { + } + + vector<Contour*> fChildren; + const SkRect fBounds; + SkPoint fMinXY{SK_ScalarMax, SK_ScalarMax}; + const int fVerbStart; + const int fVerbEnd; + Direction fDirection{Direction::kNone}; + bool fContained{false}; + bool fReverse{false}; +}; + +static const int kPtCount[] = { 1, 1, 2, 2, 3, 0 }; +static const int kPtIndex[] = { 0, 1, 1, 1, 1, 0 }; + +static Contour::Direction to_direction(SkScalar dy) { + return dy > 0 ? Contour::Direction::kCCW : dy < 0 ? Contour::Direction::kCW : + Contour::Direction::kNone; +} + +static int contains_edge(SkPoint pts[4], SkPath::Verb verb, SkScalar weight, const SkPoint& edge) { + SkRect bounds; + bounds.set(pts, kPtCount[verb] + 1); + if (bounds.fTop > edge.fY) { + return 0; + } + if (bounds.fBottom <= edge.fY) { // check to see if y is at line end to avoid double counting + return 0; + } + if (bounds.fLeft >= edge.fX) { + return 0; + } + int winding = 0; + double tVals[3]; + Contour::Direction directions[3]; + // must intersect horz ray with curve in case it intersects more than once + int count = (*CurveIntercept[verb * 2])(pts, weight, edge.fY, tVals); + SkASSERT(between(0, count, 3)); + // remove results to the right of edge + for (int index = 0; index < count; ) { + SkScalar intersectX = (*CurvePointAtT[verb])(pts, weight, tVals[index]).fX; + if (intersectX < edge.fX) { + ++index; + continue; + } + if (intersectX > edge.fX) { + tVals[index] = tVals[--count]; + continue; + } + // if intersect x equals edge x, we need to determine if pts is to the left or right of edge + if (pts[0].fX < edge.fX && pts[kPtCount[verb]].fX < edge.fX) { + ++index; + continue; + } + // TODO : other cases need discriminating. need op angle code to figure it out + // example: edge ends 45 degree diagonal going up. If pts is to the left of edge, keep. + // if pts is to the right of edge, discard. With code as is, can't distiguish the two cases. + if (intersectX < edge.fX) { + tVals[index] = tVals[--count]; + continue; + } + } + // use first derivative to determine if intersection is contributing +1 or -1 to winding + for (int index = 0; index < count; ++index) { + directions[index] = to_direction((*CurveSlopeAtT[verb])(pts, weight, tVals[index]).fY); + } + for (int index = 0; index < count; ++index) { + // skip intersections that end at edge and go up + if (zero_or_one(tVals[index]) && Contour::Direction::kCCW != directions[index]) { + continue; + } + winding += (int) directions[index]; + } + return winding; // note winding indicates containership, not contour direction +} + +static SkScalar conic_weight(const SkPath::Iter& iter, SkPath::Verb verb) { + return SkPath::kConic_Verb == verb ? iter.conicWeight() : 1; +} + +static SkPoint left_edge(SkPoint pts[4], SkPath::Verb verb, SkScalar weight, + Contour::Direction* direction) { + SkASSERT(SkPath::kLine_Verb <= verb && verb <= SkPath::kCubic_Verb); + SkPoint result; + double dy; + double t SK_INIT_TO_AVOID_WARNING; + int roots = 0; + if (SkPath::kLine_Verb == verb) { + result = pts[0].fX < pts[1].fX ? pts[0] : pts[1]; + dy = pts[1].fY - pts[0].fY; + } else if (SkPath::kQuad_Verb == verb) { + SkDQuad quad; + quad.set(pts); + if (!quad.monotonicInX()) { + roots = SkDQuad::FindExtrema(&quad[0].fX, &t); + } + if (roots) { + result = quad.ptAtT(t).asSkPoint(); + } else { + result = pts[0].fX < pts[2].fX ? pts[0] : pts[2]; + t = pts[0].fX < pts[2].fX ? 0 : 1; + } + dy = quad.dxdyAtT(t).fY; + } else if (SkPath::kConic_Verb == verb) { + SkDConic conic; + conic.set(pts, weight); + if (!conic.monotonicInX()) { + roots = SkDConic::FindExtrema(&conic[0].fX, weight, &t); + } + if (roots) { + result = conic.ptAtT(t).asSkPoint(); + } else { + result = pts[0].fX < pts[2].fX ? pts[0] : pts[2]; + t = pts[0].fX < pts[2].fX ? 0 : 1; + } + dy = conic.dxdyAtT(t).fY; + } else { + SkASSERT(SkPath::kCubic_Verb == verb); + SkDCubic cubic; + cubic.set(pts); + if (!cubic.monotonicInX()) { + double tValues[2]; + roots = SkDCubic::FindExtrema(&cubic[0].fX, tValues); + SkASSERT(roots <= 2); + for (int index = 0; index < roots; ++index) { + SkPoint temp = cubic.ptAtT(tValues[index]).asSkPoint(); + if (0 == index || result.fX > temp.fX) { + result = temp; + t = tValues[index]; + } + } + } + if (roots) { + result = cubic.ptAtT(t).asSkPoint(); + } else { + result = pts[0].fX < pts[3].fX ? pts[0] : pts[3]; + t = pts[0].fX < pts[3].fX ? 0 : 1; + } + dy = cubic.dxdyAtT(t).fY; + } + *direction = to_direction(dy); + return result; +} + +class OpAsWinding { +public: + enum class Edge { + kInitial, + kCompare, + }; + + OpAsWinding(const SkPath& path) + : fPath(path) { + } + + void contourBounds(vector<Contour>* containers) { + SkRect bounds; + bounds.setEmpty(); + SkPath::RawIter iter(fPath); + SkPoint pts[4]; + SkPath::Verb verb; + int lastStart = 0; + int verbStart = 0; + do { + verb = iter.next(pts); + if (SkPath::kMove_Verb == verb) { + if (!bounds.isEmpty()) { + containers->emplace_back(bounds, lastStart, verbStart); + lastStart = verbStart; + } + bounds.setBounds(&pts[kPtIndex[verb]], kPtCount[verb]); + } + if (SkPath::kLine_Verb <= verb && verb <= SkPath::kCubic_Verb) { + SkRect verbBounds; + verbBounds.setBounds(&pts[kPtIndex[verb]], kPtCount[verb]); + bounds.joinPossiblyEmptyRect(verbBounds); + } + ++verbStart; + } while (SkPath::kDone_Verb != verb); + if (!bounds.isEmpty()) { + containers->emplace_back(bounds, lastStart, verbStart); + } + } + + int nextEdge(Contour& contour, Edge edge) { + SkPath::Iter iter(fPath, true); + SkPoint pts[4]; + SkPath::Verb verb; + int verbCount = -1; + int winding = 0; + do { + verb = iter.next(pts); + if (++verbCount < contour.fVerbStart) { + continue; + } + if (verbCount >= contour.fVerbEnd) { + continue; + } + if (SkPath::kLine_Verb > verb || verb > SkPath::kCubic_Verb) { + continue; + } + bool horizontal = true; + for (int index = 1; index <= kPtCount[verb]; ++index) { + if (pts[0].fY != pts[index].fY) { + horizontal = false; + break; + } + } + if (horizontal) { + continue; + } + if (edge == Edge::kCompare) { + winding += contains_edge(pts, verb, conic_weight(iter, verb), contour.fMinXY); + continue; + } + SkASSERT(edge == Edge::kInitial); + Contour::Direction direction; + SkPoint minXY = left_edge(pts, verb, conic_weight(iter, verb), &direction); + if (minXY.fX > contour.fMinXY.fX) { + continue; + } + if (minXY.fX == contour.fMinXY.fX) { + if (minXY.fY != contour.fMinXY.fY) { + continue; + } + if (direction == contour.fDirection) { + continue; + } + // incomplete: must sort edges to find the one most to left + SkDebugf("incomplete\n"); + // TODO: add edges as opangle and sort + } + contour.fMinXY = minXY; + contour.fDirection = direction; + } while (SkPath::kDone_Verb != verb); + return winding; + } + + void containerContains(Contour& contour, Contour& test) { + // find outside point on lesser contour + // arbitrarily, choose non-horizontal edge where point <= bounds left + // note that if leftmost point is control point, may need tight bounds + // to find edge with minimum-x + if (SK_ScalarMax == test.fMinXY.fX) { + this->nextEdge(test, Edge::kInitial); + } + // find all edges on greater equal or to the left of one on lesser + contour.fMinXY = test.fMinXY; + int winding = this->nextEdge(contour, Edge::kCompare); + // if edge is up, mark contour cw, otherwise, ccw + // sum of greater edges direction should be cw, 0, ccw + SkASSERT(-1 <= winding && winding <= 1); + test.fContained = winding != 0; + } + + void inParent(Contour& contour, Contour& parent) { + // move contour into sibling list contained by parent + for (auto test : parent.fChildren) { + if (test->fBounds.contains(contour.fBounds)) { + inParent(contour, *test); + return; + } + } + // move parent's children into contour's children if contained by contour + for (auto iter = parent.fChildren.begin(); iter != parent.fChildren.end(); ) { + if (contour.fBounds.contains((*iter)->fBounds)) { + contour.fChildren.push_back(*iter); + iter = parent.fChildren.erase(iter); + continue; + } + ++iter; + } + parent.fChildren.push_back(&contour); + } + + void checkContainerChildren(Contour* parent, Contour* child) { + for (auto grandChild : child->fChildren) { + checkContainerChildren(child, grandChild); + } + if (parent) { + containerContains(*parent, *child); + } + } + + bool markReverse(Contour* parent, Contour* child) { + bool reversed = false; + for (auto grandChild : child->fChildren) { + reversed |= markReverse(grandChild->fContained ? child : parent, grandChild); + } + if (parent && parent->fDirection == child->fDirection) { + child->fReverse = true; + child->fDirection = (Contour::Direction) -(int) child->fDirection; + return true; + } + return reversed; + } + + void reverseMarkedContours(vector<Contour>& contours, SkPath* result) { + SkPath::RawIter iter(fPath); + int verbCount = 0; + for (auto contour : contours) { + SkPath reverse; + SkPath* temp = contour.fReverse ? &reverse : result; + do { + SkPoint pts[4]; + switch (iter.next(pts)) { + case SkPath::kMove_Verb: + temp->moveTo(pts[0]); + break; + case SkPath::kLine_Verb: + temp->lineTo(pts[1]); + break; + case SkPath::kQuad_Verb: + temp->quadTo(pts[1], pts[2]); + break; + case SkPath::kConic_Verb: + temp->conicTo(pts[1], pts[2], iter.conicWeight()); + break; + case SkPath::kCubic_Verb: + temp->cubicTo(pts[1], pts[2], pts[3]); + break; + case SkPath::kClose_Verb: + temp->close(); + break; + case SkPath::kDone_Verb: + break; + default: + SkASSERT(0); + } + } while (++verbCount < contour.fVerbEnd); + if (contour.fReverse) { + result->reverseAddPath(reverse); + } + } + } + +private: + const SkPath& fPath; +}; + +static bool set_result_path(SkPath* result, const SkPath& path, SkPath::FillType fillType) { + *result = path; + result->setFillType(fillType); + return true; +} + +bool SK_API AsWinding(const SkPath& path, SkPath* result) { + if (!path.isFinite()) { + return false; + } + SkPath::FillType fillType = path.getFillType(); + if (fillType == SkPath::kWinding_FillType + || fillType == SkPath::kInverseWinding_FillType ) { + return set_result_path(result, path, fillType); + } + fillType = path.isInverseFillType() ? SkPath::kInverseWinding_FillType : + SkPath::kWinding_FillType; + if (path.isEmpty() || path.isConvex()) { + return set_result_path(result, path, fillType); + } + // count contours + vector<Contour> contours; // one per contour + OpAsWinding winder(path); + winder.contourBounds(&contours); + if (contours.size() <= 1) { + return set_result_path(result, path, fillType); + } + // create contour bounding box tree + Contour sorted(SkRect(), 0, 0); + for (auto& contour : contours) { + winder.inParent(contour, sorted); + } + // if sorted has no grandchildren, no child has to fix its children's winding + if (std::all_of(sorted.fChildren.begin(), sorted.fChildren.end(), + [](const Contour* contour) -> bool { return !contour->fChildren.size(); } )) { + return set_result_path(result, path, fillType); + } + // starting with outermost and moving inward, see if one path contains another + for (auto contour : sorted.fChildren) { + winder.nextEdge(*contour, OpAsWinding::Edge::kInitial); + winder.checkContainerChildren(nullptr, contour); + } + // starting with outermost and moving inward, mark paths to reverse + bool reversed = false; + for (auto contour : sorted.fChildren) { + reversed |= winder.markReverse(nullptr, contour); + } + if (!reversed) { + return set_result_path(result, path, fillType); + } + SkPath temp; + temp.setFillType(fillType); + winder.reverseMarkedContours(contours, &temp); + result->swap(temp); + return true; +} |