diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 99c8122d0d994..5083bee902362 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -5115,6 +5115,8 @@ ORIGIN: ../../../flutter/impeller/entity/geometry/point_field_geometry.cc + ../. ORIGIN: ../../../flutter/impeller/entity/geometry/point_field_geometry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/rect_geometry.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/rect_geometry.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/geometry/round_rect_geometry.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/geometry/round_rect_geometry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/stroke_path_geometry.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/stroke_path_geometry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/vertices_geometry.cc + ../../../flutter/LICENSE @@ -7916,6 +7918,8 @@ FILE: ../../../flutter/impeller/entity/geometry/point_field_geometry.cc FILE: ../../../flutter/impeller/entity/geometry/point_field_geometry.h FILE: ../../../flutter/impeller/entity/geometry/rect_geometry.cc FILE: ../../../flutter/impeller/entity/geometry/rect_geometry.h +FILE: ../../../flutter/impeller/entity/geometry/round_rect_geometry.cc +FILE: ../../../flutter/impeller/entity/geometry/round_rect_geometry.h FILE: ../../../flutter/impeller/entity/geometry/stroke_path_geometry.cc FILE: ../../../flutter/impeller/entity/geometry/stroke_path_geometry.h FILE: ../../../flutter/impeller/entity/geometry/vertices_geometry.cc diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index de64dc27ec688..43ce3a337b997 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -2264,6 +2264,74 @@ TEST_P(AiksTest, FilledEllipsesRenderCorrectly) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +TEST_P(AiksTest, FilledRoundRectsRenderCorrectly) { + Canvas canvas; + canvas.Scale(GetContentScale()); + Paint paint; + const int color_count = 3; + Color colors[color_count] = { + Color::Blue(), + Color::Green(), + Color::Crimson(), + }; + + paint.color = Color::White(); + canvas.DrawPaint(paint); + + int c_index = 0; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + paint.color = colors[(c_index++) % color_count]; + canvas.DrawRRect(Rect::MakeXYWH(i * 100 + 10, j * 100 + 20, 80, 80), + Size(i * 5 + 10, j * 5 + 10), paint); + } + } + + std::vector gradient_colors = { + Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, + Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, + Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, + Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, + Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; + std::vector stops = { + 0.0, + (1.0 / 6.0) * 1, + (1.0 / 6.0) * 2, + (1.0 / 6.0) * 3, + (1.0 / 6.0) * 4, + (1.0 / 6.0) * 5, + 1.0, + }; + auto texture = CreateTextureForFixture("airplane.jpg", + /*enable_mipmapping=*/true); + + paint.color = Color::White().WithAlpha(0.1); + + paint.color_source = ColorSource::MakeRadialGradient( + {500, 550}, 75, std::move(gradient_colors), std::move(stops), + Entity::TileMode::kMirror, {}); + for (int i = 1; i <= 10; i++) { + int j = 11 - i; + canvas.DrawRRect(Rect::MakeLTRB(500 - i * 20, 550 - j * 20, // + 500 + i * 20, 550 + j * 20), + Size(i * 10, j * 10), paint); + } + + paint.color_source = ColorSource::MakeImage( + texture, Entity::TileMode::kRepeat, Entity::TileMode::kRepeat, {}, + Matrix::MakeTranslation({500, 20})); + for (int i = 1; i <= 10; i++) { + int j = 11 - i; + canvas.DrawRRect(Rect::MakeLTRB(700 - i * 20, 220 - j * 20, // + 700 + i * 20, 220 + j * 20), + Size(i * 10, j * 10), paint); + } + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + TEST_P(AiksTest, GradientStrokesRenderCorrectly) { // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 auto callback = [&](AiksContext& renderer) -> std::optional { @@ -4335,9 +4403,9 @@ TEST_P(AiksTest, GaussianBlurAtPeripheryVertical) { canvas.Scale(GetContentScale()); canvas.DrawRRect(Rect::MakeLTRB(0, 0, GetWindowSize().width, 100), - Point(10, 10), Paint{.color = Color::LimeGreen()}); + Size(10, 10), Paint{.color = Color::LimeGreen()}); canvas.DrawRRect(Rect::MakeLTRB(0, 110, GetWindowSize().width, 210), - Point(10, 10), Paint{.color = Color::Magenta()}); + Size(10, 10), Paint{.color = Color::Magenta()}); canvas.ClipRect(Rect::MakeLTRB(100, 0, 200, GetWindowSize().height)); canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt, ImageFilter::MakeBlur(Sigma(20.0), Sigma(20.0), @@ -4358,7 +4426,7 @@ TEST_P(AiksTest, GaussianBlurAtPeripheryHorizontal) { Rect::MakeXYWH(0, 0, boston->GetSize().width, boston->GetSize().height), Rect::MakeLTRB(0, 0, GetWindowSize().width, 100), Paint{}); canvas.DrawRRect(Rect::MakeLTRB(0, 110, GetWindowSize().width, 210), - Point(10, 10), Paint{.color = Color::Magenta()}); + Size(10, 10), Paint{.color = Color::Magenta()}); canvas.ClipRect(Rect::MakeLTRB(0, 50, GetWindowSize().width, 150)); canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt, ImageFilter::MakeBlur(Sigma(20.0), Sigma(20.0), diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index 221326aff4d82..6e51001b87b78 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -365,27 +365,31 @@ void Canvas::DrawOval(const Rect& rect, const Paint& paint) { GetCurrentPass().AddEntity(std::move(entity)); } -void Canvas::DrawRRect(Rect rect, Point corner_radii, const Paint& paint) { - if (corner_radii.x == corner_radii.y && - AttemptDrawBlurredRRect(rect, corner_radii.x, paint)) { +void Canvas::DrawRRect(const Rect& rect, + const Size& corner_radii, + const Paint& paint) { + if (corner_radii.IsSquare() && + AttemptDrawBlurredRRect(rect, corner_radii.width, paint)) { return; } - auto path = PathBuilder{} - .SetConvexity(Convexity::kConvex) - .AddRoundedRect(rect, corner_radii) - .SetBounds(rect) - .TakePath(); + if (paint.style == Paint::Style::kFill) { Entity entity; entity.SetTransform(GetCurrentTransform()); entity.SetClipDepth(GetClipDepth()); entity.SetBlendMode(paint.blend_mode); entity.SetContents(CreateContentsForGeometryWithFilters( - paint, Geometry::MakeFillPath(std::move(path)))); + paint, Geometry::MakeRoundRect(rect, corner_radii))); GetCurrentPass().AddEntity(std::move(entity)); return; } + + auto path = PathBuilder{} + .SetConvexity(Convexity::kConvex) + .AddRoundedRect(rect, corner_radii) + .SetBounds(rect) + .TakePath(); DrawPath(std::move(path), paint); } @@ -445,7 +449,7 @@ void Canvas::ClipRect(const Rect& rect, Entity::ClipOperation clip_op) { } void Canvas::ClipRRect(const Rect& rect, - Point corner_radii, + const Size& corner_radii, Entity::ClipOperation clip_op) { auto path = PathBuilder{} .SetConvexity(Convexity::kConvex) @@ -455,8 +459,8 @@ void Canvas::ClipRRect(const Rect& rect, auto size = rect.GetSize(); // Does the rounded rect have a flat part on the top/bottom or left/right? - bool flat_on_TB = corner_radii.x * 2 < size.width; - bool flat_on_LR = corner_radii.y * 2 < size.height; + bool flat_on_TB = corner_radii.width * 2 < size.width; + bool flat_on_LR = corner_radii.height * 2 < size.height; std::optional inner_rect = (flat_on_LR && flat_on_TB) ? rect.Expand(-corner_radii) : std::make_optional(); @@ -475,7 +479,7 @@ void Canvas::ClipRRect(const Rect& rect, IntersectCulling(rect); break; case Entity::ClipOperation::kDifference: - if (corner_radii.x <= 0.0 || corner_radii.y <= 0) { + if (corner_radii.IsEmpty()) { SubtractCulling(rect); } else { // We subtract the inner "tall" and "wide" rectangle pieces @@ -484,10 +488,10 @@ void Canvas::ClipRRect(const Rect& rect, // Since this is a subtract operation, we can subtract each // rectangle piece individually without fear of interference. if (flat_on_TB) { - SubtractCulling(rect.Expand({-corner_radii.x, 0.0})); + SubtractCulling(rect.Expand(Size{-corner_radii.width, 0.0})); } if (flat_on_LR) { - SubtractCulling(rect.Expand({0.0, -corner_radii.y})); + SubtractCulling(rect.Expand(Size{0.0, -corner_radii.height})); } } break; diff --git a/impeller/aiks/canvas.h b/impeller/aiks/canvas.h index 70c85cb1ef74f..b1c6bab07340c 100644 --- a/impeller/aiks/canvas.h +++ b/impeller/aiks/canvas.h @@ -106,7 +106,9 @@ class Canvas { void DrawOval(const Rect& rect, const Paint& paint); - void DrawRRect(Rect rect, Point corner_radii, const Paint& paint); + void DrawRRect(const Rect& rect, + const Size& corner_radii, + const Paint& paint); void DrawCircle(const Point& center, Scalar radius, const Paint& paint); @@ -136,7 +138,7 @@ class Canvas { void ClipRRect( const Rect& rect, - Point corner_radii, + const Size& corner_radii, Entity::ClipOperation clip_op = Entity::ClipOperation::kIntersect); void DrawPicture(const Picture& picture); diff --git a/impeller/aiks/canvas_recorder.h b/impeller/aiks/canvas_recorder.h index 99ddf1dab4d97..2a07a371ce0f2 100644 --- a/impeller/aiks/canvas_recorder.h +++ b/impeller/aiks/canvas_recorder.h @@ -195,7 +195,9 @@ class CanvasRecorder { paint); } - void DrawRRect(Rect rect, Point corner_radii, const Paint& paint) { + void DrawRRect(const Rect& rect, + const Size& corner_radii, + const Paint& paint) { return ExecuteAndSerialize(FLT_CANVAS_RECORDER_OP_ARG(DrawRRect), rect, corner_radii, paint); } @@ -248,7 +250,7 @@ class CanvasRecorder { void ClipRRect( const Rect& rect, - Point corner_radii, + const Size& corner_radii, Entity::ClipOperation clip_op = Entity::ClipOperation::kIntersect) { return ExecuteAndSerialize(FLT_CANVAS_RECORDER_OP_ARG(ClipRRect), rect, corner_radii, clip_op); diff --git a/impeller/display_list/dl_dispatcher.cc b/impeller/display_list/dl_dispatcher.cc index 465b71f49cf26..12681579fbb91 100644 --- a/impeller/display_list/dl_dispatcher.cc +++ b/impeller/display_list/dl_dispatcher.cc @@ -731,7 +731,7 @@ void DlDispatcher::clipRRect(const SkRRect& rrect, ClipOp clip_op, bool is_aa) { ToClipOperation(clip_op)); } else if (rrect.isSimple()) { canvas_.ClipRRect(skia_conversions::ToRect(rrect.rect()), - skia_conversions::ToPoint(rrect.getSimpleRadii()), + skia_conversions::ToSize(rrect.getSimpleRadii()), ToClipOperation(clip_op)); } else { canvas_.ClipPath(skia_conversions::ToPath(rrect), ToClipOperation(clip_op)); @@ -782,8 +782,7 @@ void DlDispatcher::drawCircle(const SkPoint& center, SkScalar radius) { void DlDispatcher::drawRRect(const SkRRect& rrect) { if (rrect.isSimple()) { canvas_.DrawRRect(skia_conversions::ToRect(rrect.rect()), - skia_conversions::ToPoint(rrect.getSimpleRadii()), - paint_); + skia_conversions::ToSize(rrect.getSimpleRadii()), paint_); } else { canvas_.DrawPath(skia_conversions::ToPath(rrect), paint_); } @@ -817,7 +816,7 @@ void DlDispatcher::SimplifyOrDrawPath(CanvasType& canvas, SkRRect rrect; if (path.isRRect(&rrect) && rrect.isSimple()) { canvas.DrawRRect(skia_conversions::ToRect(rrect.rect()), - skia_conversions::ToPoint(rrect.getSimpleRadii()), paint); + skia_conversions::ToSize(rrect.getSimpleRadii()), paint); return; } diff --git a/impeller/display_list/skia_conversions.cc b/impeller/display_list/skia_conversions.cc index a0b5eaf0ddd78..f356e9973a57a 100644 --- a/impeller/display_list/skia_conversions.cc +++ b/impeller/display_list/skia_conversions.cc @@ -141,6 +141,10 @@ Point ToPoint(const SkPoint& point) { return Point::MakeXY(point.fX, point.fY); } +Size ToSize(const SkPoint& point) { + return Size(point.fX, point.fY); +} + Color ToColor(const flutter::DlColor& color) { return { static_cast(color.getRedF()), // diff --git a/impeller/display_list/skia_conversions.h b/impeller/display_list/skia_conversions.h index 4eed60fb124d2..6b02c8b609e5e 100644 --- a/impeller/display_list/skia_conversions.h +++ b/impeller/display_list/skia_conversions.h @@ -33,6 +33,8 @@ std::vector ToPoints(const SkPoint points[], int count); Point ToPoint(const SkPoint& point); +Size ToSize(const SkPoint& point); + Color ToColor(const flutter::DlColor& color); std::vector ToRSXForms(const SkRSXform xform[], int count); diff --git a/impeller/display_list/skia_conversions_unittests.cc b/impeller/display_list/skia_conversions_unittests.cc index eaf1f571ab7f5..e31005e322c9f 100644 --- a/impeller/display_list/skia_conversions_unittests.cc +++ b/impeller/display_list/skia_conversions_unittests.cc @@ -11,6 +11,24 @@ namespace impeller { namespace testing { +TEST(SkiaConversionsTest, SkPointToPoint) { + for (int x = -100; x < 100; x += 4) { + for (int y = -100; y < 100; y += 4) { + EXPECT_EQ(skia_conversions::ToPoint(SkPoint::Make(x * 0.25f, y * 0.25f)), + Point(x * 0.25f, y * 0.25f)); + } + } +} + +TEST(SkiaConversionsTest, SkPointToSize) { + for (int x = -100; x < 100; x += 4) { + for (int y = -100; y < 100; y += 4) { + EXPECT_EQ(skia_conversions::ToSize(SkPoint::Make(x * 0.25f, y * 0.25f)), + Size(x * 0.25f, y * 0.25f)); + } + } +} + TEST(SkiaConversionsTest, ToColor) { // Create a color with alpha, red, green, and blue values that are all // trivially divisible by 255 so that we can test the conversion results in diff --git a/impeller/entity/BUILD.gn b/impeller/entity/BUILD.gn index 6c3f9ff273dd6..f324e6a1fa133 100644 --- a/impeller/entity/BUILD.gn +++ b/impeller/entity/BUILD.gn @@ -207,6 +207,8 @@ impeller_component("entity") { "geometry/point_field_geometry.h", "geometry/rect_geometry.cc", "geometry/rect_geometry.h", + "geometry/round_rect_geometry.cc", + "geometry/round_rect_geometry.h", "geometry/stroke_path_geometry.cc", "geometry/stroke_path_geometry.h", "geometry/vertices_geometry.cc", diff --git a/impeller/entity/geometry/geometry.cc b/impeller/entity/geometry/geometry.cc index 44f3d91ee9291..a9855033e2769 100644 --- a/impeller/entity/geometry/geometry.cc +++ b/impeller/entity/geometry/geometry.cc @@ -14,6 +14,7 @@ #include "impeller/entity/geometry/line_geometry.h" #include "impeller/entity/geometry/point_field_geometry.h" #include "impeller/entity/geometry/rect_geometry.h" +#include "impeller/entity/geometry/round_rect_geometry.h" #include "impeller/entity/geometry/stroke_path_geometry.h" #include "impeller/geometry/rect.h" @@ -204,6 +205,11 @@ std::shared_ptr Geometry::MakeStrokedCircle(const Point& center, return std::make_shared(center, radius, stroke_width); } +std::shared_ptr Geometry::MakeRoundRect(const Rect& rect, + const Size& radii) { + return std::make_shared(rect, radii); +} + bool Geometry::CoversArea(const Matrix& transform, const Rect& rect) const { return false; } diff --git a/impeller/entity/geometry/geometry.h b/impeller/entity/geometry/geometry.h index 353bd2b5e730b..a58dfec38d677 100644 --- a/impeller/entity/geometry/geometry.h +++ b/impeller/entity/geometry/geometry.h @@ -85,6 +85,9 @@ class Geometry { Scalar radius, Scalar stroke_width); + static std::shared_ptr MakeRoundRect(const Rect& rect, + const Size& radii); + static std::shared_ptr MakePointField(std::vector points, Scalar radius, bool round); diff --git a/impeller/entity/geometry/geometry_unittests.cc b/impeller/entity/geometry/geometry_unittests.cc index 759870fc59bee..170a01ff98e85 100644 --- a/impeller/entity/geometry/geometry_unittests.cc +++ b/impeller/entity/geometry/geometry_unittests.cc @@ -62,5 +62,14 @@ TEST(EntityGeometryTest, LineGeometryCoverage) { } } +TEST(EntityGeometryTest, RoundRectGeometryCoversArea) { + auto geometry = + Geometry::MakeRoundRect(Rect::MakeLTRB(0, 0, 100, 100), Size(20, 20)); + EXPECT_FALSE(geometry->CoversArea({}, Rect::MakeLTRB(15, 15, 85, 85))); + EXPECT_TRUE(geometry->CoversArea({}, Rect::MakeLTRB(20, 20, 80, 80))); + EXPECT_TRUE(geometry->CoversArea({}, Rect::MakeLTRB(30, 1, 70, 99))); + EXPECT_TRUE(geometry->CoversArea({}, Rect::MakeLTRB(1, 30, 99, 70))); +} + } // namespace testing } // namespace impeller diff --git a/impeller/entity/geometry/round_rect_geometry.cc b/impeller/entity/geometry/round_rect_geometry.cc new file mode 100644 index 0000000000000..c6c47d0c54cbe --- /dev/null +++ b/impeller/entity/geometry/round_rect_geometry.cc @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "flutter/impeller/entity/geometry/round_rect_geometry.h" + +#include "flutter/impeller/entity/geometry/line_geometry.h" + +namespace impeller { + +RoundRectGeometry::RoundRectGeometry(const Rect& bounds, const Size& radii) + : bounds_(bounds), radii_(radii) {} + +GeometryResult RoundRectGeometry::GetPositionBuffer( + const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const { + return ComputePositionGeometry(renderer.GetTessellator()->FilledRoundRect( + entity.GetTransform(), bounds_, radii_), + entity, pass); +} + +// |Geometry| +GeometryResult RoundRectGeometry::GetPositionUVBuffer( + Rect texture_coverage, + Matrix effect_transform, + const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const { + return ComputePositionUVGeometry( + renderer.GetTessellator()->FilledRoundRect(entity.GetTransform(), bounds_, + radii_), + texture_coverage.GetNormalizingTransform() * effect_transform, entity, + pass); +} + +GeometryVertexType RoundRectGeometry::GetVertexType() const { + return GeometryVertexType::kPosition; +} + +std::optional RoundRectGeometry::GetCoverage( + const Matrix& transform) const { + return bounds_.TransformBounds(transform); +} + +bool RoundRectGeometry::CoversArea(const Matrix& transform, + const Rect& rect) const { + if (!transform.IsTranslationScaleOnly()) { + return false; + } + bool flat_on_tb = bounds_.GetSize().width > radii_.width * 2; + bool flat_on_lr = bounds_.GetSize().height > radii_.height * 2; + if (!flat_on_tb && !flat_on_lr) { + return false; + } + // We either transform the bounds and delta-transform the radii, + // or we compute the vertical and horizontal bounds and then + // transform each. Either way there are 2 transform operations. + // We could also get a weaker answer by computing just the + // "inner rect" and only doing a coverage analysis on that, + // but this process will produce more culling results. + if (flat_on_tb) { + Rect vertical_bounds = bounds_.Expand(Size{-radii_.width, 0}); + Rect coverage = vertical_bounds.TransformBounds(transform); + if (coverage.Contains(rect)) { + return true; + } + } + if (flat_on_lr) { + Rect horizontal_bounds = bounds_.Expand(Size{0, -radii_.height}); + Rect coverage = horizontal_bounds.TransformBounds(transform); + if (coverage.Contains(rect)) { + return true; + } + } + return false; +} + +bool RoundRectGeometry::IsAxisAlignedRect() const { + return false; +} + +} // namespace impeller diff --git a/impeller/entity/geometry/round_rect_geometry.h b/impeller/entity/geometry/round_rect_geometry.h new file mode 100644 index 0000000000000..36d345fc49941 --- /dev/null +++ b/impeller/entity/geometry/round_rect_geometry.h @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include "impeller/entity/geometry/geometry.h" + +namespace impeller { + +// Geometry class that can generate vertices (with or without texture +// coordinates) for filled ellipses. Generating vertices for a stroked +// ellipse would require a lot more work since the line width must be +// applied perpendicular to the distorted ellipse shape. +class RoundRectGeometry final : public Geometry { + public: + explicit RoundRectGeometry(const Rect& bounds, const Size& radii); + + ~RoundRectGeometry() = default; + + // |Geometry| + bool CoversArea(const Matrix& transform, const Rect& rect) const override; + + // |Geometry| + bool IsAxisAlignedRect() const override; + + private: + // |Geometry| + GeometryResult GetPositionBuffer(const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const override; + + // |Geometry| + GeometryVertexType GetVertexType() const override; + + // |Geometry| + std::optional GetCoverage(const Matrix& transform) const override; + + // |Geometry| + GeometryResult GetPositionUVBuffer(Rect texture_coverage, + Matrix effect_transform, + const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const override; + + const Rect bounds_; + const Size radii_; + + RoundRectGeometry(const RoundRectGeometry&) = delete; + + RoundRectGeometry& operator=(const RoundRectGeometry&) = delete; +}; + +} // namespace impeller diff --git a/impeller/geometry/path_builder.cc b/impeller/geometry/path_builder.cc index da92208247c83..616b165aed2e6 100644 --- a/impeller/geometry/path_builder.cc +++ b/impeller/geometry/path_builder.cc @@ -139,8 +139,8 @@ PathBuilder& PathBuilder::AddRoundedRect(Rect rect, Scalar radius) { : AddRoundedRect(rect, RoundingRadii(radius)); } -PathBuilder& PathBuilder::AddRoundedRect(Rect rect, Point radii) { - return radii.x <= 0 || radii.y <= 0 +PathBuilder& PathBuilder::AddRoundedRect(Rect rect, Size radii) { + return radii.width <= 0 || radii.height <= 0 ? AddRect(rect) : AddRoundedRect(rect, RoundingRadii(radii)); } diff --git a/impeller/geometry/path_builder.h b/impeller/geometry/path_builder.h index 1502944f00bfd..d6c302f16454b 100644 --- a/impeller/geometry/path_builder.h +++ b/impeller/geometry/path_builder.h @@ -132,6 +132,12 @@ class PathBuilder { top_right(radii), bottom_right(radii) {} + explicit RoundingRadii(Size radii) + : top_left(radii), + bottom_left(radii), + top_right(radii), + bottom_right(radii) {} + bool AreAllZero() const { return top_left.IsZero() && // bottom_left.IsZero() && // @@ -142,7 +148,7 @@ class PathBuilder { PathBuilder& AddRoundedRect(Rect rect, RoundingRadii radii); - PathBuilder& AddRoundedRect(Rect rect, Point radii); + PathBuilder& AddRoundedRect(Rect rect, Size radii); PathBuilder& AddRoundedRect(Rect rect, Scalar radius); diff --git a/impeller/geometry/path_unittests.cc b/impeller/geometry/path_unittests.cc index 157bf907a0f5c..6f626f691205d 100644 --- a/impeller/geometry/path_unittests.cc +++ b/impeller/geometry/path_unittests.cc @@ -79,6 +79,17 @@ TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) { ASSERT_TRUE(contour.is_closed); } + { + Path path = + PathBuilder{} + .AddRoundedRect(Rect::MakeXYWH(100, 100, 100, 100), Size(10, 20)) + .TakePath(); + ContourComponent contour; + path.GetContourComponentAtIndex(0, contour); + ASSERT_POINT_NEAR(contour.destination, Point(110, 100)); + ASSERT_TRUE(contour.is_closed); + } + // Open shapes. { Point p(100, 100); diff --git a/impeller/geometry/rect.h b/impeller/geometry/rect.h index 8d0fc5da499c2..20781582b7890 100644 --- a/impeller/geometry/rect.h +++ b/impeller/geometry/rect.h @@ -356,6 +356,15 @@ struct TRect { size.height + amount.y * 2); } + /// @brief Returns a rectangle with expanded edges in all directions. + /// Negative expansion results in shrinking. + constexpr TRect Expand(TSize amount) const { + return TRect(origin.x - amount.width, // + origin.y - amount.height, // + size.width + amount.width * 2, // + size.height + amount.height * 2); + } + /// @brief Returns a new rectangle that represents the projection of the /// source rectangle onto this rectangle. In other words, the source /// rectangle is redefined in terms of the corrdinate space of this diff --git a/impeller/geometry/rect_unittests.cc b/impeller/geometry/rect_unittests.cc index dcac4d8734ade..6cf2e971863dd 100644 --- a/impeller/geometry/rect_unittests.cc +++ b/impeller/geometry/rect_unittests.cc @@ -267,5 +267,25 @@ TEST(RectTest, GetCenter) { EXPECT_EQ(IRect::MakeXYWH(10, 30, 20, 19).GetCenter(), Point(20, 39.5)); } +TEST(RectTest, Expand) { + auto rect = Rect::MakeLTRB(100, 100, 200, 200); + + // Expand(T amount) + EXPECT_EQ(rect.Expand(10), Rect::MakeLTRB(90, 90, 210, 210)); + EXPECT_EQ(rect.Expand(-10), Rect::MakeLTRB(110, 110, 190, 190)); + + // Expand(Point amount) + EXPECT_EQ(rect.Expand(Point{10, 10}), Rect::MakeLTRB(90, 90, 210, 210)); + EXPECT_EQ(rect.Expand(Point{10, -10}), Rect::MakeLTRB(90, 110, 210, 190)); + EXPECT_EQ(rect.Expand(Point{-10, 10}), Rect::MakeLTRB(110, 90, 190, 210)); + EXPECT_EQ(rect.Expand(Point{-10, -10}), Rect::MakeLTRB(110, 110, 190, 190)); + + // Expand(Size amount) + EXPECT_EQ(rect.Expand(Size{10, 10}), Rect::MakeLTRB(90, 90, 210, 210)); + EXPECT_EQ(rect.Expand(Size{10, -10}), Rect::MakeLTRB(90, 110, 210, 190)); + EXPECT_EQ(rect.Expand(Size{-10, 10}), Rect::MakeLTRB(110, 90, 190, 210)); + EXPECT_EQ(rect.Expand(Size{-10, -10}), Rect::MakeLTRB(110, 110, 190, 190)); +} + } // namespace testing } // namespace impeller diff --git a/impeller/geometry/size.h b/impeller/geometry/size.h index d8f26994cc371..9a40dc9a0cd72 100644 --- a/impeller/geometry/size.h +++ b/impeller/geometry/size.h @@ -68,6 +68,8 @@ struct TSize { return {width - s.width, height - s.height}; } + constexpr TSize operator-() const { return {-width, -height}; } + constexpr TSize Min(const TSize& o) const { return { std::min(width, o.width), diff --git a/impeller/geometry/size_unittests.cc b/impeller/geometry/size_unittests.cc index e0857f0190fa9..9c1a65eebc00e 100644 --- a/impeller/geometry/size_unittests.cc +++ b/impeller/geometry/size_unittests.cc @@ -74,5 +74,12 @@ TEST(SizeTest, MaxDimension) { EXPECT_EQ(ISize(21, 20).MaxDimension(), 21); } +TEST(SizeTest, NegationOperator) { + EXPECT_EQ(-Size(10, 20), Size(-10, -20)); + EXPECT_EQ(-Size(-10, 20), Size(10, -20)); + EXPECT_EQ(-Size(10, -20), Size(-10, 20)); + EXPECT_EQ(-Size(-10, -20), Size(10, 20)); +} + } // namespace testing } // namespace impeller diff --git a/impeller/tessellator/tessellator.cc b/impeller/tessellator/tessellator.cc index 4a89f42bd6d3f..35685c3f34479 100644 --- a/impeller/tessellator/tessellator.cc +++ b/impeller/tessellator/tessellator.cc @@ -468,6 +468,34 @@ EllipticalVertexGenerator Tessellator::FilledEllipse( }); } +EllipticalVertexGenerator Tessellator::FilledRoundRect( + const Matrix& view_transform, + const Rect& bounds, + const Size& radii) { + if (radii.width * 2 < bounds.GetSize().width && + radii.height * 2 < bounds.GetSize().height) { + auto max_radius = radii.MaxDimension(); + auto divisions = ComputeQuadrantDivisions( + view_transform.GetMaxBasisLength() * max_radius); + auto upper_left = bounds.GetLeftTop() + radii; + auto lower_right = bounds.GetRightBottom() - radii; + return EllipticalVertexGenerator(Tessellator::GenerateFilledRoundRect, + GetTrigsForDivisions(divisions), + PrimitiveType::kTriangleStrip, 4, + { + .reference_centers = + { + upper_left, + lower_right, + }, + .radii = radii, + .half_width = -1.0f, + }); + } else { + return FilledEllipse(view_transform, bounds); + } +} + void Tessellator::GenerateFilledCircle( const Trigs& trigs, const EllipticalVertexGenerator::Data& data, @@ -615,4 +643,35 @@ void Tessellator::GenerateFilledEllipse( } } +void Tessellator::GenerateFilledRoundRect( + const Trigs& trigs, + const EllipticalVertexGenerator::Data& data, + const TessellatedVertexProc& proc) { + Scalar left = data.reference_centers[0].x; + Scalar top = data.reference_centers[0].y; + Scalar right = data.reference_centers[1].x; + Scalar bottom = data.reference_centers[1].y; + auto radii = data.radii; + + FML_DCHECK(data.half_width < 0); + + // Quadrant 1 connecting with Quadrant 4: + for (auto& trig : trigs) { + auto offset = trig * radii; + proc({left - offset.x, bottom + offset.y}); + proc({left - offset.x, top - offset.y}); + } + + // The second half of the round rect should be iterated in reverse, but + // we can instead iterate forward and swap the x/y values of the + // offset as the angles should be symmetric and thus should generate + // symmetrically reversed trig vectors. + // Quadrant 2 connecting with Quadrant 2: + for (auto& trig : trigs) { + auto offset = Point(trig.sin * radii.width, trig.cos * radii.height); + proc({right + offset.x, bottom + offset.y}); + proc({right + offset.x, top - offset.y}); + } +} + } // namespace impeller diff --git a/impeller/tessellator/tessellator.h b/impeller/tessellator/tessellator.h index 686c8d3f25c53..b283c6c9cbd22 100644 --- a/impeller/tessellator/tessellator.h +++ b/impeller/tessellator/tessellator.h @@ -280,6 +280,19 @@ class Tessellator { EllipticalVertexGenerator FilledEllipse(const Matrix& view_transform, const Rect& bounds); + /// @brief Create a |VertexGenerator| that can produce vertices for + /// a filled round rect within the given bounds and corner radii + /// with enough polygon sub-divisions to provide reasonable + /// fidelity when viewed under the given view transform. + /// + /// Note that the view transform is only used to choose the + /// number of sample points to use per quarter circle and the + /// returned points are not transformed by it, instead they are + /// relative to the coordinate space of the bounds. + EllipticalVertexGenerator FilledRoundRect(const Matrix& view_transform, + const Rect& bounds, + const Size& radii); + private: /// Used for polyline generation. std::unique_ptr> point_buffer_; @@ -309,6 +322,11 @@ class Tessellator { const EllipticalVertexGenerator::Data& data, const TessellatedVertexProc& proc); + static void GenerateFilledRoundRect( + const Trigs& trigs, + const EllipticalVertexGenerator::Data& data, + const TessellatedVertexProc& proc); + Tessellator(const Tessellator&) = delete; Tessellator& operator=(const Tessellator&) = delete;