blob: d019e578878450d09f2a214da42a5166883f2c74 [file] [log] [blame]
/*
* Copyright 2025 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/private/SkPathRef.h"
#include "src/core/SkPathPriv.h"
/*
Determines if path is a rect by keeping track of changes in direction
and looking for a loop either clockwise or counterclockwise.
The direction is computed such that:
0: vertical up
1: horizontal left
2: vertical down
3: horizontal right
A rectangle cycles up/right/down/left or up/left/down/right.
The test fails if:
The path is closed, and followed by a line.
A second move creates a new endpoint.
A diagonal line is parsed.
There's more than four changes of direction.
There's a discontinuity on the line (e.g., a move in the middle)
The line reverses direction.
The path contains a quadratic or cubic.
The path contains fewer than four points.
*The rectangle doesn't complete a cycle.
*The final point isn't equal to the first point.
*These last two conditions we relax if we have a 3-edge path that would
form a rectangle if it were closed (as we do when we fill a path)
It's OK if the path has:
Several colinear line segments composing a rectangle side.
Single points on the rectangle side.
The direction takes advantage of the corners found since opposite sides
must travel in opposite directions.
FIXME: Allow colinear quads and cubics to be treated like lines.
FIXME: If the API passes fill-only, return true if the filled stroke
is a rectangle, though the caller failed to close the path.
directions values:
0x1 is set if the segment is horizontal
0x2 is set if the segment is moving to the right or down
thus:
two directions are opposites iff (dirA ^ dirB) == 0x2
two directions are perpendicular iff (dirA ^ dirB) == 0x1
*/
static int rect_make_dir(SkScalar dx, SkScalar dy) {
return ((0 != dx) << 0) | ((dx > 0 || dy > 0) << 1);
}
std::optional<SkPathPriv::RectContour> SkPathPriv::IsRectContour(SkSpan<const SkPoint> ptSpan,
SkSpan<const SkPathVerb> vbSpan,
bool allowPartial) {
if (ptSpan.size() < 4) {
return {};
}
size_t currVerb = 0;
const size_t verbCnt = vbSpan.size();
int corners = 0;
SkPoint closeXY; // used to determine if final line falls on a diagonal
SkPoint lineStart; // used to construct line from previous point
const SkPoint* firstPt = nullptr; // first point in the rect (last of first moves)
const SkPoint* lastPt = nullptr; // last point in the rect (last of lines or first if closed)
SkPoint firstCorner;
SkPoint thirdCorner;
const SkPoint* pts = ptSpan.data();
const SkPoint* savePts = nullptr; // used to allow caller to iterate through a pair of rects
lineStart.set(0, 0);
signed char directions[] = {-1, -1, -1, -1, -1}; // -1 to 3; -1 is uninitialized
bool closedOrMoved = false;
bool autoClose = false;
bool insertClose = false;
while (currVerb < verbCnt && (!allowPartial || !autoClose)) {
SkPathVerb verb = insertClose ? SkPathVerb::kClose : vbSpan[currVerb];
switch (verb) {
case SkPathVerb::kClose:
savePts = pts;
autoClose = true;
insertClose = false;
[[fallthrough]];
case SkPathVerb::kLine: {
if (SkPathVerb::kClose != verb) {
lastPt = pts;
}
SkPoint lineEnd = SkPathVerb::kClose == verb ? *firstPt : *pts++;
SkVector lineDelta = lineEnd - lineStart;
if (lineDelta.fX && lineDelta.fY) {
return {}; // diagonal
}
if (!lineDelta.isFinite()) {
return {}; // path contains infinity or NaN
}
if (lineStart == lineEnd) {
break; // single point on side OK
}
int nextDirection = rect_make_dir(lineDelta.fX, lineDelta.fY); // 0 to 3
if (0 == corners) {
directions[0] = nextDirection;
corners = 1;
closedOrMoved = false;
lineStart = lineEnd;
break;
}
if (closedOrMoved) {
return {}; // closed followed by a line
}
if (autoClose && nextDirection == directions[0]) {
break; // colinear with first
}
closedOrMoved = autoClose;
if (directions[corners - 1] == nextDirection) {
if (3 == corners && SkPathVerb::kLine == verb) {
thirdCorner = lineEnd;
}
lineStart = lineEnd;
break; // colinear segment
}
directions[corners++] = nextDirection;
// opposite lines must point in opposite directions; xoring them should equal 2
switch (corners) {
case 2:
firstCorner = lineStart;
break;
case 3:
if ((directions[0] ^ directions[2]) != 2) {
return {};
}
thirdCorner = lineEnd;
break;
case 4:
if ((directions[1] ^ directions[3]) != 2) {
return {};
}
break;
default:
return {}; // too many direction changes
}
lineStart = lineEnd;
break;
}
case SkPathVerb::kQuad:
case SkPathVerb::kConic:
case SkPathVerb::kCubic:
return {}; // quadratic, cubic not allowed
case SkPathVerb::kMove:
if (allowPartial && !autoClose && directions[0] >= 0) {
insertClose = true;
currVerb -= 1; // try move again afterwards
goto addMissingClose;
}
if (!corners) {
firstPt = pts;
} else {
closeXY = *firstPt - *lastPt;
if (closeXY.fX && closeXY.fY) {
return {}; // we're diagonal, abort
}
}
lineStart = *pts++;
closedOrMoved = true;
break;
default:
SkDEBUGFAIL("unexpected verb");
break;
}
currVerb += 1;
addMissingClose:
;
}
// Success if 4 corners and first point equals last
if (corners < 3 || corners > 4) {
return {};
}
// check if close generates diagonal
closeXY = *firstPt - *lastPt;
if (closeXY.fX && closeXY.fY) {
return {};
}
auto bounds = [](SkPoint a, SkPoint b) {
SkRect r;
r.set(a, b);
return r;
};
return {{
bounds(firstCorner, thirdCorner),
autoClose,
directions[0] == ((directions[1] + 1) & 3) ? SkPathDirection::kCW
: SkPathDirection::kCCW,
savePts ? size_t(savePts - ptSpan.data()) : 0,
currVerb,
}};
}
bool SkPathPriv::IsRectContour(const SkPath& path, bool allowPartial, int* currVerb,
const SkPoint** ptsPtr, bool* isClosed, SkPathDirection* direction,
SkRect* rect) {
// This crazy signature passes us a path, and a pointer into ints points-array.
// We want to construct a span that safely describes that pointer, so we 'deduce' its
// offset into the path's points.
const SkPoint* pathPoints = path.fPathRef->points();
const size_t npoints = path.fPathRef->countPoints();
const size_t ptIndex = *ptsPtr - pathPoints;
SkASSERT(ptIndex <= npoints);
SkSpan<const SkPoint> pts = { *ptsPtr, npoints - ptIndex };
SkDEBUGCODE(const size_t nverbs = path.fPathRef->verbs().size();)
SkASSERT((unsigned)*currVerb <= nverbs);
SkSpan<const SkPathVerb> vbs = path.fPathRef->verbs().subspan(*currVerb);
if (auto rc = IsRectContour(pts, vbs, allowPartial)) {
if (rect) {
*rect = rc->fRect;
}
if (isClosed) {
*isClosed = rc->fIsClosed;
}
if (direction) {
*direction = rc->fDirection;
}
SkASSERT(ptIndex + rc->fPointsConsumed <= npoints);
*ptsPtr += rc->fPointsConsumed;
SkASSERT(*currVerb + rc->fVerbsConsumed <= nverbs);
*currVerb += rc->fVerbsConsumed;
return true;
}
return false;
}
bool SkPathPriv::IsNestedFillRects(const SkPath& path, SkRect rects[2], SkPathDirection dirs[2]) {
SkDEBUGCODE(path.validate();)
int currVerb = 0;
const SkPoint* pts = path.fPathRef->points();
SkPathDirection testDirs[2];
SkRect testRects[2];
if (!IsRectContour(path, true, &currVerb, &pts, nullptr, &testDirs[0], &testRects[0])) {
return false;
}
if (IsRectContour(path, false, &currVerb, &pts, nullptr, &testDirs[1], &testRects[1])) {
if (testRects[0].contains(testRects[1])) {
if (rects) {
rects[0] = testRects[0];
rects[1] = testRects[1];
}
if (dirs) {
dirs[0] = testDirs[0];
dirs[1] = testDirs[1];
}
return true;
}
if (testRects[1].contains(testRects[0])) {
if (rects) {
rects[0] = testRects[1];
rects[1] = testRects[0];
}
if (dirs) {
dirs[0] = testDirs[1];
dirs[1] = testDirs[0];
}
return true;
}
}
return false;
}