[Impeller] Direct tessellation of simple filled round rects (flutter/engine#48919)

Simple round rects with the same width and height at each corner are tessellated directly for fill operations, but not yet for stroke operations.
This commit is contained in:
Jim Graham 2023-12-12 14:23:00 -08:00 committed by GitHub
parent 181ced849c
commit 06f5a18499
24 changed files with 423 additions and 29 deletions

View File

@ -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

View File

@ -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<Color> 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<Scalar> 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<Picture> {
@ -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),

View File

@ -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<Rect> inner_rect = (flat_on_LR && flat_on_TB)
? rect.Expand(-corner_radii)
: std::make_optional<Rect>();
@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View File

@ -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<Scalar>(color.getRedF()), //

View File

@ -33,6 +33,8 @@ std::vector<Point> ToPoints(const SkPoint points[], int count);
Point ToPoint(const SkPoint& point);
Size ToSize(const SkPoint& point);
Color ToColor(const flutter::DlColor& color);
std::vector<Matrix> ToRSXForms(const SkRSXform xform[], int count);

View File

@ -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

View File

@ -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",

View File

@ -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> Geometry::MakeStrokedCircle(const Point& center,
return std::make_shared<CircleGeometry>(center, radius, stroke_width);
}
std::shared_ptr<Geometry> Geometry::MakeRoundRect(const Rect& rect,
const Size& radii) {
return std::make_shared<RoundRectGeometry>(rect, radii);
}
bool Geometry::CoversArea(const Matrix& transform, const Rect& rect) const {
return false;
}

View File

@ -85,6 +85,9 @@ class Geometry {
Scalar radius,
Scalar stroke_width);
static std::shared_ptr<Geometry> MakeRoundRect(const Rect& rect,
const Size& radii);
static std::shared_ptr<Geometry> MakePointField(std::vector<Point> points,
Scalar radius,
bool round);

View File

@ -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

View File

@ -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 <algorithm>
#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<Rect> 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

View File

@ -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<Rect> 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

View File

@ -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));
}

View File

@ -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);

View File

@ -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);

View File

@ -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<T> Expand(TSize<T> 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

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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<std::vector<Point>> 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;