/** * Generates the lines and quads to be rendered. Lines are always recorded in * device space. We will do a device space bloat to account for the 1pixel * thickness. * Quads are recorded in device space unless m contains * perspective, then in they are in src space. We do this because we will * subdivide large quads to reduce over-fill. This subdivision has to be * performed before applying the perspective matrix. */ static int gather_lines_and_quads(const SkPath& path, const SkMatrix& m, const SkIRect& devClipBounds, GrAAHairLinePathRenderer::PtArray* lines, GrAAHairLinePathRenderer::PtArray* quads, GrAAHairLinePathRenderer::PtArray* conics, GrAAHairLinePathRenderer::IntArray* quadSubdivCnts, GrAAHairLinePathRenderer::FloatArray* conicWeights) { SkPath::Iter iter(path, false); int totalQuadCount = 0; SkRect bounds; SkIRect ibounds; bool persp = m.hasPerspective(); for (;;) { SkPoint pathPts[4]; SkPoint devPts[4]; SkPath::Verb verb = iter.next(pathPts); switch (verb) { case SkPath::kConic_Verb: { SkConic dst[4]; // We chop the conics to create tighter clipping to hide error // that appears near max curvature of very thin conics. Thin // hyperbolas with high weight still show error. int conicCnt = chop_conic(pathPts, dst, iter.conicWeight()); for (int i = 0; i < conicCnt; ++i) { SkPoint* chopPnts = dst[i].fPts; m.mapPoints(devPts, chopPnts, 3); bounds.setBounds(devPts, 3); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { if (is_degen_quad_or_conic(devPts)) { SkPoint* pts = lines->push_back_n(4); pts[0] = devPts[0]; pts[1] = devPts[1]; pts[2] = devPts[1]; pts[3] = devPts[2]; } else { // when in perspective keep conics in src space SkPoint* cPts = persp ? chopPnts : devPts; SkPoint* pts = conics->push_back_n(3); pts[0] = cPts[0]; pts[1] = cPts[1]; pts[2] = cPts[2]; conicWeights->push_back() = dst[i].fW; } } } break; } case SkPath::kMove_Verb: break; case SkPath::kLine_Verb: m.mapPoints(devPts, pathPts, 2); bounds.setBounds(devPts, 2); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { SkPoint* pts = lines->push_back_n(2); pts[0] = devPts[0]; pts[1] = devPts[1]; } break; case SkPath::kQuad_Verb: { SkPoint choppedPts[5]; // Chopping the quad helps when the quad is either degenerate or nearly degenerate. // When it is degenerate it allows the approximation with lines to work since the // chop point (if there is one) will be at the parabola's vertex. In the nearly // degenerate the QuadUVMatrix computed for the points is almost singular which // can cause rendering artifacts. int n = SkChopQuadAtMaxCurvature(pathPts, choppedPts); for (int i = 0; i < n; ++i) { SkPoint* quadPts = choppedPts + i * 2; m.mapPoints(devPts, quadPts, 3); bounds.setBounds(devPts, 3); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { int subdiv = num_quad_subdivs(devPts); SkASSERT(subdiv >= -1); if (-1 == subdiv) { SkPoint* pts = lines->push_back_n(4); pts[0] = devPts[0]; pts[1] = devPts[1]; pts[2] = devPts[1]; pts[3] = devPts[2]; } else { // when in perspective keep quads in src space SkPoint* qPts = persp ? quadPts : devPts; SkPoint* pts = quads->push_back_n(3); pts[0] = qPts[0]; pts[1] = qPts[1]; pts[2] = qPts[2]; quadSubdivCnts->push_back() = subdiv; totalQuadCount += 1 << subdiv; } } } break; } case SkPath::kCubic_Verb: m.mapPoints(devPts, pathPts, 4); bounds.setBounds(devPts, 4); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { PREALLOC_PTARRAY(32) q; // we don't need a direction if we aren't constraining the subdivision const SkPathPriv::FirstDirection kDummyDir = SkPathPriv::kCCW_FirstDirection; // We convert cubics to quadratics (for now). // In perspective have to do conversion in src space. if (persp) { SkScalar tolScale = GrPathUtils::scaleToleranceToSrc(SK_Scalar1, m, path.getBounds()); GrPathUtils::convertCubicToQuads(pathPts, tolScale, false, kDummyDir, &q); } else { GrPathUtils::convertCubicToQuads(devPts, SK_Scalar1, false, kDummyDir, &q); } for (int i = 0; i < q.count(); i += 3) { SkPoint* qInDevSpace; // bounds has to be calculated in device space, but q is // in src space when there is perspective. if (persp) { m.mapPoints(devPts, &q[i], 3); bounds.setBounds(devPts, 3); qInDevSpace = devPts; } else { bounds.setBounds(&q[i], 3); qInDevSpace = &q[i]; } bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { int subdiv = num_quad_subdivs(qInDevSpace); SkASSERT(subdiv >= -1); if (-1 == subdiv) { SkPoint* pts = lines->push_back_n(4); // lines should always be in device coords pts[0] = qInDevSpace[0]; pts[1] = qInDevSpace[1]; pts[2] = qInDevSpace[1]; pts[3] = qInDevSpace[2]; } else { SkPoint* pts = quads->push_back_n(3); // q is already in src space when there is no // perspective and dev coords otherwise. pts[0] = q[0 + i]; pts[1] = q[1 + i]; pts[2] = q[2 + i]; quadSubdivCnts->push_back() = subdiv; totalQuadCount += 1 << subdiv; } } } } break; case SkPath::kClose_Verb: break; case SkPath::kDone_Verb: return totalQuadCount; } } }
/** * Generates the lines and quads to be rendered. Lines are always recorded in * device space. We will do a device space bloat to account for the 1pixel * thickness. * Quads are recorded in device space unless m contains * perspective, then in they are in src space. We do this because we will * subdivide large quads to reduce over-fill. This subdivision has to be * performed before applying the perspective matrix. */ static int gather_lines_and_quads(const SkPath& path, const SkMatrix& m, const SkIRect& devClipBounds, SkScalar capLength, bool convertConicsToQuads, GrAAHairLinePathRenderer::PtArray* lines, GrAAHairLinePathRenderer::PtArray* quads, GrAAHairLinePathRenderer::PtArray* conics, GrAAHairLinePathRenderer::IntArray* quadSubdivCnts, GrAAHairLinePathRenderer::FloatArray* conicWeights) { SkPath::Iter iter(path, false); int totalQuadCount = 0; SkRect bounds; SkIRect ibounds; bool persp = m.hasPerspective(); // Whenever a degenerate, zero-length contour is encountered, this code will insert a // 'capLength' x-aligned line segment. Since this is rendering hairlines it is hoped this will // suffice for AA square & circle capping. int verbsInContour = 0; // Does not count moves bool seenZeroLengthVerb = false; SkPoint zeroVerbPt; // Adds a quad that has already been chopped to the list and checks for quads that are close to // lines. Also does a bounding box check. It takes points that are in src space and device // space. The src points are only required if the view matrix has perspective. auto addChoppedQuad = [&](const SkPoint srcPts[3], const SkPoint devPts[4], bool isContourStart) { SkRect bounds; SkIRect ibounds; bounds.setBounds(devPts, 3); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); // We only need the src space space pts when not in perspective. SkASSERT(srcPts || !persp); if (SkIRect::Intersects(devClipBounds, ibounds)) { int subdiv = num_quad_subdivs(devPts); SkASSERT(subdiv >= -1); if (-1 == subdiv) { SkPoint* pts = lines->push_back_n(4); pts[0] = devPts[0]; pts[1] = devPts[1]; pts[2] = devPts[1]; pts[3] = devPts[2]; if (isContourStart && pts[0] == pts[1] && pts[2] == pts[3]) { seenZeroLengthVerb = true; zeroVerbPt = pts[0]; } } else { // when in perspective keep quads in src space const SkPoint* qPts = persp ? srcPts : devPts; SkPoint* pts = quads->push_back_n(3); pts[0] = qPts[0]; pts[1] = qPts[1]; pts[2] = qPts[2]; quadSubdivCnts->push_back() = subdiv; totalQuadCount += 1 << subdiv; } } }; // Applies the view matrix to quad src points and calls the above helper. auto addSrcChoppedQuad = [&](const SkPoint srcSpaceQuadPts[3], bool isContourStart) { SkPoint devPts[3]; m.mapPoints(devPts, srcSpaceQuadPts, 3); addChoppedQuad(srcSpaceQuadPts, devPts, isContourStart); }; for (;;) { SkPoint pathPts[4]; SkPath::Verb verb = iter.next(pathPts, false); switch (verb) { case SkPath::kConic_Verb: if (convertConicsToQuads) { SkScalar weight = iter.conicWeight(); SkAutoConicToQuads converter; const SkPoint* quadPts = converter.computeQuads(pathPts, weight, 0.25f); for (int i = 0; i < converter.countQuads(); ++i) { addSrcChoppedQuad(quadPts + 2 * i, !verbsInContour && 0 == i); } } else { SkConic dst[4]; // We chop the conics to create tighter clipping to hide error // that appears near max curvature of very thin conics. Thin // hyperbolas with high weight still show error. int conicCnt = chop_conic(pathPts, dst, iter.conicWeight()); for (int i = 0; i < conicCnt; ++i) { SkPoint devPts[4]; SkPoint* chopPnts = dst[i].fPts; m.mapPoints(devPts, chopPnts, 3); bounds.setBounds(devPts, 3); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { if (is_degen_quad_or_conic(devPts)) { SkPoint* pts = lines->push_back_n(4); pts[0] = devPts[0]; pts[1] = devPts[1]; pts[2] = devPts[1]; pts[3] = devPts[2]; if (verbsInContour == 0 && i == 0 && pts[0] == pts[1] && pts[2] == pts[3]) { seenZeroLengthVerb = true; zeroVerbPt = pts[0]; } } else { // when in perspective keep conics in src space SkPoint* cPts = persp ? chopPnts : devPts; SkPoint* pts = conics->push_back_n(3); pts[0] = cPts[0]; pts[1] = cPts[1]; pts[2] = cPts[2]; conicWeights->push_back() = dst[i].fW; } } } } verbsInContour++; break; case SkPath::kMove_Verb: // New contour (and last one was unclosed). If it was just a zero length drawing // operation, and we're supposed to draw caps, then add a tiny line. if (seenZeroLengthVerb && verbsInContour == 1 && capLength > 0) { SkPoint* pts = lines->push_back_n(2); pts[0] = SkPoint::Make(zeroVerbPt.fX - capLength, zeroVerbPt.fY); pts[1] = SkPoint::Make(zeroVerbPt.fX + capLength, zeroVerbPt.fY); } verbsInContour = 0; seenZeroLengthVerb = false; break; case SkPath::kLine_Verb: { SkPoint devPts[2]; m.mapPoints(devPts, pathPts, 2); bounds.setBounds(devPts, 2); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { SkPoint* pts = lines->push_back_n(2); pts[0] = devPts[0]; pts[1] = devPts[1]; if (verbsInContour == 0 && pts[0] == pts[1]) { seenZeroLengthVerb = true; zeroVerbPt = pts[0]; } } verbsInContour++; break; } case SkPath::kQuad_Verb: { SkPoint choppedPts[5]; // Chopping the quad helps when the quad is either degenerate or nearly degenerate. // When it is degenerate it allows the approximation with lines to work since the // chop point (if there is one) will be at the parabola's vertex. In the nearly // degenerate the QuadUVMatrix computed for the points is almost singular which // can cause rendering artifacts. int n = SkChopQuadAtMaxCurvature(pathPts, choppedPts); for (int i = 0; i < n; ++i) { addSrcChoppedQuad(choppedPts + i * 2, !verbsInContour && 0 == i); } verbsInContour++; break; } case SkPath::kCubic_Verb: { SkPoint devPts[4]; m.mapPoints(devPts, pathPts, 4); bounds.setBounds(devPts, 4); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { PREALLOC_PTARRAY(32) q; // We convert cubics to quadratics (for now). // In perspective have to do conversion in src space. if (persp) { SkScalar tolScale = GrPathUtils::scaleToleranceToSrc(SK_Scalar1, m, path.getBounds()); GrPathUtils::convertCubicToQuads(pathPts, tolScale, &q); } else { GrPathUtils::convertCubicToQuads(devPts, SK_Scalar1, &q); } for (int i = 0; i < q.count(); i += 3) { if (persp) { addSrcChoppedQuad(&q[i], !verbsInContour && 0 == i); } else { addChoppedQuad(nullptr, &q[i], !verbsInContour && 0 == i); } } } verbsInContour++; break; } case SkPath::kClose_Verb: // Contour is closed, so we don't need to grow the starting line, unless it's // *just* a zero length subpath. (SVG Spec 11.4, 'stroke'). if (capLength > 0) { if (seenZeroLengthVerb && verbsInContour == 1) { SkPoint* pts = lines->push_back_n(2); pts[0] = SkPoint::Make(zeroVerbPt.fX - capLength, zeroVerbPt.fY); pts[1] = SkPoint::Make(zeroVerbPt.fX + capLength, zeroVerbPt.fY); } else if (verbsInContour == 0) { // Contour was (moveTo, close). Add a line. SkPoint devPts[2]; m.mapPoints(devPts, pathPts, 1); devPts[1] = devPts[0]; bounds.setBounds(devPts, 2); bounds.outset(SK_Scalar1, SK_Scalar1); bounds.roundOut(&ibounds); if (SkIRect::Intersects(devClipBounds, ibounds)) { SkPoint* pts = lines->push_back_n(2); pts[0] = SkPoint::Make(devPts[0].fX - capLength, devPts[0].fY); pts[1] = SkPoint::Make(devPts[1].fX + capLength, devPts[1].fY); } } } break; case SkPath::kDone_Verb: if (seenZeroLengthVerb && verbsInContour == 1 && capLength > 0) { // Path ended with a dangling (moveTo, line|quad|etc). If the final verb is // degenerate, we need to draw a line. SkPoint* pts = lines->push_back_n(2); pts[0] = SkPoint::Make(zeroVerbPt.fX - capLength, zeroVerbPt.fY); pts[1] = SkPoint::Make(zeroVerbPt.fX + capLength, zeroVerbPt.fY); } return totalQuadCount; } } }