Handle all corner cases for stroke geometry, add bevel join & cap/join enums (flutter/engine#35)

This commit is contained in:
Brandon DeRosier 2022-02-25 13:42:50 -08:00 committed by Dan Field
parent 0b707db40b
commit 106ef2d16e
3 changed files with 183 additions and 48 deletions

View File

@ -293,6 +293,30 @@ const Color& SolidStrokeContents::GetColor() const {
return color_;
}
static void CreateCap(
VertexBufferBuilder<SolidStrokeVertexShader::PerVertexData>& vtx_builder,
const Point& position,
const Point& normal) {}
static void CreateJoin(
VertexBufferBuilder<SolidStrokeVertexShader::PerVertexData>& vtx_builder,
const Point& position,
const Point& start_normal,
const Point& end_normal) {
SolidStrokeVertexShader::PerVertexData vtx;
vtx.vertex_position = position;
vtx.pen_down = 1.0;
vtx.vertex_normal = {};
vtx_builder.AppendVertex(vtx);
// A simple bevel join to start with.
Scalar dir = start_normal.Cross(end_normal) > 0 ? -1 : 1;
vtx.vertex_normal = start_normal * dir;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = end_normal * dir;
vtx_builder.AppendVertex(vtx);
}
static VertexBuffer CreateSolidStrokeVertices(const Path& path,
HostBuffer& buffer) {
using VS = SolidStrokeVertexShader;
@ -300,44 +324,90 @@ static VertexBuffer CreateSolidStrokeVertices(const Path& path,
VertexBufferBuilder<VS::PerVertexData> vtx_builder;
auto polyline = path.CreatePolyline();
for (size_t i = 0, polyline_size = polyline.points.size(); i < polyline_size;
i++) {
const auto is_last_point = i == polyline_size - 1;
size_t point_i = 0;
if (polyline.points.size() < 2) {
return {}; // Nothing to render.
}
const auto& p1 = polyline.points[i];
const auto& p2 =
is_last_point ? polyline.points[i - 1] : polyline.points[i + 1];
VS::PerVertexData vtx;
const auto diff = p2 - p1;
// Cursor state.
Point direction;
Point normal;
Point previous_normal; // Used for computing joins.
auto compute_normals = [&](size_t point_i) {
previous_normal = normal;
direction =
(polyline.points[point_i] - polyline.points[point_i - 1]).Normalize();
normal = {-direction.y, direction.x};
};
compute_normals(1);
const Scalar direction = is_last_point ? -1.0 : 1.0;
// Break state.
auto breaks_it = polyline.breaks.begin();
size_t break_end =
breaks_it != polyline.breaks.end() ? *breaks_it : polyline.points.size();
const auto normal =
Point{-diff.y * direction, diff.x * direction}.Normalize();
while (point_i < polyline.points.size()) {
if (point_i > 0) {
compute_normals(point_i);
VS::PerVertexData vtx;
vtx.vertex_position = p1;
auto pen_down =
polyline.breaks.find(i) == polyline.breaks.end() ? 1.0 : 0.0;
vtx.vertex_normal = normal;
vtx.pen_down = pen_down;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = -normal;
vtx.pen_down = pen_down;
vtx_builder.AppendVertex(vtx);
// Put the pen down again for the next contour.
if (!pen_down) {
vtx.vertex_normal = normal;
vtx.pen_down = 1.0;
// This branch only executes when we've just finished drawing a contour
// and are switching to a new one.
// We're drawing a triangle strip, so we need to "pick up the pen" by
// appending transparent vertices between the end of the previous contour
// and the beginning of the new contour.
vtx.vertex_position = polyline.points[point_i - 1];
vtx.vertex_normal = {};
vtx.pen_down = 0.0;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = -normal;
vtx.pen_down = 1.0;
vtx.vertex_position = polyline.points[point_i];
vtx_builder.AppendVertex(vtx);
}
// Generate start cap.
CreateCap(vtx_builder, polyline.points[point_i], -direction);
// Generate contour geometry.
size_t contour_point_i = 0;
while (point_i < break_end) {
if (contour_point_i > 0) {
if (contour_point_i > 1) {
// Generate join from the previous line to the current line.
CreateJoin(vtx_builder, polyline.points[point_i - 1], previous_normal,
normal);
} else {
compute_normals(point_i);
}
// Generate line rect.
vtx.vertex_position = polyline.points[point_i - 1];
vtx.pen_down = 1.0;
vtx.vertex_normal = normal;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = -normal;
vtx_builder.AppendVertex(vtx);
vtx.vertex_position = polyline.points[point_i];
vtx.vertex_normal = normal;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = -normal;
vtx_builder.AppendVertex(vtx);
compute_normals(point_i + 1);
}
++contour_point_i;
++point_i;
}
// Generate end cap.
CreateCap(vtx_builder, polyline.points[point_i - 1], -direction);
if (break_end < polyline.points.size()) {
++breaks_it;
break_end = breaks_it != polyline.breaks.end() ? *breaks_it
: polyline.points.size();
}
}
return vtx_builder.CreateVertexBuffer(buffer);
@ -384,6 +454,30 @@ Scalar SolidStrokeContents::GetStrokeSize() const {
return stroke_size_;
}
void SolidStrokeContents::SetStrokeMiter(Scalar miter) {
miter_ = miter;
}
Scalar SolidStrokeContents::GetStrokeMiter(Scalar miter) {
return miter_;
}
void SolidStrokeContents::SetStrokeCap(Cap cap) {
cap_ = cap;
}
SolidStrokeContents::Cap SolidStrokeContents::GetStrokeCap() {
return cap_;
}
void SolidStrokeContents::SetStrokeJoin(Join join) {
join_ = join;
}
SolidStrokeContents::Join SolidStrokeContents::GetStrokeJoin() {
return join_;
}
/*******************************************************************************
******* ClipContents
******************************************************************************/

View File

@ -113,6 +113,20 @@ class TextureContents final : public Contents {
class SolidStrokeContents final : public Contents {
public:
enum class Cap {
kButt,
kRound,
kSquare,
kLast,
};
enum class Join {
kMiter,
kRound,
kBevel,
kLast,
};
SolidStrokeContents();
~SolidStrokeContents() override;
@ -125,6 +139,18 @@ class SolidStrokeContents final : public Contents {
Scalar GetStrokeSize() const;
void SetStrokeMiter(Scalar miter);
Scalar GetStrokeMiter(Scalar miter);
void SetStrokeCap(Cap cap);
Cap GetStrokeCap();
void SetStrokeJoin(Join join);
Join GetStrokeJoin();
// |Contents|
bool Render(const ContentContext& renderer,
const Entity& entity,
@ -133,6 +159,9 @@ class SolidStrokeContents final : public Contents {
private:
Color color_;
Scalar stroke_size_ = 0.0;
Scalar miter_ = 0.0;
Cap cap_ = Cap::kButt;
Join join_ = Join::kMiter;
FML_DISALLOW_COPY_AND_ASSIGN(SolidStrokeContents);
};

View File

@ -7,6 +7,7 @@
#include "impeller/entity/entity_playground.h"
#include "impeller/geometry/path_builder.h"
#include "impeller/playground/playground.h"
#include "impeller/playground/widgets.h"
namespace impeller {
namespace testing {
@ -45,25 +46,36 @@ TEST_F(EntityTest, ThreeStrokesInOnePath) {
}
TEST_F(EntityTest, TriangleInsideASquare) {
Path path = PathBuilder{}
.MoveTo({10, 10})
.LineTo({210, 10})
.LineTo({210, 210})
.LineTo({10, 210})
.Close()
.MoveTo({50, 50})
.LineTo({100, 50})
.LineTo({50, 150})
.Close()
.TakePath();
auto callback = [&](ContentContext& context, RenderPass& pass) {
Point a = IMPELLER_PLAYGROUND_POINT(Point(10, 10), 20, Color::White());
Point b = IMPELLER_PLAYGROUND_POINT(Point(210, 10), 20, Color::White());
Point c = IMPELLER_PLAYGROUND_POINT(Point(210, 210), 20, Color::White());
Point d = IMPELLER_PLAYGROUND_POINT(Point(10, 210), 20, Color::White());
Point e = IMPELLER_PLAYGROUND_POINT(Point(50, 50), 20, Color::White());
Point f = IMPELLER_PLAYGROUND_POINT(Point(100, 50), 20, Color::White());
Point g = IMPELLER_PLAYGROUND_POINT(Point(50, 150), 20, Color::White());
Path path = PathBuilder{}
.MoveTo(a)
.LineTo(b)
.LineTo(c)
.LineTo(d)
.Close()
.MoveTo(e)
.LineTo(f)
.LineTo(g)
.Close()
.TakePath();
Entity entity;
entity.SetPath(path);
auto contents = std::make_unique<SolidStrokeContents>();
contents->SetColor(Color::Red());
contents->SetStrokeSize(5.0);
entity.SetContents(std::move(contents));
ASSERT_TRUE(OpenPlaygroundHere(entity));
Entity entity;
entity.SetPath(path);
auto contents = std::make_unique<SolidStrokeContents>();
contents->SetColor(Color::Red());
contents->SetStrokeSize(20.0);
entity.SetContents(std::move(contents));
return entity.Render(context, pass);
};
ASSERT_TRUE(OpenPlaygroundHere(callback));
}
TEST_F(EntityTest, CubicCurveTest) {