From c62318c748a1907649bd75382c4f4fd10533f2b3 Mon Sep 17 00:00:00 2001 From: bsalomon Date: Tue, 28 Jun 2016 10:41:34 -0700 Subject: [PATCH] Make lines a special case in GrShape BUG=skia: GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2108523002 Review-Url: https://codereview.chromium.org/2108523002 --- src/gpu/GrShape.cpp | 89 ++++++++--- src/gpu/GrShape.h | 48 +++++- src/gpu/GrSoftwarePathRenderer.cpp | 3 +- .../batches/GrAADistanceFieldPathRenderer.cpp | 3 +- src/gpu/batches/GrDashLinePathRenderer.cpp | 7 +- tests/GrShapeTest.cpp | 142 ++++++++++++++++-- 6 files changed, 244 insertions(+), 48 deletions(-) diff --git a/src/gpu/GrShape.cpp b/src/gpu/GrShape.cpp index 45ddb77778..5ffd32d46d 100644 --- a/src/gpu/GrShape.cpp +++ b/src/gpu/GrShape.cpp @@ -14,10 +14,10 @@ GrShape& GrShape::operator=(const GrShape& that) { case Type::kEmpty: break; case Type::kRRect: - fRRectData.fRRect = that.fRRectData.fRRect; - fRRectData.fDir = that.fRRectData.fDir; - fRRectData.fStart = that.fRRectData.fStart; - fRRectData.fInverted = that.fRRectData.fInverted; + fRRectData = that.fRRectData; + break; + case Type::kLine: + fLineData = that.fLineData; break; case Type::kPath: fPathData.fGenID = that.fPathData.fGenID; @@ -29,11 +29,29 @@ GrShape& GrShape::operator=(const GrShape& that) { return *this; } -const SkRect& GrShape::bounds() const { +SkRect GrShape::bounds() const { static constexpr SkRect kEmpty = SkRect::MakeEmpty(); switch (fType) { case Type::kEmpty: return kEmpty; + case Type::kLine: { + SkRect bounds; + if (fLineData.fPts[0].fX < fLineData.fPts[1].fX) { + bounds.fLeft = fLineData.fPts[0].fX; + bounds.fRight = fLineData.fPts[1].fX; + } else { + bounds.fLeft = fLineData.fPts[1].fX; + bounds.fRight = fLineData.fPts[0].fX; + } + if (fLineData.fPts[0].fY < fLineData.fPts[1].fY) { + bounds.fTop = fLineData.fPts[0].fY; + bounds.fBottom = fLineData.fPts[1].fY; + } else { + bounds.fTop = fLineData.fPts[1].fY; + bounds.fBottom = fLineData.fPts[0].fY; + } + return bounds; + } case Type::kRRect: return fRRectData.fRRect.getBounds(); case Type::kPath: @@ -43,12 +61,13 @@ const SkRect& GrShape::bounds() const { return kEmpty; } -void GrShape::styledBounds(SkRect* bounds) const { +SkRect GrShape::styledBounds() const { if (Type::kEmpty == fType && !fStyle.hasNonDashPathEffect()) { - *bounds = SkRect::MakeEmpty(); - } else { - fStyle.adjustBounds(bounds, this->bounds()); + return SkRect::MakeEmpty(); } + SkRect bounds; + fStyle.adjustBounds(&bounds, this->bounds()); + return bounds; } int GrShape::unstyledKeySize() const { @@ -63,6 +82,10 @@ int GrShape::unstyledKeySize() const { SkASSERT(0 == SkRRect::kSizeInMemory % sizeof(uint32_t)); // + 1 for the direction, start index, and inverseness. return SkRRect::kSizeInMemory / sizeof(uint32_t) + 1; + case Type::kLine: + GR_STATIC_ASSERT(2 * sizeof(uint32_t) == sizeof(SkPoint)); + // 4 for the end points and 1 for the inverseness + return 5; case Type::kPath: if (0 == fPathData.fGenID) { return -1; @@ -94,6 +117,11 @@ void GrShape::writeUnstyledKey(uint32_t* key) const { *key++ |= fRRectData.fStart; SkASSERT(fRRectData.fStart < 8); break; + case Type::kLine: + memcpy(key, fLineData.fPts, 2 * sizeof(SkPoint)); + key += 4; + *key++ = fLineData.fInverted ? 1 : 0; + break; case Type::kPath: SkASSERT(fPathData.fGenID); *key++ = fPathData.fGenID; @@ -159,10 +187,10 @@ GrShape::GrShape(const GrShape& that) : fStyle(that.fStyle) { case Type::kEmpty: break; case Type::kRRect: - fRRectData.fRRect = that.fRRectData.fRRect; - fRRectData.fDir = that.fRRectData.fDir; - fRRectData.fStart = that.fRRectData.fStart; - fRRectData.fInverted = that.fRRectData.fInverted; + fRRectData = that.fRRectData; + break; + case Type::kLine: + fLineData = that.fLineData; break; case Type::kPath: fPathData.fGenID = that.fPathData.fGenID; @@ -266,8 +294,14 @@ void GrShape::attemptToSimplifyPath() { SkPath::Direction rrectDir; unsigned rrectStart; bool inverted = this->path().isInverseFillType(); + SkPoint pts[2]; if (this->path().isEmpty()) { this->changeType(Type::kEmpty); + } else if (this->path().isLine(pts)) { + this->changeType(Type::kLine); + fLineData.fPts[0] = pts[0]; + fLineData.fPts[1] = pts[1]; + fLineData.fInverted = inverted; } else if (this->path().isRRect(&rrect, &rrectDir, &rrectStart)) { this->changeType(Type::kRRect); fRRectData.fRRect = rrect; @@ -313,6 +347,8 @@ void GrShape::attemptToSimplifyPath() { fInheritedKey.reset(0); if (Type::kRRect == fType) { this->attemptToSimplifyRRect(); + } else if (Type::kLine == fType) { + this->attemptToSimplifyLine(); } } else { if (fInheritedKey.count() || this->path().isVolatile()) { @@ -321,13 +357,8 @@ void GrShape::attemptToSimplifyPath() { fPathData.fGenID = this->path().getGenerationID(); } if (this->style().isSimpleFill()) { - // Filled paths are treated as though all their contours were closed. - // Since SkPath doesn't track individual contours, this will only close the last. :( - // There is no point in closing lines, though, since they loose their line-ness. - if (!this->path().isLine(nullptr)) { - this->path().close(); - this->path().setIsVolatile(true); - } + this->path().close(); + this->path().setIsVolatile(true); } if (!this->style().hasNonDashPathEffect()) { if (this->style().strokeRec().getStyle() == SkStrokeRec::kStroke_Style || @@ -368,3 +399,21 @@ void GrShape::attemptToSimplifyRRect() { fRRectData.fInverted = false; } } + +void GrShape::attemptToSimplifyLine() { + if (fStyle.isSimpleFill() && !fLineData.fInverted) { + this->changeType(Type::kEmpty); + } else { + // Only path effects could care about the order of the points. Otherwise canonicalize + // the point order + if (!fStyle.hasPathEffect()) { + SkPoint* pts = fLineData.fPts; + if (pts[1].fY < pts[0].fY || (pts[1].fY == pts[0].fY && pts[1].fX < pts[0].fX)) { + SkTSwap(pts[0], pts[1]); + } + } else if (fStyle.isDashed()) { + // Dashing ignores inverseness. + fLineData.fInverted = false; + } + } +} diff --git a/src/gpu/GrShape.h b/src/gpu/GrShape.h index 87ee26e173..8f3be58702 100644 --- a/src/gpu/GrShape.h +++ b/src/gpu/GrShape.h @@ -152,12 +152,19 @@ public: * If the unstyled shape is a straight line segment, returns true and sets pts to the endpoints. * An inverse filled line path is still considered a line. */ - bool asLine(SkPoint pts[2]) const { - if (fType != Type::kPath) { - return false; - } - return this->path().isLine(pts); - } + bool asLine(SkPoint pts[2], bool* inverted) const { + if (fType != Type::kLine) { + return false; + } + if (pts) { + pts[0] = fLineData.fPts[0]; + pts[1] = fLineData.fPts[1]; + } + if (inverted) { + *inverted = fLineData.fInverted; + } + return true; + } /** Returns the unstyled geometry as a path. */ void asPath(SkPath* out) const { @@ -175,6 +182,16 @@ public: out->setFillType(kDefaultPathFillType); } break; + case Type::kLine: + out->reset(); + out->moveTo(fLineData.fPts[0]); + out->lineTo(fLineData.fPts[1]); + if (fLineData.fInverted) { + out->setFillType(kDefaultPathInverseFillType); + } else { + out->setFillType(kDefaultPathFillType); + } + break; case Type::kPath: *out = this->path(); break; @@ -191,13 +208,13 @@ public: * Gets the bounds of the geometry without reflecting the shape's styling. This ignores * the inverse fill nature of the geometry. */ - const SkRect& bounds() const; + SkRect bounds() const; /** * Gets the bounds of the geometry reflecting the shape's styling (ignoring inverse fill * status). */ - void styledBounds(SkRect* bounds) const; + SkRect styledBounds() const; /** * Is this shape known to be convex, before styling is applied. An unclosed but otherwise @@ -210,6 +227,8 @@ public: return true; case Type::kRRect: return true; + case Type::kLine: + return true; case Type::kPath: // SkPath.isConvex() really means "is this path convex were it to be closed" and // thus doesn't give the correct answer for stroked paths, hence we also check @@ -231,6 +250,9 @@ public: case Type::kRRect: ret = fRRectData.fInverted; break; + case Type::kLine: + ret = fLineData.fInverted; + break; case Type::kPath: ret = this->path().isInverseFillType(); break; @@ -264,6 +286,8 @@ public: return true; case Type::kRRect: return true; + case Type::kLine: + return false; case Type::kPath: // SkPath doesn't keep track of the closed status of each contour. return SkPathPriv::IsClosedSingleContour(this->path()); @@ -282,6 +306,8 @@ public: return SkPath::kLine_SegmentMask; } return SkPath::kLine_SegmentMask | SkPath::kConic_SegmentMask; + case Type::kLine: + return SkPath::kLine_SegmentMask; case Type::kPath: return this->path().getSegmentMasks(); } @@ -307,6 +333,7 @@ private: enum class Type { kEmpty, kRRect, + kLine, kPath, }; @@ -356,6 +383,7 @@ private: void attemptToSimplifyPath(); void attemptToSimplifyRRect(); + void attemptToSimplifyLine(); // Defaults to use when there is no distinction between even/odd and winding fills. static constexpr SkPath::FillType kDefaultPathFillType = SkPath::kEvenOdd_FillType; @@ -420,6 +448,10 @@ private: // Gen ID of the original path (fPath may be modified) int32_t fGenID; } fPathData; + struct { + SkPoint fPts[2]; + bool fInverted; + } fLineData; }; GrStyle fStyle; SkAutoSTArray<8, uint32_t> fInheritedKey; diff --git a/src/gpu/GrSoftwarePathRenderer.cpp b/src/gpu/GrSoftwarePathRenderer.cpp index 983bd20cbe..45a4b78b18 100644 --- a/src/gpu/GrSoftwarePathRenderer.cpp +++ b/src/gpu/GrSoftwarePathRenderer.cpp @@ -38,8 +38,7 @@ bool get_shape_and_clip_bounds(int width, int height, *devShapeBounds = SkIRect::MakeWH(width, height); return false; } - SkRect shapeBounds; - shape.styledBounds(&shapeBounds); + SkRect shapeBounds = shape.styledBounds(); if (!shapeBounds.isEmpty()) { SkRect shapeSBounds; matrix.mapRect(&shapeSBounds, shapeBounds); diff --git a/src/gpu/batches/GrAADistanceFieldPathRenderer.cpp b/src/gpu/batches/GrAADistanceFieldPathRenderer.cpp index b178a68107..eb6a67dc20 100644 --- a/src/gpu/batches/GrAADistanceFieldPathRenderer.cpp +++ b/src/gpu/batches/GrAADistanceFieldPathRenderer.cpp @@ -109,8 +109,7 @@ bool GrAADistanceFieldPathRenderer::onCanDrawPath(const CanDrawPathArgs& args) c // scaled to have bounds within 2.0f*kLargeMIP by 2.0f*kLargeMIP // the goal is to accelerate rendering of lots of small paths that may be scaling SkScalar maxScale = args.fViewMatrix->getMaxScale(); - SkRect bounds; - args.fShape->styledBounds(&bounds); + SkRect bounds = args.fShape->styledBounds(); SkScalar maxDim = SkMaxScalar(bounds.width(), bounds.height()); return maxDim <= kMediumMIP && maxDim * maxScale <= 2.0f*kLargeMIP; diff --git a/src/gpu/batches/GrDashLinePathRenderer.cpp b/src/gpu/batches/GrDashLinePathRenderer.cpp index 8dba7b94da..e7ef2402a9 100644 --- a/src/gpu/batches/GrDashLinePathRenderer.cpp +++ b/src/gpu/batches/GrDashLinePathRenderer.cpp @@ -13,7 +13,10 @@ bool GrDashLinePathRenderer::onCanDrawPath(const CanDrawPathArgs& args) const { SkPoint pts[2]; - if (args.fShape->style().isDashed() && args.fShape->asLine(pts)) { + bool inverted; + if (args.fShape->style().isDashed() && args.fShape->asLine(pts, &inverted)) { + // We should never have an inverse dashed case. + SkASSERT(!inverted); return GrDashingEffect::CanDrawDashLine(pts, args.fShape->style(), *args.fViewMatrix); } return false; @@ -34,7 +37,7 @@ bool GrDashLinePathRenderer::onDrawPath(const DrawPathArgs& args) { aaMode = GrDashingEffect::AAMode::kNone; } SkPoint pts[2]; - SkAssertResult(args.fShape->asLine(pts)); + SkAssertResult(args.fShape->asLine(pts, nullptr)); SkAutoTUnref batch(GrDashingEffect::CreateDashLineBatch(args.fColor, *args.fViewMatrix, pts, diff --git a/tests/GrShapeTest.cpp b/tests/GrShapeTest.cpp index 1ee9bbc018..ca907a1f87 100644 --- a/tests/GrShapeTest.cpp +++ b/tests/GrShapeTest.cpp @@ -177,10 +177,9 @@ private: CheckBounds(r, fAppliedPE, fAppliedPE.bounds()); CheckBounds(r, fAppliedPEThenStroke, fAppliedPEThenStroke.bounds()); CheckBounds(r, fAppliedFull, fAppliedFull.bounds()); - SkRect styledBounds; - fBase.styledBounds(&styledBounds); + SkRect styledBounds = fBase.styledBounds(); CheckBounds(r, fAppliedFull, styledBounds); - fAppliedPE.styledBounds(&styledBounds); + styledBounds = fAppliedPE.styledBounds(); CheckBounds(r, fAppliedFull, styledBounds); // Check that the same path is produced when style is applied by GrShape and GrStyle. @@ -372,8 +371,10 @@ static void check_equivalence(skiatest::Reporter* r, const GrShape& a, const GrS } REPORTER_ASSERT(r, a.bounds() == b.bounds()); REPORTER_ASSERT(r, a.segmentMask() == b.segmentMask()); - SkPoint pts[4]; - REPORTER_ASSERT(r, a.asLine(pts) == b.asLine(pts + 2)); + // Init these to suppress warnings. + SkPoint pts[4] {{0, 0,}, {0, 0}, {0, 0}, {0, 0}} ; + bool invertedLine[2] {true, true}; + REPORTER_ASSERT(r, a.asLine(pts, &invertedLine[0]) == b.asLine(pts + 2, &invertedLine[1])); // mayBeInverseFilledAfterStyling() is allowed to differ if one has a arbitrary PE and the other // doesn't (since the PE can set any fill type on its output path). // Moreover, dash style explicitly ignores inverseness. So if one is dashed but not the other @@ -383,8 +384,11 @@ static void check_equivalence(skiatest::Reporter* r, const GrShape& a, const GrS REPORTER_ASSERT(r, a.mayBeInverseFilledAfterStyling() == b.mayBeInverseFilledAfterStyling()); } - if (a.asLine(pts)) { + if (a.asLine(nullptr, nullptr)) { REPORTER_ASSERT(r, pts[2] == pts[0] && pts[3] == pts[1]); + REPORTER_ASSERT(r, ignoreInversenessDifference || invertedLine[0] == invertedLine[1]); + REPORTER_ASSERT(r, invertedLine[0] == a.inverseFilled()); + REPORTER_ASSERT(r, invertedLine[1] == b.inverseFilled()); } REPORTER_ASSERT(r, ignoreInversenessDifference || a.inverseFilled() == b.inverseFilled()); } @@ -1349,6 +1353,114 @@ void test_rrect(skiatest::Reporter* r, const SkRRect& rrect) { } } +void test_lines(skiatest::Reporter* r) { + static constexpr SkPoint kA { 1, 1}; + static constexpr SkPoint kB { 5, -9}; + static constexpr SkPoint kC {-3, 17}; + + SkPath lineAB; + lineAB.moveTo(kA); + lineAB.lineTo(kB); + + SkPath lineBA; + lineBA.moveTo(kB); + lineBA.lineTo(kA); + + SkPath lineAC; + lineAC.moveTo(kB); + lineAC.lineTo(kC); + + SkPath invLineAB = lineAB; + invLineAB.setFillType(SkPath::kInverseEvenOdd_FillType); + + SkPaint fill; + SkPaint stroke; + stroke.setStyle(SkPaint::kStroke_Style); + stroke.setStrokeWidth(2.f); + SkPaint hairline; + hairline.setStyle(SkPaint::kStroke_Style); + hairline.setStrokeWidth(0.f); + SkPaint dash = stroke; + dash.setPathEffect(make_dash()); + + TestCase fillAB(lineAB, fill, r); + TestCase fillEmpty(SkPath(), fill, r); + fillAB.compare(r, fillEmpty, TestCase::kAllSame_ComparisonExpecation); + REPORTER_ASSERT(r, !fillAB.baseShape().asLine(nullptr, nullptr)); + + TestCase strokeAB(lineAB, stroke, r); + TestCase strokeBA(lineBA, stroke, r); + TestCase strokeAC(lineAC, stroke, r); + + TestCase hairlineAB(lineAB, hairline, r); + TestCase hairlineBA(lineBA, hairline, r); + TestCase hairlineAC(lineAC, hairline, r); + + TestCase dashAB(lineAB, dash, r); + TestCase dashBA(lineBA, dash, r); + TestCase dashAC(lineAC, dash, r); + + strokeAB.compare(r, fillAB, TestCase::kAllDifferent_ComparisonExpecation); + + strokeAB.compare(r, strokeBA, TestCase::kAllSame_ComparisonExpecation); + strokeAB.compare(r, strokeAC, TestCase::kAllDifferent_ComparisonExpecation); + + hairlineAB.compare(r, hairlineBA, TestCase::kAllSame_ComparisonExpecation); + hairlineAB.compare(r, hairlineAC, TestCase::kAllDifferent_ComparisonExpecation); + + dashAB.compare(r, dashBA, TestCase::kAllDifferent_ComparisonExpecation); + dashAB.compare(r, dashAC, TestCase::kAllDifferent_ComparisonExpecation); + + strokeAB.compare(r, hairlineAB, TestCase::kSameUpToStroke_ComparisonExpecation); + + // One of dashAB or dashBA should have the same line as strokeAB. It depends upon how + // GrShape canonicalizes line endpoints (when it can, i.e. when not dashed). + bool canonicalizeAsAB; + SkPoint canonicalPts[2] {kA, kB}; + // Init these to suppress warnings. + bool inverted = true; + SkPoint pts[2] {{0, 0}, {0, 0}}; + REPORTER_ASSERT(r, strokeAB.baseShape().asLine(pts, &inverted) && !inverted); + if (pts[0] == kA && pts[1] == kB) { + canonicalizeAsAB = true; + } else if (pts[1] == kA && pts[0] == kB) { + canonicalizeAsAB = false; + SkTSwap(canonicalPts[0], canonicalPts[1]); + } else { + ERRORF(r, "Should return pts (a,b) or (b, a)"); + return; + }; + + strokeAB.compare(r, canonicalizeAsAB ? dashAB : dashBA, + TestCase::kSameUpToPE_ComparisonExpecation); + REPORTER_ASSERT(r, strokeAB.baseShape().asLine(pts, &inverted) && !inverted && + pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]); + REPORTER_ASSERT(r, hairlineAB.baseShape().asLine(pts, &inverted) && !inverted && + pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]); + REPORTER_ASSERT(r, dashAB.baseShape().asLine(pts, &inverted) && !inverted && + pts[0] == kA && pts[1] == kB); + REPORTER_ASSERT(r, dashBA.baseShape().asLine(pts, &inverted) && !inverted && + pts[0] == kB && pts[1] == kA); + + + TestCase strokeInvAB(invLineAB, stroke, r); + TestCase hairlineInvAB(invLineAB, hairline, r); + TestCase dashInvAB(invLineAB, dash, r); + strokeInvAB.compare(r, strokeAB, TestCase::kAllDifferent_ComparisonExpecation); + hairlineInvAB.compare(r, hairlineAB, TestCase::kAllDifferent_ComparisonExpecation); + // Dashing ignores inverse. + dashInvAB.compare(r, dashAB, TestCase::kAllSame_ComparisonExpecation); + + REPORTER_ASSERT(r, strokeInvAB.baseShape().asLine(pts, &inverted) && inverted && + pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]); + REPORTER_ASSERT(r, hairlineInvAB.baseShape().asLine(pts, &inverted) && inverted && + pts[0] == canonicalPts[0] && pts[1] == canonicalPts[1]); + // Dashing ignores inverse. + REPORTER_ASSERT(r, dashInvAB.baseShape().asLine(pts, &inverted) && !inverted && + pts[0] == kA && pts[1] == kB); + +} + DEF_TEST(GrShape, reporter) { for (auto r : { SkRect::MakeWH(10, 20), SkRect::MakeWH(-10, -20), @@ -1375,7 +1487,7 @@ DEF_TEST(GrShape, reporter) { test_path_effect_fails(reporter, r); test_make_hairline_path_effect(reporter, r, true); GrShape shape(r); - REPORTER_ASSERT(reporter, !shape.asLine(nullptr)); + REPORTER_ASSERT(reporter, !shape.asLine(nullptr, nullptr)); } for (auto rr : { SkRRect::MakeRect(SkRect::MakeWH(10, 10)), @@ -1403,7 +1515,7 @@ DEF_TEST(GrShape, reporter) { test_path_effect_fails(reporter, rr); test_make_hairline_path_effect(reporter, rr, true); GrShape shape(rr); - REPORTER_ASSERT(reporter, !shape.asLine(nullptr)); + REPORTER_ASSERT(reporter, !shape.asLine(nullptr, nullptr)); } struct TestPath { @@ -1459,15 +1571,16 @@ DEF_TEST(GrShape, reporter) { // These tests all assume that the original GrShape for fill and stroke will be the // same. // However, that is not the case in special cases (e.g. an unclosed rect becomes a RRect - // GrShape with a fill style but becomes a Path GrShape when stroked). - if (testPath.fIsRRectForFill == testPath.fIsRRectForStroke) { + // GrShape with a fill style but becomes a Path GrShape when stroked). Similarly, a path + // that is a line becomes empty when filled but is special-cased as a line when stroked. + if (testPath.fIsRRectForFill == testPath.fIsRRectForStroke && !testPath.fIsLine) { test_basic(reporter, path); test_null_dash(reporter, path); test_path_effect_makes_rrect(reporter, path); } test_scale(reporter, path); // This test uses a stroking paint, hence use of fIsRRectForStroke - test_volatile_path(reporter, path, testPath.fIsRRectForStroke); + test_volatile_path(reporter, path, testPath.fIsRRectForStroke || testPath.fIsLine); test_dash_fill(reporter, path); // Test modifying various stroke params. test_stroke_param( @@ -1483,7 +1596,8 @@ DEF_TEST(GrShape, reporter) { test_unknown_path_effect(reporter, path); test_path_effect_makes_empty_shape(reporter, path); test_path_effect_fails(reporter, path); - test_make_hairline_path_effect(reporter, path, testPath.fIsRRectForStroke); + test_make_hairline_path_effect(reporter, path, testPath.fIsRRectForStroke || + testPath.fIsLine); } } @@ -1516,14 +1630,14 @@ DEF_TEST(GrShape, reporter) { strokePathCase.compare(reporter, strokeRRectCase, TestCase::kAllSame_ComparisonExpecation); } - REPORTER_ASSERT(reporter, testPath.fIsLine == fillPathCase.baseShape().asLine(nullptr)); - REPORTER_ASSERT(reporter, testPath.fIsLine == strokePathCase.baseShape().asLine(nullptr)); } // Test a volatile empty path. test_volatile_path(reporter, SkPath(), true); test_empty_shape(reporter); + + test_lines(reporter); } #endif -- 2.34.1