diff --git a/engine/src/flutter/display_list/geometry/dl_geometry_types.h b/engine/src/flutter/display_list/geometry/dl_geometry_types.h index 30637de5a6..2e645149c7 100644 --- a/engine/src/flutter/display_list/geometry/dl_geometry_types.h +++ b/engine/src/flutter/display_list/geometry/dl_geometry_types.h @@ -139,6 +139,10 @@ inline const SkPoint* ToSkPoints(const DlPoint* points) { return points == nullptr ? nullptr : reinterpret_cast(points); } +inline SkPoint* ToSkPoints(DlPoint* points) { + return points == nullptr ? nullptr : reinterpret_cast(points); +} + inline const SkRect& ToSkRect(const DlRect& rect) { return *reinterpret_cast(&rect); } diff --git a/engine/src/flutter/display_list/geometry/dl_path.cc b/engine/src/flutter/display_list/geometry/dl_path.cc index 695895e14f..1b28af1181 100644 --- a/engine/src/flutter/display_list/geometry/dl_path.cc +++ b/engine/src/flutter/display_list/geometry/dl_path.cc @@ -265,39 +265,46 @@ SkPath DlPath::ConvertToSkiaPath(const Path& path, const DlPoint& shift) { } }; - size_t count = path.GetComponentCount(); - for (size_t i = 0; i < count; i++) { - switch (path.GetComponentTypeAtIndex(i)) { + for (auto it = path.begin(), end = path.end(); it != end; ++it) { + switch (it.type()) { case ComponentType::kContour: { - impeller::ContourComponent contour; - path.GetContourComponentAtIndex(i, contour); + const impeller::ContourComponent* contour = it.contour(); + FML_DCHECK(contour != nullptr); if (subpath_needs_close) { sk_path.close(); } - pending_moveto = contour.destination; - subpath_needs_close = contour.IsClosed(); + pending_moveto = contour->destination; + subpath_needs_close = contour->IsClosed(); break; } case ComponentType::kLinear: { - impeller::LinearPathComponent linear; - path.GetLinearComponentAtIndex(i, linear); + const impeller::LinearPathComponent* linear = it.linear(); + FML_DCHECK(linear != nullptr); resolve_moveto(); - sk_path.lineTo(ToSkPoint(linear.p2)); + sk_path.lineTo(ToSkPoint(linear->p2)); break; } case ComponentType::kQuadratic: { - impeller::QuadraticPathComponent quadratic; - path.GetQuadraticComponentAtIndex(i, quadratic); + const impeller::QuadraticPathComponent* quadratic = it.quadratic(); + FML_DCHECK(quadratic != nullptr); resolve_moveto(); - sk_path.quadTo(ToSkPoint(quadratic.cp), ToSkPoint(quadratic.p2)); + sk_path.quadTo(ToSkPoint(quadratic->cp), ToSkPoint(quadratic->p2)); + break; + } + case ComponentType::kConic: { + const impeller::ConicPathComponent* conic = it.conic(); + FML_DCHECK(conic != nullptr); + resolve_moveto(); + sk_path.conicTo(ToSkPoint(conic->cp), ToSkPoint(conic->p2), + conic->weight.x); break; } case ComponentType::kCubic: { - impeller::CubicPathComponent cubic; - path.GetCubicComponentAtIndex(i, cubic); + const impeller::CubicPathComponent* cubic = it.cubic(); + FML_DCHECK(cubic != nullptr); resolve_moveto(); - sk_path.cubicTo(ToSkPoint(cubic.cp1), ToSkPoint(cubic.cp2), - ToSkPoint(cubic.p2)); + sk_path.cubicTo(ToSkPoint(cubic->cp1), ToSkPoint(cubic->cp2), + ToSkPoint(cubic->p2)); break; } } @@ -339,27 +346,26 @@ Path DlPath::ConvertToImpellerPath(const SkPath& path, const DlPoint& shift) { builder.QuadraticCurveTo(ToDlPoint(data.points[1]), ToDlPoint(data.points[2])); break; - case SkPath::kConic_Verb: { - constexpr auto kPow2 = 1; // Only works for sweeps up to 90 degrees. - constexpr auto kQuadCount = 1 + (2 * (1 << kPow2)); - SkPoint points[kQuadCount]; - const auto curve_count = - SkPath::ConvertConicToQuads(data.points[0], // - data.points[1], // - data.points[2], // - iterator.conicWeight(), // - points, // - kPow2 // - ); - - for (int curve_index = 0, point_index = 0; // - curve_index < curve_count; // - curve_index++, point_index += 2 // - ) { - builder.QuadraticCurveTo(ToDlPoint(points[point_index + 1]), - ToDlPoint(points[point_index + 2])); + case SkPath::kConic_Verb: + // We might eventually have conic conversion math that deals with + // degenerate conics gracefully (or just handle them directly), + // but until then, we will detect and ignore them. + if (data.points[0] != data.points[1]) { + if (data.points[1] != data.points[2]) { + std::array points; + impeller::ConicPathComponent conic( + ToDlPoint(data.points[0]), ToDlPoint(data.points[1]), + ToDlPoint(data.points[2]), iterator.conicWeight()); + conic.SubdivideToQuadraticPoints(points); + builder.QuadraticCurveTo(points[1], points[2]); + builder.QuadraticCurveTo(points[3], points[4]); + } else { + builder.LineTo(ToDlPoint(data.points[1])); + } + } else if (data.points[1] != data.points[2]) { + builder.LineTo(ToDlPoint(data.points[2])); } - } break; + break; case SkPath::kCubic_Verb: builder.CubicCurveTo(ToDlPoint(data.points[1]), ToDlPoint(data.points[2]), diff --git a/engine/src/flutter/display_list/skia/dl_sk_conversions_unittests.cc b/engine/src/flutter/display_list/skia/dl_sk_conversions_unittests.cc index 1cf50f716f..c69ba297cc 100644 --- a/engine/src/flutter/display_list/skia/dl_sk_conversions_unittests.cc +++ b/engine/src/flutter/display_list/skia/dl_sk_conversions_unittests.cc @@ -11,6 +11,7 @@ #include "flutter/display_list/effects/dl_color_sources.h" #include "flutter/display_list/effects/dl_image_filters.h" #include "flutter/display_list/skia/dl_sk_conversions.h" +#include "flutter/impeller/geometry/path_component.h" #include "gtest/gtest.h" #include "third_party/skia/include/core/SkColorSpace.h" #include "third_party/skia/include/core/SkSamplingOptions.h" @@ -372,5 +373,92 @@ TEST(DisplayListSkConversions, ToSkRSTransform) { } } +// This tests the new conic subdivision code in the Impeller conic path +// component object vs the code we used to rely on inside Skia +TEST(DisplayListSkConversions, ConicToQuads) { + SkScalar weights[4] = { + 0.02f, + 0.5f, + SK_ScalarSqrt2 * 0.5f, + 1.0f, + }; + + for (SkScalar weight : weights) { + SkPoint sk_points[5]; + int ncurves = SkPath::ConvertConicToQuads( + SkPoint::Make(10, 10), SkPoint::Make(20, 10), SkPoint::Make(20, 20), + weight, sk_points, 1); + ASSERT_EQ(ncurves, 2) << "weight: " << weight; + + std::array i_points; + impeller::ConicPathComponent i_conic(DlPoint(10, 10), DlPoint(20, 10), + DlPoint(20, 20), weight); + i_conic.SubdivideToQuadraticPoints(i_points); + + for (int i = 0; i < 5; i++) { + EXPECT_FLOAT_EQ(sk_points[i].fX, i_points[i].x) + << "weight: " << weight << "point[" << i << "].x"; + EXPECT_FLOAT_EQ(sk_points[i].fY, i_points[i].y) + << "weight: " << weight << "point[" << i << "].y"; + } + } +} + +// This tests the new conic subdivision code in the Impeller conic path +// component object vs the code we used to rely on inside Skia +TEST(DisplayListSkConversions, ConicPathToQuads) { + // If we execute conicTo with a weight of exactly 1.0, SkPath will turn + // it into a quadTo, so we avoid that by using 0.999 + SkScalar weights[4] = { + 0.02f, + 0.5f, + SK_ScalarSqrt2 * 0.5f, + 1.0f - kEhCloseEnough, + }; + + for (SkScalar weight : weights) { + SkPath sk_path; + sk_path.moveTo(10, 10); + sk_path.conicTo(20, 10, 20, 20, weight); + + DlPath dl_path(sk_path); + impeller::Path i_path = dl_path.GetPath(); + + auto it = i_path.begin(); + ASSERT_EQ(it.type(), impeller::Path::ComponentType::kContour); + ++it; + + ASSERT_EQ(it.type(), impeller::Path::ComponentType::kQuadratic); + auto quad1 = it.quadratic(); + ASSERT_NE(quad1, nullptr); + ++it; + + ASSERT_EQ(it.type(), impeller::Path::ComponentType::kQuadratic); + auto quad2 = it.quadratic(); + ASSERT_NE(quad2, nullptr); + ++it; + + SkPoint sk_points[5]; + int ncurves = SkPath::ConvertConicToQuads( + SkPoint::Make(10, 10), SkPoint::Make(20, 10), SkPoint::Make(20, 20), + weight, sk_points, 1); + ASSERT_EQ(ncurves, 2); + + EXPECT_FLOAT_EQ(sk_points[0].fX, quad1->p1.x) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[0].fY, quad1->p1.y) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[1].fX, quad1->cp.x) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[1].fY, quad1->cp.y) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[2].fX, quad1->p2.x) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[2].fY, quad1->p2.y) << "weight: " << weight; + + EXPECT_FLOAT_EQ(sk_points[2].fX, quad2->p1.x) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[2].fY, quad2->p1.y) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[3].fX, quad2->cp.x) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[3].fY, quad2->cp.y) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[4].fX, quad2->p2.x) << "weight: " << weight; + EXPECT_FLOAT_EQ(sk_points[4].fY, quad2->p2.y) << "weight: " << weight; + } +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/impeller/geometry/constants.h b/engine/src/flutter/impeller/geometry/constants.h index 2fa8e037db..3630f64ad3 100644 --- a/engine/src/flutter/impeller/geometry/constants.h +++ b/engine/src/flutter/impeller/geometry/constants.h @@ -46,8 +46,9 @@ constexpr float k2OverSqrtPi = 1.12837916709551257390f; // sqrt(2) constexpr float kSqrt2 = 1.41421356237309504880f; -// 1/sqrt(2) +// sqrt(2) / 2 == 1/sqrt(2) constexpr float k1OverSqrt2 = 0.70710678118654752440f; +constexpr float kSqrt2Over2 = 0.70710678118654752440f; // phi constexpr float kPhi = 1.61803398874989484820f; diff --git a/engine/src/flutter/impeller/geometry/path.cc b/engine/src/flutter/impeller/geometry/path.cc index 9920f12a8f..385fe0b90a 100644 --- a/engine/src/flutter/impeller/geometry/path.cc +++ b/engine/src/flutter/impeller/geometry/path.cc @@ -19,6 +19,64 @@ Path::Path(Data data) : data_(std::make_shared(std::move(data))) {} Path::~Path() = default; +Path::ComponentType Path::ComponentIterator::type() const { + return path_.data_->components[component_index_]; +} + +#define CHECK_COMPONENT(type) \ + (component_index_ < path_.data_->components.size() && \ + path_.data_->components[component_index_] == type && \ + storage_offset_ + VerbToOffset(type) <= path_.data_->points.size()) + +const LinearPathComponent* Path::ComponentIterator::linear() const { + if (!CHECK_COMPONENT(Path::ComponentType::kLinear)) { + return nullptr; + } + const Point* points = &(path_.data_->points[storage_offset_]); + return reinterpret_cast(points); +} + +const QuadraticPathComponent* Path::ComponentIterator::quadratic() const { + if (!CHECK_COMPONENT(Path::ComponentType::kQuadratic)) { + return nullptr; + } + const Point* points = &(path_.data_->points[storage_offset_]); + return reinterpret_cast(points); +} + +const ConicPathComponent* Path::ComponentIterator::conic() const { + if (!CHECK_COMPONENT(Path::ComponentType::kConic)) { + return nullptr; + } + const Point* points = &(path_.data_->points[storage_offset_]); + return reinterpret_cast(points); +} + +const CubicPathComponent* Path::ComponentIterator::cubic() const { + if (!CHECK_COMPONENT(Path::ComponentType::kCubic)) { + return nullptr; + } + const Point* points = &(path_.data_->points[storage_offset_]); + return reinterpret_cast(points); +} + +const ContourComponent* Path::ComponentIterator::contour() const { + if (!CHECK_COMPONENT(Path::ComponentType::kContour)) { + return nullptr; + } + const Point* points = &(path_.data_->points[storage_offset_]); + return reinterpret_cast(points); +} + +Path::ComponentIterator& Path::ComponentIterator::operator++() { + auto components = path_.data_->components; + if (component_index_ < components.size()) { + storage_offset_ += VerbToOffset(path_.data_->components[component_index_]); + component_index_++; + } + return *this; +} + std::tuple Path::Polyline::GetContourPointBounds( size_t contour_index) const { if (contour_index >= contours.size()) { @@ -87,6 +145,13 @@ std::pair Path::CountStorage(Scalar scale) const { points += quad->CountLinearPathComponents(scale); break; } + case ComponentType::kConic: { + const ConicPathComponent* conic = + reinterpret_cast( + &path_points[storage_offset]); + points += conic->CountLinearPathComponents(scale); + break; + } case ComponentType::kCubic: { const CubicPathComponent* cubic = reinterpret_cast( @@ -135,6 +200,17 @@ void Path::WritePolyline(Scalar scale, VertexWriter& writer) const { quad->ToLinearPathComponents(scale, writer); break; } + case ComponentType::kConic: { + const ConicPathComponent* conic = + reinterpret_cast( + &path_points[storage_offset]); + if (first_point) { + writer.Write(conic->p1); + first_point = false; + } + conic->ToLinearPathComponents(scale, writer); + break; + } case ComponentType::kCubic: { const CubicPathComponent* cubic = reinterpret_cast( @@ -170,86 +246,13 @@ void Path::WritePolyline(Scalar scale, VertexWriter& writer) const { } } -Path::ComponentType Path::GetComponentTypeAtIndex(size_t index) const { - auto& components = data_->components; - return components[index]; +Path::ComponentIterator Path::begin() const { + return ComponentIterator(*this, 0u, 0u); } -bool Path::GetLinearComponentAtIndex(size_t index, - LinearPathComponent& linear) const { - auto& components = data_->components; - if (index >= components.size() || - components[index] != ComponentType::kLinear) { - return false; - } - - size_t storage_offset = 0u; - for (auto i = 0u; i < index; i++) { - storage_offset += VerbToOffset(components[i]); - } - auto& points = data_->points; - linear = - LinearPathComponent(points[storage_offset], points[storage_offset + 1]); - return true; -} - -bool Path::GetQuadraticComponentAtIndex( - size_t index, - QuadraticPathComponent& quadratic) const { - auto& components = data_->components; - if (index >= components.size() || - components[index] != ComponentType::kQuadratic) { - return false; - } - - size_t storage_offset = 0u; - for (auto i = 0u; i < index; i++) { - storage_offset += VerbToOffset(components[i]); - } - auto& points = data_->points; - - quadratic = - QuadraticPathComponent(points[storage_offset], points[storage_offset + 1], - points[storage_offset + 2]); - return true; -} - -bool Path::GetCubicComponentAtIndex(size_t index, - CubicPathComponent& cubic) const { - auto& components = data_->components; - if (index >= components.size() || - components[index] != ComponentType::kCubic) { - return false; - } - - size_t storage_offset = 0u; - for (auto i = 0u; i < index; i++) { - storage_offset += VerbToOffset(components[i]); - } - auto& points = data_->points; - - cubic = CubicPathComponent(points[storage_offset], points[storage_offset + 1], - points[storage_offset + 2], - points[storage_offset + 3]); - return true; -} - -bool Path::GetContourComponentAtIndex(size_t index, - ContourComponent& move) const { - auto& components = data_->components; - if (index >= components.size() || - components[index] != ComponentType::kContour) { - return false; - } - - size_t storage_offset = 0u; - for (auto i = 0u; i < index; i++) { - storage_offset += VerbToOffset(components[i]); - } - auto& points = data_->points; - - move = ContourComponent(points[storage_offset], points[storage_offset + 1]); - return true; +Path::ComponentIterator Path::end() const { + return ComponentIterator(*this, data_->components.size(), + data_->points.size()); } Path::Polyline::Polyline(Path::Polyline::PointBufferPtr point_buffer, @@ -315,6 +318,16 @@ void Path::EndContour( } break; } + case ComponentType::kConic: { + auto* conic = reinterpret_cast( + &path_points[storage_offset]); + auto maybe_end = conic->GetEndDirection(); + if (maybe_end.has_value()) { + contour.end_direction = maybe_end.value(); + return; + } + break; + } case ComponentType::kCubic: { auto* cubic = reinterpret_cast( &path_points[storage_offset]); @@ -377,6 +390,19 @@ Path::Polyline Path::CreatePolyline( } break; } + case ComponentType::kConic: { + poly_components.push_back({ + .component_start_index = polyline.points->size() - 1, + .is_curve = true, + }); + auto* conic = reinterpret_cast( + &path_points[storage_offset]); + conic->AppendPolylinePoints(scale, *polyline.points); + if (!start_direction.has_value()) { + start_direction = conic->GetStartDirection(); + } + break; + } case ComponentType::kCubic: { poly_components.push_back({ .component_start_index = polyline.points->size() - 1, diff --git a/engine/src/flutter/impeller/geometry/path.h b/engine/src/flutter/impeller/geometry/path.h index 93c46f7367..978e9f3559 100644 --- a/engine/src/flutter/impeller/geometry/path.h +++ b/engine/src/flutter/impeller/geometry/path.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -55,16 +56,50 @@ class Path { enum class ComponentType { kLinear, kQuadratic, + kConic, kCubic, kContour, }; + class ComponentIterator { + public: + ComponentType type() const; + + // Return pointer to path component or null if the type is wrong or + // the iterator is past the end of the path. + const LinearPathComponent* linear() const; + const QuadraticPathComponent* quadratic() const; + const ConicPathComponent* conic() const; + const CubicPathComponent* cubic() const; + const ContourComponent* contour() const; + + ComponentIterator& operator++(); + bool operator==(const ComponentIterator& other) const { + return component_index_ == other.component_index_; + } + bool operator!=(const ComponentIterator& other) const { + return component_index_ != other.component_index_; + } + + private: + ComponentIterator(const Path& path, size_t index, size_t offset) + : path_(path), component_index_(index), storage_offset_(offset) {} + + const Path& path_; + size_t component_index_ = 0u; + size_t storage_offset_ = 0u; + + friend class Path; + }; + static constexpr size_t VerbToOffset(Path::ComponentType verb) { switch (verb) { case Path::ComponentType::kLinear: return 2u; case Path::ComponentType::kQuadratic: return 3u; + case Path::ComponentType::kConic: + return 4u; case Path::ComponentType::kCubic: return 4u; case Path::ComponentType::kContour: @@ -157,18 +192,8 @@ class Path { /// @brief Whether the line contains a single contour. bool IsSingleContour() const; - ComponentType GetComponentTypeAtIndex(size_t index) const; - - bool GetLinearComponentAtIndex(size_t index, - LinearPathComponent& linear) const; - - bool GetQuadraticComponentAtIndex(size_t index, - QuadraticPathComponent& quadratic) const; - - bool GetCubicComponentAtIndex(size_t index, CubicPathComponent& cubic) const; - - bool GetContourComponentAtIndex(size_t index, - ContourComponent& contour) const; + ComponentIterator begin() const; + ComponentIterator end() const; /// Callers must provide the scale factor for how this path will be /// transformed. diff --git a/engine/src/flutter/impeller/geometry/path_builder.cc b/engine/src/flutter/impeller/geometry/path_builder.cc index 4923c54371..30e45a85f1 100644 --- a/engine/src/flutter/impeller/geometry/path_builder.cc +++ b/engine/src/flutter/impeller/geometry/path_builder.cc @@ -252,6 +252,17 @@ PathBuilder& PathBuilder::QuadraticCurveTo(Point controlPoint, return *this; } +PathBuilder& PathBuilder::ConicCurveTo(Point controlPoint, + Point point, + Scalar weight, + bool relative) { + point = relative ? current_ + point : point; + controlPoint = relative ? current_ + controlPoint : controlPoint; + AddConicComponent(current_, controlPoint, point, weight); + current_ = point; + return *this; +} + PathBuilder& PathBuilder::SetConvexity(Convexity value) { prototype_.convexity = value; return *this; @@ -269,22 +280,33 @@ PathBuilder& PathBuilder::CubicCurveTo(Point controlPoint1, return *this; } -PathBuilder& PathBuilder::AddQuadraticCurve(Point p1, Point cp, Point p2) { +PathBuilder& PathBuilder::AddQuadraticCurve(const Point& p1, + const Point& cp, + const Point& p2) { MoveTo(p1); AddQuadraticComponent(p1, cp, p2); return *this; } -PathBuilder& PathBuilder::AddCubicCurve(Point p1, - Point cp1, - Point cp2, - Point p2) { +PathBuilder& PathBuilder::AddConicCurve(const Point& p1, + const Point& cp, + const Point& p2, + Scalar weight) { + MoveTo(p1); + AddConicComponent(p1, cp, p2, weight); + return *this; +} + +PathBuilder& PathBuilder::AddCubicCurve(const Point& p1, + const Point& cp1, + const Point& cp2, + const Point& p2) { MoveTo(p1); AddCubicComponent(p1, cp1, cp2, p2); return *this; } -PathBuilder& PathBuilder::AddRect(Rect rect) { +PathBuilder& PathBuilder::AddRect(const Rect& rect) { auto origin = rect.GetOrigin(); auto size = rect.GetSize(); @@ -515,6 +537,28 @@ void PathBuilder::AddQuadraticComponent(const Point& p1, prototype_.bounds.reset(); } +void PathBuilder::AddConicComponent(const Point& p1, + const Point& cp, + const Point& p2, + Scalar weight) { + if (!std::isfinite(weight)) { + AddLinearComponent(p1, cp); + AddLinearComponent(cp, p2); + } else if (weight <= 0) { + AddLinearComponent(p1, p2); + } else if (weight == 1) { + AddQuadraticComponent(p1, cp, p2); + } else { + auto& points = prototype_.points; + points.push_back(p1); + points.push_back(cp); + points.push_back(p2); + points.emplace_back(weight, weight); + prototype_.components.push_back(Path::ComponentType::kConic); + prototype_.bounds.reset(); + } +} + void PathBuilder::AddCubicComponent(const Point& p1, const Point& cp1, const Point& cp2, @@ -684,6 +728,13 @@ PathBuilder& PathBuilder::Shift(Point offset) { quad->p2 += offset; quad->cp += offset; } break; + case Path::ComponentType::kConic: { + auto* conic = + reinterpret_cast(&points[storage_offset]); + conic->p1 += offset; + conic->p2 += offset; + conic->cp += offset; + } break; case Path::ComponentType::kCubic: { auto* cubic = reinterpret_cast(&points[storage_offset]); @@ -767,6 +818,13 @@ std::optional> PathBuilder::GetMinMaxCoveragePoints() clamp(extrema); } break; + case Path::ComponentType::kConic: + for (const auto& extrema : reinterpret_cast( + &points[storage_offset]) + ->Extrema()) { + clamp(extrema); + } + break; case Path::ComponentType::kCubic: for (const auto& extrema : reinterpret_cast( &points[storage_offset]) diff --git a/engine/src/flutter/impeller/geometry/path_builder.h b/engine/src/flutter/impeller/geometry/path_builder.h index 78a570b2f6..3569675884 100644 --- a/engine/src/flutter/impeller/geometry/path_builder.h +++ b/engine/src/flutter/impeller/geometry/path_builder.h @@ -60,6 +60,16 @@ class PathBuilder { Point point, bool relative = false); + /// @brief Insert a conic curve from the current position to `point` using + /// the control point `controlPoint` and the weight `weight`. + /// + /// If `relative` is true the `point` and `controlPoint` are relative to + /// current location. + PathBuilder& ConicCurveTo(Point controlPoint, + Point point, + Scalar weight, + bool relative = false); + /// @brief Insert a cubic curve from the curren position to `point` using the /// control points `controlPoint1` and `controlPoint2`. /// @@ -70,7 +80,7 @@ class PathBuilder { Point point, bool relative = false); - PathBuilder& AddRect(Rect rect); + PathBuilder& AddRect(const Rect& rect); PathBuilder& AddCircle(const Point& center, Scalar radius); @@ -86,11 +96,23 @@ class PathBuilder { /// @brief Move to point `p1`, then insert a quadradic curve from `p1` to `p2` /// with the control point `cp`. - PathBuilder& AddQuadraticCurve(Point p1, Point cp, Point p2); + PathBuilder& AddQuadraticCurve(const Point& p1, + const Point& cp, + const Point& p2); + + /// @brief Move to point `p1`, then insert a conic curve from `p1` to `p2` + /// with the control point `cp` and weight `weight`. + PathBuilder& AddConicCurve(const Point& p1, + const Point& cp, + const Point& p2, + Scalar weight); /// @brief Move to point `p1`, then insert a cubic curve from `p1` to `p2` /// with control points `cp1` and `cp2`. - PathBuilder& AddCubicCurve(Point p1, Point cp1, Point cp2, Point p2); + PathBuilder& AddCubicCurve(const Point& p1, + const Point& cp1, + const Point& cp2, + const Point& p2); /// @brief Transform the existing path segments and contours by the given /// `offset`. @@ -135,6 +157,11 @@ class PathBuilder { void AddQuadraticComponent(const Point& p1, const Point& cp, const Point& p2); + void AddConicComponent(const Point& p1, + const Point& cp, + const Point& p2, + Scalar weight); + void AddCubicComponent(const Point& p1, const Point& cp1, const Point& cp2, diff --git a/engine/src/flutter/impeller/geometry/path_component.cc b/engine/src/flutter/impeller/geometry/path_component.cc index 1637bc9eb4..f61e89a704 100644 --- a/engine/src/flutter/impeller/geometry/path_component.cc +++ b/engine/src/flutter/impeller/geometry/path_component.cc @@ -7,8 +7,9 @@ #include #include -#include "impeller/geometry/scalar.h" -#include "impeller/geometry/wangs_formula.h" +#include "flutter/fml/logging.h" +#include "flutter/impeller/geometry/scalar.h" +#include "flutter/impeller/geometry/wangs_formula.h" namespace impeller { @@ -183,6 +184,20 @@ static inline Scalar QuadraticSolveDerivative(Scalar t, 2 * t * (p2 - p1); } +static inline Scalar ConicSolve(Scalar t, + Scalar p0, + Scalar p1, + Scalar p2, + Scalar w) { + auto u = (1 - t); + auto coefficient_p0 = t * t; + auto coefficient_p1 = 2 * t * u * w; + auto coefficient_p2 = u * u; + + return ((p0 * coefficient_p0 + p1 * coefficient_p1 + p2 * coefficient_p2) / + (coefficient_p0 + coefficient_p1 + coefficient_p2)); +} + static inline Scalar CubicSolve(Scalar t, Scalar p0, Scalar p1, @@ -309,6 +324,115 @@ std::optional QuadraticPathComponent::GetEndDirection() const { return std::nullopt; } +Point ConicPathComponent::Solve(Scalar time) const { + return { + ConicSolve(time, p1.x, cp.x, p2.x, weight.x), // x + ConicSolve(time, p1.y, cp.y, p2.y, weight.y), // y + }; +} + +void ConicPathComponent::AppendPolylinePoints( + Scalar scale_factor, + std::vector& points) const { + for (auto quad : ToQuadraticPathComponents()) { + quad.AppendPolylinePoints(scale_factor, points); + } +} + +void ConicPathComponent::ToLinearPathComponents(Scalar scale, + VertexWriter& writer) const { + for (auto quad : ToQuadraticPathComponents()) { + quad.ToLinearPathComponents(scale, writer); + } +} + +size_t ConicPathComponent::CountLinearPathComponents(Scalar scale) const { + size_t count = 0; + for (auto quad : ToQuadraticPathComponents()) { + count += quad.CountLinearPathComponents(scale); + } + return count; +} + +std::vector ConicPathComponent::Extrema() const { + std::vector points; + for (auto quad : ToQuadraticPathComponents()) { + auto quad_extrema = quad.Extrema(); + points.insert(points.end(), quad_extrema.begin(), quad_extrema.end()); + } + return points; +} + +std::optional ConicPathComponent::GetStartDirection() const { + if (p1 != cp) { + return (p1 - cp).Normalize(); + } + if (p1 != p2) { + return (p1 - p2).Normalize(); + } + return std::nullopt; +} + +std::optional ConicPathComponent::GetEndDirection() const { + if (p2 != cp) { + return (p2 - cp).Normalize(); + } + if (p2 != p1) { + return (p2 - p1).Normalize(); + } + return std::nullopt; +} + +void ConicPathComponent::SubdivideToQuadraticPoints( + std::array& points) const { + FML_DCHECK(weight.IsFinite() && weight.x > 0 && weight.y > 0); + + // Observe that scale will always be smaller than 1 because weight > 0. + const Scalar scale = 1.0f / (1.0f + weight.x); + + // The subdivided control points below are the sums of the following three + // terms. Because the terms are multiplied by something <1, and the resulting + // control points lie within the control points of the original then the + // terms and the sums below will not overflow. Note that weight * scale + // approaches 1 as weight becomes very large. + Point tp1 = p1 * scale; + Point tcp = cp * (weight.x * scale); + Point tp2 = p2 * scale; + + // Calculate the subdivided control points + Point sub_cp1 = tp1 + tcp; + Point sub_cp2 = tcp + tp2; + + // The middle point shared by the 2 sub-divisions, the interpolation of + // the original curve at its halfway point. + Point sub_mid = (tp1 + tcp + tcp + tp2) * 0.5f; + + FML_DCHECK(sub_cp1.IsFinite() && sub_mid.IsFinite() && sub_cp2.IsFinite()); + + points[0] = p1; + points[1] = sub_cp1; + points[2] = sub_mid; + points[3] = sub_cp2; + points[4] = p2; + + // Update w. + // Currently this method only subdivides a single time directly to 2 + // quadratics, but if we eventually want to keep the weights for further + // subdivision, this was the code that did it in Skia: + // sub_w1 = sub_w2 = SkScalarSqrt(SK_ScalarHalf + w * SK_ScalarHalf) +} + +std::array +ConicPathComponent::ToQuadraticPathComponents() const { + std::array points; + SubdivideToQuadraticPoints(points); + + return { + QuadraticPathComponent(points[0], points[1], points[2]), + QuadraticPathComponent(points[2], points[3], points[4]), + }; +} + Point CubicPathComponent::Solve(Scalar time) const { return { CubicSolve(time, p1.x, cp1.x, cp2.x, p2.x), // x diff --git a/engine/src/flutter/impeller/geometry/path_component.h b/engine/src/flutter/impeller/geometry/path_component.h index b558c006a3..f2aeea4699 100644 --- a/engine/src/flutter/impeller/geometry/path_component.h +++ b/engine/src/flutter/impeller/geometry/path_component.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_IMPELLER_GEOMETRY_PATH_COMPONENT_H_ #define FLUTTER_IMPELLER_GEOMETRY_PATH_COMPONENT_H_ +#include #include #include #include @@ -170,6 +171,107 @@ struct QuadraticPathComponent { std::optional GetEndDirection() const; }; +// A component that represets a Conic section curve. +// +// A conic section is basically nearly a quadratic bezier curve, but it +// has an additional weight that is applied to the middle term (the control +// point term). +// +// Starting with the equation for a quadratic curve which is: +// (A) P1 * (1 - t) * (1 - t) +// + CP * 2 * t * (1 - t) +// + P2 * t * t +// One thing to note is that the quadratic coefficients always sum to 1: +// (B) (1-t)(1-t) + 2t(1-t) + tt +// == 1 - 2t + tt + 2t - 2tt + tt +// == 1 +// which means that the resulting point is always a weighted average of +// the 3 points without having to "divide by the sum of the coefficients" +// that is normally done when computing weighted averages. +// +// The conic equation, though, would then be: +// (C) P1 * (1 - t) * (1 - t) +// + CP * 2 * t * (1 - t) * w +// + P2 * t * t +// That would be the final equation, but if we look at the coefficients: +// (D) (1-t)(1-t) + 2wt(1-t) + tt +// == 1 - 2t + tt + 2wt - 2wtt + tt +// == 1 + (2w - 2)t + (2 - 2w)tt +// These only sum to 1 if the weight (w) is 1. In order for this math to +// produce a point that is the weighted average of the 3 points, we have +// to compute both and divide the equation (C) by the equation (D). +// +// Note that there are important potential optimizations we could apply. +// If w is 0, +// then this equation devolves into a straight line from P1 to P2. +// Note that the "progress" from P1 to P2, as a function of t, is +// quadratic if you compute it as the indicated numerator and denominator, +// but the actual points generated are all on the line from P1 to P2 +// If w is (sqrt(2) / 2), +// then this math is exactly an elliptical section, provided the 3 points +// P1, CP, P2 form a right angle, and a circular section if they are also +// of equal length (|P1,CP| == |CP,P2|) +// If w is 1, +// then we really don't need the division since the denominator will always +// be 1 and we could just treat that curve as a quadratic. +// If w is (infinity/large enough), +// then the equation devolves into 2 straight lines P1->CP->P2, but +// the straightforward math may encounter infinity/NaN values in the +// intermediate stages. The limit as w approaches infinity are those +// two lines. +// +// Some examples: https://fiddle.skia.org/c/986b521feb3b832f04cbdfeefc9fbc58 +// Note that the quadratic drawn in red in the center is identical to the +// conic with a weight of 1, drawn in green in the lower left. +struct ConicPathComponent { + // Start point. + Point p1; + // Control point. + Point cp; + // End point. + Point p2; + + // Weight + // + // We only need one value, but the underlying storage allocation is always + // a multiple of Point objects. To avoid confusion over which field the + // weight is stored in, and what the value of the other field may be, we + // store it in both x,y components of the |weight| field. + // + // This may also be an advantage eventually for code that can vectorize + // the conic calculations on both X & Y simultaneously. + Point weight; + + ConicPathComponent() {} + + ConicPathComponent(Point ap1, Point acp, Point ap2, Scalar weight) + : p1(ap1), cp(acp), p2(ap2), weight(weight, weight) {} + + Point Solve(Scalar time) const; + + void AppendPolylinePoints(Scalar scale_factor, + std::vector& points) const; + + void ToLinearPathComponents(Scalar scale, VertexWriter& writer) const; + + size_t CountLinearPathComponents(Scalar scale) const; + + std::vector Extrema() const; + + bool operator==(const ConicPathComponent& other) const { + return p1 == other.p1 && cp == other.cp && p2 == other.p2 && + weight == other.weight; + } + + std::optional GetStartDirection() const; + + std::optional GetEndDirection() const; + + std::array ToQuadraticPathComponents() const; + + void SubdivideToQuadraticPoints(std::array& points) const; +}; + // A component that represets a Cubic Bézier curve. struct CubicPathComponent { // Start point. diff --git a/engine/src/flutter/impeller/geometry/path_unittests.cc b/engine/src/flutter/impeller/geometry/path_unittests.cc index 531a43f82b..8b7b5000de 100644 --- a/engine/src/flutter/impeller/geometry/path_unittests.cc +++ b/engine/src/flutter/impeller/geometry/path_unittests.cc @@ -113,6 +113,14 @@ TEST(PathTest, PathSingleContour) { EXPECT_TRUE(path.IsSingleContour()); } + + { + Path path = PathBuilder{} + .AddConicCurve({100, 100}, {100, 50}, {200, 100}, 0.75f) + .TakePath(); + + EXPECT_TRUE(path.IsSingleContour()); + } } TEST(PathTest, PathSingleContourDoubleShapes) { @@ -215,34 +223,50 @@ TEST(PathTest, PathSingleContourDoubleShapes) { EXPECT_FALSE(path.IsSingleContour()); } + + { + Path path = PathBuilder{} + .AddConicCurve({100, 100}, {100, 50}, {200, 100}, 0.75f) + .Close() + .AddConicCurve({100, 100}, {100, 50}, {200, 100}, 0.75f) + .TakePath(); + + EXPECT_FALSE(path.IsSingleContour()); + } } TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) { // Closed shapes. { Path path = PathBuilder{}.AddCircle({100, 100}, 50).TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - EXPECT_POINT_NEAR(contour.destination, Point(100, 50)); - EXPECT_TRUE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(100, 50)); + EXPECT_TRUE(contour->IsClosed()); } { Path path = PathBuilder{}.AddOval(Rect::MakeXYWH(100, 100, 100, 100)).TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - EXPECT_POINT_NEAR(contour.destination, Point(150, 100)); - EXPECT_TRUE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(150, 100)); + EXPECT_TRUE(contour->IsClosed()); } { Path path = PathBuilder{}.AddRect(Rect::MakeXYWH(100, 100, 100, 100)).TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - EXPECT_POINT_NEAR(contour.destination, Point(100, 100)); - EXPECT_TRUE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(100, 100)); + EXPECT_TRUE(contour->IsClosed()); } { @@ -250,10 +274,12 @@ TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) { .AddRoundRect(RoundRect::MakeRectRadius( Rect::MakeXYWH(100, 100, 100, 100), 10)) .TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - EXPECT_POINT_NEAR(contour.destination, Point(110, 100)); - EXPECT_TRUE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(110, 100)); + EXPECT_TRUE(contour->IsClosed()); } { @@ -261,10 +287,12 @@ TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) { .AddRoundRect(RoundRect::MakeRectXY( Rect::MakeXYWH(100, 100, 100, 100), Size(10, 20))) .TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - EXPECT_POINT_NEAR(contour.destination, Point(110, 100)); - EXPECT_TRUE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(110, 100)); + EXPECT_TRUE(contour->IsClosed()); } { @@ -272,10 +300,12 @@ TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) { .AddRoundSuperellipse(RoundSuperellipse::MakeRectRadius( Rect::MakeXYWH(100, 100, 100, 100), 10)) .TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - EXPECT_POINT_NEAR(contour.destination, Point(150, 100)); - EXPECT_TRUE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(150, 100)); + EXPECT_TRUE(contour->IsClosed()); } { @@ -283,20 +313,24 @@ TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) { .AddRoundSuperellipse(RoundSuperellipse::MakeRectXY( Rect::MakeXYWH(100, 100, 100, 100), Size(10, 20))) .TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - EXPECT_POINT_NEAR(contour.destination, Point(150, 100)); - EXPECT_TRUE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(150, 100)); + EXPECT_TRUE(contour->IsClosed()); } // Open shapes. { Point p(100, 100); Path path = PathBuilder{}.AddLine(p, {200, 100}).TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - ASSERT_POINT_NEAR(contour.destination, p); - ASSERT_FALSE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, p); + EXPECT_FALSE(contour->IsClosed()); } { @@ -304,20 +338,36 @@ TEST(PathTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) { PathBuilder{} .AddCubicCurve({100, 100}, {100, 50}, {100, 150}, {200, 100}) .TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - ASSERT_POINT_NEAR(contour.destination, Point(100, 100)); - ASSERT_FALSE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(100, 100)); + EXPECT_FALSE(contour->IsClosed()); } { Path path = PathBuilder{} .AddQuadraticCurve({100, 100}, {100, 50}, {200, 100}) .TakePath(); - ContourComponent contour; - path.GetContourComponentAtIndex(0, contour); - ASSERT_POINT_NEAR(contour.destination, Point(100, 100)); - ASSERT_FALSE(contour.IsClosed()); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(100, 100)); + EXPECT_FALSE(contour->IsClosed()); + } + + { + Path path = PathBuilder{} + .AddConicCurve({100, 100}, {100, 50}, {200, 100}, 0.75f) + .TakePath(); + EXPECT_NE(path.begin(), path.end()); + EXPECT_EQ(path.begin().type(), Path::ComponentType::kContour); + auto contour = path.begin().contour(); + ASSERT_NE(contour, nullptr); + EXPECT_POINT_NEAR(contour->destination, Point(100, 100)); + EXPECT_FALSE(contour->IsClosed()); } } @@ -433,35 +483,68 @@ TEST(PathTest, PathShifting) { auto path = builder.AddLine(Point(0, 0), Point(10, 10)) .AddQuadraticCurve(Point(10, 10), Point(15, 15), Point(20, 20)) + .AddConicCurve(Point(10, 10), Point(15, 10), Point(15, 15), 0.75f) .AddCubicCurve(Point(20, 20), Point(25, 25), Point(-5, -5), Point(30, 30)) .Close() .Shift(Point(1, 1)) .TakePath(); - ContourComponent contour; - LinearPathComponent linear; - QuadraticPathComponent quad; - CubicPathComponent cubic; + auto it = path.begin(); - ASSERT_TRUE(path.GetContourComponentAtIndex(0, contour)); - ASSERT_TRUE(path.GetLinearComponentAtIndex(1, linear)); - ASSERT_TRUE(path.GetQuadraticComponentAtIndex(3, quad)); - ASSERT_TRUE(path.GetCubicComponentAtIndex(5, cubic)); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + const ContourComponent* contour = it.contour(); + ASSERT_NE(contour, nullptr); + ++it; - EXPECT_EQ(contour.destination, Point(1, 1)); + ASSERT_EQ(it.type(), Path::ComponentType::kLinear); + const LinearPathComponent* linear = it.linear(); + ASSERT_NE(linear, nullptr); + ++it; - EXPECT_EQ(linear.p1, Point(1, 1)); - EXPECT_EQ(linear.p2, Point(11, 11)); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; - EXPECT_EQ(quad.cp, Point(16, 16)); - EXPECT_EQ(quad.p1, Point(11, 11)); - EXPECT_EQ(quad.p2, Point(21, 21)); + ASSERT_EQ(it.type(), Path::ComponentType::kQuadratic); + const QuadraticPathComponent* quad = it.quadratic(); + ASSERT_NE(quad, nullptr); + ++it; - EXPECT_EQ(cubic.cp1, Point(26, 26)); - EXPECT_EQ(cubic.cp2, Point(-4, -4)); - EXPECT_EQ(cubic.p1, Point(21, 21)); - EXPECT_EQ(cubic.p2, Point(31, 31)); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; + + ASSERT_EQ(it.type(), Path::ComponentType::kConic); + const ConicPathComponent* conic = it.conic(); + ASSERT_NE(conic, nullptr); + ++it; + + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; + + ASSERT_EQ(it.type(), Path::ComponentType::kCubic); + const CubicPathComponent* cubic = it.cubic(); + ASSERT_NE(cubic, nullptr); + ++it; + + // Close always opens a new contour, even if it isn't needed + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; + + EXPECT_EQ(it, path.end()); + + EXPECT_EQ(contour->destination, Point(1, 1)); + + EXPECT_EQ(linear->p1, Point(1, 1)); + EXPECT_EQ(linear->p2, Point(11, 11)); + + EXPECT_EQ(quad->cp, Point(16, 16)); + EXPECT_EQ(quad->p1, Point(11, 11)); + EXPECT_EQ(quad->p2, Point(21, 21)); + + EXPECT_EQ(cubic->cp1, Point(26, 26)); + EXPECT_EQ(cubic->cp2, Point(-4, -4)); + EXPECT_EQ(cubic->p1, Point(21, 21)); + EXPECT_EQ(cubic->p2, Point(31, 31)); } TEST(PathTest, PathBuilderWillComputeBounds) { @@ -490,34 +573,68 @@ TEST(PathTest, PathHorizontalLine) { PathBuilder builder; auto path = builder.HorizontalLineTo(10).TakePath(); - LinearPathComponent linear; - path.GetLinearComponentAtIndex(1, linear); + auto it = path.begin(); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; - EXPECT_EQ(linear.p1, Point(0, 0)); - EXPECT_EQ(linear.p2, Point(10, 0)); + ASSERT_EQ(it.type(), Path::ComponentType::kLinear); + const LinearPathComponent* linear = it.linear(); + ASSERT_NE(linear, nullptr); + + EXPECT_EQ(linear->p1, Point(0, 0)); + EXPECT_EQ(linear->p2, Point(10, 0)); } TEST(PathTest, PathVerticalLine) { PathBuilder builder; auto path = builder.VerticalLineTo(10).TakePath(); - LinearPathComponent linear; - path.GetLinearComponentAtIndex(1, linear); + auto it = path.begin(); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; - EXPECT_EQ(linear.p1, Point(0, 0)); - EXPECT_EQ(linear.p2, Point(0, 10)); + ASSERT_EQ(it.type(), Path::ComponentType::kLinear); + const LinearPathComponent* linear = it.linear(); + ASSERT_NE(linear, nullptr); + + EXPECT_EQ(linear->p1, Point(0, 0)); + EXPECT_EQ(linear->p2, Point(0, 10)); } TEST(PathTest, QuadradicPath) { PathBuilder builder; auto path = builder.QuadraticCurveTo(Point(10, 10), Point(20, 20)).TakePath(); - QuadraticPathComponent quad; - path.GetQuadraticComponentAtIndex(1, quad); + auto it = path.begin(); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; - EXPECT_EQ(quad.p1, Point(0, 0)); - EXPECT_EQ(quad.cp, Point(10, 10)); - EXPECT_EQ(quad.p2, Point(20, 20)); + ASSERT_EQ(it.type(), Path::ComponentType::kQuadratic); + const QuadraticPathComponent* quad = it.quadratic(); + ASSERT_NE(quad, nullptr); + + EXPECT_EQ(quad->p1, Point(0, 0)); + EXPECT_EQ(quad->cp, Point(10, 10)); + EXPECT_EQ(quad->p2, Point(20, 20)); +} + +TEST(PathTest, ConicPath) { + PathBuilder builder; + auto path = + builder.ConicCurveTo(Point(10, 10), Point(20, 20), 0.75f).TakePath(); + + auto it = path.begin(); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; + + ASSERT_EQ(it.type(), Path::ComponentType::kConic); + const ConicPathComponent* conic = it.conic(); + ASSERT_NE(conic, nullptr); + + EXPECT_EQ(conic->p1, Point(0, 0)); + EXPECT_EQ(conic->cp, Point(10, 10)); + EXPECT_EQ(conic->p2, Point(20, 20)); + EXPECT_EQ(conic->weight, Point(0.75f, 0.75f)); } TEST(PathTest, CubicPath) { @@ -526,13 +643,18 @@ TEST(PathTest, CubicPath) { builder.CubicCurveTo(Point(10, 10), Point(-10, -10), Point(20, 20)) .TakePath(); - CubicPathComponent cubic; - path.GetCubicComponentAtIndex(1, cubic); + auto it = path.begin(); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + ++it; - EXPECT_EQ(cubic.p1, Point(0, 0)); - EXPECT_EQ(cubic.cp1, Point(10, 10)); - EXPECT_EQ(cubic.cp2, Point(-10, -10)); - EXPECT_EQ(cubic.p2, Point(20, 20)); + ASSERT_EQ(it.type(), Path::ComponentType::kCubic); + const CubicPathComponent* cubic = it.cubic(); + ASSERT_NE(cubic, nullptr); + + EXPECT_EQ(cubic->p1, Point(0, 0)); + EXPECT_EQ(cubic->cp1, Point(10, 10)); + EXPECT_EQ(cubic->cp2, Point(-10, -10)); + EXPECT_EQ(cubic->p2, Point(20, 20)); } TEST(PathTest, BoundingBoxCubic) { @@ -585,9 +707,8 @@ TEST(PathTest, EmptyPath) { auto path = PathBuilder{}.TakePath(); ASSERT_EQ(path.GetComponentCount(), 1u); - ContourComponent c; - path.GetContourComponentAtIndex(0, c); - ASSERT_POINT_NEAR(c.destination, Point()); + const ContourComponent* c = path.begin().contour(); + ASSERT_POINT_NEAR(c->destination, Point()); Path::Polyline polyline = path.CreatePolyline(1.0f); ASSERT_TRUE(polyline.points->empty()); @@ -599,77 +720,122 @@ TEST(PathTest, SimplePath) { auto path = builder.AddLine({0, 0}, {100, 100}) .AddQuadraticCurve({100, 100}, {200, 200}, {300, 300}) + .AddConicCurve({100, 100}, {200, 200}, {300, 300}, 0.75f) .AddCubicCurve({300, 300}, {400, 400}, {500, 500}, {600, 600}) .TakePath(); - EXPECT_EQ(path.GetComponentCount(), 6u); + EXPECT_EQ(path.GetComponentCount(), 8u); EXPECT_EQ(path.GetComponentCount(Path::ComponentType::kLinear), 1u); EXPECT_EQ(path.GetComponentCount(Path::ComponentType::kQuadratic), 1u); + EXPECT_EQ(path.GetComponentCount(Path::ComponentType::kConic), 1u); EXPECT_EQ(path.GetComponentCount(Path::ComponentType::kCubic), 1u); - EXPECT_EQ(path.GetComponentCount(Path::ComponentType::kContour), 3u); + EXPECT_EQ(path.GetComponentCount(Path::ComponentType::kContour), 4u); + + auto it = path.begin(); { - LinearPathComponent linear; - EXPECT_TRUE(path.GetLinearComponentAtIndex(1, linear)); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + const ContourComponent* contour = it.contour(); + ASSERT_NE(contour, nullptr); + ++it; Point p1(0, 0); - Point p2(100, 100); - EXPECT_EQ(linear.p1, p1); - EXPECT_EQ(linear.p2, p2); + EXPECT_EQ(contour->destination, p1); + EXPECT_FALSE(contour->IsClosed()); } { - QuadraticPathComponent quad; - EXPECT_TRUE(path.GetQuadraticComponentAtIndex(3, quad)); + ASSERT_EQ(it.type(), Path::ComponentType::kLinear); + const LinearPathComponent* linear = it.linear(); + ASSERT_NE(linear, nullptr); + ++it; + + Point p1(0, 0); + Point p2(100, 100); + EXPECT_EQ(linear->p1, p1); + EXPECT_EQ(linear->p2, p2); + } + + { + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + const ContourComponent* contour = it.contour(); + ASSERT_NE(contour, nullptr); + ++it; + + Point p1(100, 100); + EXPECT_EQ(contour->destination, p1); + EXPECT_FALSE(contour->IsClosed()); + } + + { + ASSERT_EQ(it.type(), Path::ComponentType::kQuadratic); + const QuadraticPathComponent* quad = it.quadratic(); + ASSERT_NE(quad, nullptr); + ++it; Point p1(100, 100); Point cp(200, 200); Point p2(300, 300); - EXPECT_EQ(quad.p1, p1); - EXPECT_EQ(quad.cp, cp); - EXPECT_EQ(quad.p2, p2); + EXPECT_EQ(quad->p1, p1); + EXPECT_EQ(quad->cp, cp); + EXPECT_EQ(quad->p2, p2); } { - CubicPathComponent cubic; - EXPECT_TRUE(path.GetCubicComponentAtIndex(5, cubic)); + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + const ContourComponent* contour = it.contour(); + ASSERT_NE(contour, nullptr); + ++it; + + Point p1(100, 100); + EXPECT_EQ(contour->destination, p1); + EXPECT_FALSE(contour->IsClosed()); + } + + { + ASSERT_EQ(it.type(), Path::ComponentType::kConic); + const ConicPathComponent* conic = it.conic(); + ASSERT_NE(conic, nullptr); + ++it; + + Point p1(100, 100); + Point cp(200, 200); + Point p2(300, 300); + Point weight(0.75f, 0.75f); + EXPECT_EQ(conic->p1, p1); + EXPECT_EQ(conic->cp, cp); + EXPECT_EQ(conic->p2, p2); + EXPECT_EQ(conic->weight, weight); + } + + { + ASSERT_EQ(it.type(), Path::ComponentType::kContour); + const ContourComponent* contour = it.contour(); + ASSERT_NE(contour, nullptr); + ++it; + + Point p1(300, 300); + EXPECT_EQ(contour->destination, p1); + EXPECT_FALSE(contour->IsClosed()); + } + + { + ASSERT_EQ(it.type(), Path::ComponentType::kCubic); + const CubicPathComponent* cubic = it.cubic(); + ASSERT_NE(cubic, nullptr); + ++it; Point p1(300, 300); Point cp1(400, 400); Point cp2(500, 500); Point p2(600, 600); - EXPECT_EQ(cubic.p1, p1); - EXPECT_EQ(cubic.cp1, cp1); - EXPECT_EQ(cubic.cp2, cp2); - EXPECT_EQ(cubic.p2, p2); + EXPECT_EQ(cubic->p1, p1); + EXPECT_EQ(cubic->cp1, cp1); + EXPECT_EQ(cubic->cp2, cp2); + EXPECT_EQ(cubic->p2, p2); } - { - ContourComponent contour; - EXPECT_TRUE(path.GetContourComponentAtIndex(0, contour)); - - Point p1(0, 0); - EXPECT_EQ(contour.destination, p1); - EXPECT_FALSE(contour.IsClosed()); - } - - { - ContourComponent contour; - EXPECT_TRUE(path.GetContourComponentAtIndex(2, contour)); - - Point p1(100, 100); - EXPECT_EQ(contour.destination, p1); - EXPECT_FALSE(contour.IsClosed()); - } - - { - ContourComponent contour; - EXPECT_TRUE(path.GetContourComponentAtIndex(4, contour)); - - Point p1(300, 300); - EXPECT_EQ(contour.destination, p1); - EXPECT_FALSE(contour.IsClosed()); - } + EXPECT_EQ(it, path.end()); } TEST(PathTest, RepeatCloseDoesNotAddNewLines) { @@ -915,23 +1081,36 @@ TEST(PathTest, PathBuilderDoesNotMutateCopiedPaths) { } else { EXPECT_EQ(path.GetComponentCount(), 3u) << label; } + auto it = path.begin(); { - ContourComponent contour; - EXPECT_TRUE(path.GetContourComponentAtIndex(0, contour)) << label; - EXPECT_EQ(contour.destination, offset + Point(10, 10)) << label; - EXPECT_EQ(contour.IsClosed(), is_closed) << label; + ASSERT_EQ(it.type(), Path::ComponentType::kContour) << label; + const ContourComponent* contour = it.contour(); + ASSERT_NE(contour, nullptr) << label; + ++it; + + EXPECT_EQ(contour->destination, offset + Point(10, 10)) << label; + EXPECT_EQ(contour->IsClosed(), is_closed) << label; } { - LinearPathComponent line; - EXPECT_TRUE(path.GetLinearComponentAtIndex(1, line)) << label; - EXPECT_EQ(line.p1, offset + Point(10, 10)) << label; - EXPECT_EQ(line.p2, offset + Point(20, 20)) << label; + ASSERT_EQ(it.type(), Path::ComponentType::kLinear) << label; + const LinearPathComponent* line = it.linear(); + ASSERT_NE(line, nullptr) << label; + ++it; + + EXPECT_EQ(line->p1, offset + Point(10, 10)) << label; + EXPECT_EQ(line->p2, offset + Point(20, 20)) << label; } { - LinearPathComponent line; - EXPECT_TRUE(path.GetLinearComponentAtIndex(2, line)) << label; - EXPECT_EQ(line.p1, offset + Point(20, 20)) << label; - EXPECT_EQ(line.p2, offset + Point(20, 10)) << label; + ASSERT_EQ(it.type(), Path::ComponentType::kLinear) << label; + const LinearPathComponent* line = it.linear(); + ASSERT_NE(line, nullptr) << label; + ++it; + + EXPECT_EQ(line->p1, offset + Point(20, 20)) << label; + EXPECT_EQ(line->p2, offset + Point(20, 10)) << label; + } + if (!is_mutated) { + EXPECT_EQ(it, path.end()) << label; } }; @@ -1034,6 +1213,18 @@ TEST(PathTest, PathBuilderDoesNotMutateCopiedPaths) { }, false, {}, "Relative QuadraticCurveTo"); + test_isolation( + [](PathBuilder& builder) { + builder.ConicCurveTo({20, 30}, {30, 20}, 0.75f, false); + }, + false, {}, "Absolute ConicCurveTo"); + + test_isolation( + [](PathBuilder& builder) { + builder.ConicCurveTo({20, 30}, {30, 20}, 0.75f, true); + }, + false, {}, "Relative ConicCurveTo"); + test_isolation( [](PathBuilder& builder) { builder.CubicCurveTo({20, 30}, {30, 20}, {30, 30}, false); @@ -1083,6 +1274,12 @@ TEST(PathTest, PathBuilderDoesNotMutateCopiedPaths) { }, false, {}, "AddQuadraticCurve"); + test_isolation( + [](PathBuilder& builder) { + builder.AddConicCurve({100, 100}, {150, 100}, {150, 150}, 0.75f); + }, + false, {}, "AddConicCurve"); + test_isolation( [](PathBuilder& builder) { builder.AddCubicCurve({100, 100}, {150, 100}, {100, 150}, {150, 150});