From 1a055b08acbeee34580f8953eca19f74dfe4bff5 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Fri, 8 Apr 2022 18:15:39 -0700 Subject: [PATCH] Add solid stroke coverage override (#127) --- .../entity/contents/solid_stroke_contents.cc | 24 +++ .../entity/contents/solid_stroke_contents.h | 3 + impeller/entity/entity_unittests.cc | 162 ++++++++++++------ 3 files changed, 136 insertions(+), 53 deletions(-) diff --git a/impeller/entity/contents/solid_stroke_contents.cc b/impeller/entity/contents/solid_stroke_contents.cc index 7e22f80e41f56..cee7da91438bb 100644 --- a/impeller/entity/contents/solid_stroke_contents.cc +++ b/impeller/entity/contents/solid_stroke_contents.cc @@ -4,6 +4,8 @@ #include "solid_stroke_contents.h" +#include + #include "impeller/entity/contents/clip_contents.h" #include "impeller/entity/contents/content_context.h" #include "impeller/entity/entity.h" @@ -27,6 +29,28 @@ const Color& SolidStrokeContents::GetColor() const { return color_; } +std::optional SolidStrokeContents::GetCoverage( + const Entity& entity) const { + auto path_coverage = entity.GetPathCoverage(); + if (!path_coverage.has_value()) { + return std::nullopt; + } + + Scalar max_radius = 0.5; + if (cap_ == Cap::kSquare) { + max_radius = max_radius * kSqrt2; + } + if (join_ == Join::kMiter) { + max_radius = std::max(max_radius, miter_limit_ * 0.5f); + } + Vector2 max_radius_xy = entity.GetTransformation().TransformDirection( + Vector2(max_radius, max_radius) * stroke_size_); + + return Rect(path_coverage->origin - max_radius_xy, + Size(path_coverage->size.width + max_radius_xy.x * 2, + path_coverage->size.height + max_radius_xy.y * 2)); +} + static VertexBuffer CreateSolidStrokeVertices( const Path& path, HostBuffer& buffer, diff --git a/impeller/entity/contents/solid_stroke_contents.h b/impeller/entity/contents/solid_stroke_contents.h index 66f81cc002e85..bde9f332f9b54 100644 --- a/impeller/entity/contents/solid_stroke_contents.h +++ b/impeller/entity/contents/solid_stroke_contents.h @@ -68,6 +68,9 @@ class SolidStrokeContents final : public Contents { Join GetStrokeJoin(); + // |Contents| + std::optional GetCoverage(const Entity& entity) const override; + // |Contents| bool Render(const ContentContext& renderer, const Entity& entity, diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc index b3904fba06bd8..b3cc1d0dfd5b1 100644 --- a/impeller/entity/entity_unittests.cc +++ b/impeller/entity/entity_unittests.cc @@ -12,6 +12,7 @@ #include "impeller/entity/contents/solid_stroke_contents.h" #include "impeller/entity/entity.h" #include "impeller/entity/entity_playground.h" +#include "impeller/geometry/geometry_unittests.h" #include "impeller/geometry/path_builder.h" #include "impeller/playground/playground.h" #include "impeller/playground/widgets.h" @@ -100,32 +101,51 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { ImGui::SetNextWindowPos( {0 * padding.x + margin.x, 1.7f * padding.y + margin.y}); } - ImGui::Begin("Controls"); + // Slightly above sqrt(2) by default, so that right angles are just below // the limit and acute angles are over the limit (causing them to get // beveled). static Scalar miter_limit = 1.41421357; static Scalar width = 30; - ImGui::SliderFloat("Miter limit", &miter_limit, 0, 30); - ImGui::SliderFloat("Stroke width", &width, 0, 100); - if (ImGui::Button("Reset")) { - miter_limit = 1.41421357; - width = 30; + + ImGui::Begin("Controls"); + { + ImGui::SliderFloat("Miter limit", &miter_limit, 0, 30); + ImGui::SliderFloat("Stroke width", &width, 0, 100); + if (ImGui::Button("Reset")) { + miter_limit = 1.41421357; + width = 30; + } } ImGui::End(); - auto create_contents = [width = width](SolidStrokeContents::Cap cap, - SolidStrokeContents::Join join) { + auto render_path = [width = width, &context, &pass]( + Path path, SolidStrokeContents::Cap cap, + SolidStrokeContents::Join join) { auto contents = std::make_unique(); contents->SetColor(Color::Red().Premultiply()); contents->SetStrokeSize(width); contents->SetStrokeCap(cap); contents->SetStrokeJoin(join); contents->SetStrokeMiter(miter_limit); - return contents; - }; - Entity entity; + Entity entity; + entity.SetPath(path); + entity.SetContents(std::move(contents)); + + auto coverage = entity.GetCoverage(); + if (coverage.has_value()) { + auto bounds_contents = std::make_unique(); + bounds_contents->SetColor(Color::Green().WithAlpha(0.5)); + Entity bounds_entity; + bounds_entity.SetPath( + PathBuilder{}.AddRect(entity.GetCoverage().value()).TakePath()); + bounds_entity.SetContents(std::move(bounds_contents)); + bounds_entity.Render(context, pass); + } + + entity.Render(context, pass); + }; const Point a_def(0, 0), b_def(0, 100), c_def(150, 0), d_def(150, -100), e_def(75, 75); @@ -133,43 +153,37 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { // Cap::kButt demo. { Point off = Point(0, 0) * padding + margin; - Point a, b, c, d; - std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, - Color::Black(), Color::White()); - std::tie(c, d) = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r, - Color::Black(), Color::White()); - entity.SetPath(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath()); - entity.SetContents(create_contents(SolidStrokeContents::Cap::kButt, - SolidStrokeContents::Join::kBevel)); - entity.Render(context, pass); + auto [a, b] = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, + Color::Black(), Color::White()); + auto [c, d] = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r, + Color::Black(), Color::White()); + render_path(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath(), + SolidStrokeContents::Cap::kButt, + SolidStrokeContents::Join::kBevel); } // Cap::kSquare demo. { Point off = Point(1, 0) * padding + margin; - Point a, b, c, d; - std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, - Color::Black(), Color::White()); - std::tie(c, d) = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r, - Color::Black(), Color::White()); - entity.SetPath(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath()); - entity.SetContents(create_contents(SolidStrokeContents::Cap::kSquare, - SolidStrokeContents::Join::kBevel)); - entity.Render(context, pass); + auto [a, b] = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, + Color::Black(), Color::White()); + auto [c, d] = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r, + Color::Black(), Color::White()); + render_path(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath(), + SolidStrokeContents::Cap::kSquare, + SolidStrokeContents::Join::kBevel); } // Cap::kRound demo. { Point off = Point(2, 0) * padding + margin; - Point a, b, c, d; - std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, - Color::Black(), Color::White()); - std::tie(c, d) = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r, - Color::Black(), Color::White()); - entity.SetPath(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath()); - entity.SetContents(create_contents(SolidStrokeContents::Cap::kRound, - SolidStrokeContents::Join::kBevel)); - entity.Render(context, pass); + auto [a, b] = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, + Color::Black(), Color::White()); + auto [c, d] = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r, + Color::Black(), Color::White()); + render_path(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath(), + SolidStrokeContents::Cap::kRound, + SolidStrokeContents::Join::kBevel); } // Join::kBevel demo. @@ -178,11 +192,9 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White()); Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White()); Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White()); - entity.SetPath( - PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath()); - entity.SetContents(create_contents(SolidStrokeContents::Cap::kButt, - SolidStrokeContents::Join::kBevel)); - entity.Render(context, pass); + render_path( + PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath(), + SolidStrokeContents::Cap::kButt, SolidStrokeContents::Join::kBevel); } // Join::kMiter demo. @@ -191,11 +203,9 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White()); Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White()); Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White()); - entity.SetPath( - PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath()); - entity.SetContents(create_contents(SolidStrokeContents::Cap::kButt, - SolidStrokeContents::Join::kMiter)); - entity.Render(context, pass); + render_path( + PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath(), + SolidStrokeContents::Cap::kButt, SolidStrokeContents::Join::kMiter); } // Join::kRound demo. @@ -204,11 +214,9 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White()); Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White()); Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White()); - entity.SetPath( - PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath()); - entity.SetContents(create_contents(SolidStrokeContents::Cap::kButt, - SolidStrokeContents::Join::kRound)); - entity.Render(context, pass); + render_path( + PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath(), + SolidStrokeContents::Cap::kButt, SolidStrokeContents::Join::kRound); } return true; @@ -789,5 +797,53 @@ TEST_F(EntityTest, ContentsGetBoundsForEmptyPathReturnsNullopt) { ASSERT_FALSE(entity.GetPathCoverage().has_value()); } +TEST_F(EntityTest, SolidStrokeCoverageIsCorrect) { + { + Entity entity; + auto contents = std::make_unique(); + contents->SetStrokeCap(SolidStrokeContents::Cap::kButt); + contents->SetStrokeJoin(SolidStrokeContents::Join::kBevel); + contents->SetStrokeSize(4); + entity.SetPath(PathBuilder{}.AddLine({0, 0}, {10, 10}).TakePath()); + entity.SetContents(std::move(contents)); + auto actual = entity.GetCoverage(); + auto expected = Rect::MakeLTRB(-2, -2, 12, 12); + ASSERT_TRUE(actual.has_value()); + ASSERT_RECT_NEAR(actual.value(), expected); + } + + // Cover the Cap::kSquare case. + { + Entity entity; + auto contents = std::make_unique(); + contents->SetStrokeCap(SolidStrokeContents::Cap::kSquare); + contents->SetStrokeJoin(SolidStrokeContents::Join::kBevel); + contents->SetStrokeSize(4); + entity.SetPath(PathBuilder{}.AddLine({0, 0}, {10, 10}).TakePath()); + entity.SetContents(std::move(contents)); + auto actual = entity.GetCoverage(); + auto expected = + Rect::MakeLTRB(-sqrt(8), -sqrt(8), 10 + sqrt(8), 10 + sqrt(8)); + ASSERT_TRUE(actual.has_value()); + ASSERT_RECT_NEAR(actual.value(), expected); + } + + // Cover the Join::kMiter case. + { + Entity entity; + auto contents = std::make_unique(); + contents->SetStrokeCap(SolidStrokeContents::Cap::kSquare); + contents->SetStrokeJoin(SolidStrokeContents::Join::kMiter); + contents->SetStrokeSize(4); + contents->SetStrokeMiter(2); + entity.SetPath(PathBuilder{}.AddLine({0, 0}, {10, 10}).TakePath()); + entity.SetContents(std::move(contents)); + auto actual = entity.GetCoverage(); + auto expected = Rect::MakeLTRB(-4, -4, 14, 14); + ASSERT_TRUE(actual.has_value()); + ASSERT_RECT_NEAR(actual.value(), expected); + } +} + } // namespace testing } // namespace impeller