diff options
| -rw-r--r-- | include/cru/platform/GraphicsBase.h | 10 | ||||
| -rw-r--r-- | include/cru/platform/graphics/Geometry.h | 16 | ||||
| -rw-r--r-- | include/cru/platform/graphics/cairo/CairoGeometry.h | 7 | ||||
| -rw-r--r-- | include/cru/platform/graphics/quartz/Geometry.h | 7 | ||||
| -rw-r--r-- | src/platform/graphics/Geometry.cpp | 52 | ||||
| -rw-r--r-- | src/platform/graphics/cairo/CairoGeometry.cpp | 32 | ||||
| -rw-r--r-- | src/platform/graphics/quartz/Geometry.cpp | 52 | ||||
| -rw-r--r-- | test/platform/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | test/platform/graphics/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | test/platform/graphics/GeometryTest.cpp | 53 |
10 files changed, 222 insertions, 15 deletions
diff --git a/include/cru/platform/GraphicsBase.h b/include/cru/platform/GraphicsBase.h index 0c391b11..4e88ca26 100644 --- a/include/cru/platform/GraphicsBase.h +++ b/include/cru/platform/GraphicsBase.h @@ -2,6 +2,7 @@ #include <cru/base/Range.h> #include <cru/base/StringUtil.h> +#include <cmath> #include <format> #include <limits> @@ -21,6 +22,11 @@ struct Point final { constexpr Point Negate() const { return Point(-x, -y); } + constexpr float Distance(const Point& other) { + auto dx = x - other.x, dy = y - other.y; + return std::sqrt(dx * dx + dy * dy); + } + std::string ToString() const { return std::format("Point(x: {}, y: {})", x, y); } @@ -39,6 +45,10 @@ constexpr Point operator-(const Point& left, const Point& right) { return Point(left.x - right.x, left.y - right.y); } +constexpr Point operator/(const Point& point, float scale) { + return Point(point.x / scale, point.y / scale); +} + struct Size final { constexpr Size() = default; constexpr Size(const float width, const float height) diff --git a/include/cru/platform/graphics/Geometry.h b/include/cru/platform/graphics/Geometry.h index ef661f2a..92338750 100644 --- a/include/cru/platform/graphics/Geometry.h +++ b/include/cru/platform/graphics/Geometry.h @@ -63,6 +63,22 @@ struct CRU_PLATFORM_GRAPHICS_API IGeometry : virtual IGraphicsResource { * virtual so it can override them. */ struct CRU_PLATFORM_GRAPHICS_API IGeometryBuilder : virtual IGraphicsResource { + private: + constexpr static auto kLogTag = "cru::platform::graphics::IGeometryBuilder"; + + public: + struct ArcInfo { + Point center; + // In radian. + float start_angle; + // In radian. + float end_angle; + }; + + static ArcInfo CalculateArcInfo(const Point& start_point, const Point& radius, + float angle, bool is_large_arc, + bool is_clockwise, const Point& end_point); + virtual Point GetCurrentPosition() = 0; virtual void MoveTo(const Point& point) = 0; diff --git a/include/cru/platform/graphics/cairo/CairoGeometry.h b/include/cru/platform/graphics/cairo/CairoGeometry.h index 1d69ebf2..7d2b1bcd 100644 --- a/include/cru/platform/graphics/cairo/CairoGeometry.h +++ b/include/cru/platform/graphics/cairo/CairoGeometry.h @@ -31,6 +31,10 @@ class CRU_PLATFORM_GRAPHICS_CAIRO_API CairoGeometry : public CairoResource, class CRU_PLATFORM_GRAPHICS_CAIRO_API CairoGeometryBuilder : public CairoResource, public virtual IGeometryBuilder { + private: + constexpr static auto kLogTag = + "cru::platform::graphics::cairo::CairoGeometryBuilder"; + public: explicit CairoGeometryBuilder(CairoGraphicsFactory* factory); ~CairoGeometryBuilder() override; @@ -45,7 +49,8 @@ class CRU_PLATFORM_GRAPHICS_CAIRO_API CairoGeometryBuilder const Point& end_point) override; void QuadraticBezierTo(const Point& control_point, const Point& end_point) override; - + void ArcTo(const Point& radius, float angle, bool is_large_arc, + bool is_clockwise, const Point& end_point) override; void CloseFigure(bool close) override; std::unique_ptr<IGeometry> Build() override; diff --git a/include/cru/platform/graphics/quartz/Geometry.h b/include/cru/platform/graphics/quartz/Geometry.h index e13d268e..631aa56f 100644 --- a/include/cru/platform/graphics/quartz/Geometry.h +++ b/include/cru/platform/graphics/quartz/Geometry.h @@ -26,6 +26,10 @@ class QuartzGeometry : public OsxQuartzResource, public virtual IGeometry { class QuartzGeometryBuilder : public OsxQuartzResource, public virtual IGeometryBuilder { + private: + constexpr static auto kLogTag = + "cru::platform::graphics::quartz::QuartzGeometryBuilder"; + public: explicit QuartzGeometryBuilder(IGraphicsFactory* graphics_factory); @@ -43,6 +47,9 @@ class QuartzGeometryBuilder : public OsxQuartzResource, const Point& end_point) override; void QuadraticBezierTo(const Point& control_point, const Point& end_point) override; + void ArcTo(const Point& radius, float angle, bool is_large_arc, + bool is_clockwise, const Point& end_point) override; + void CloseFigure(bool close) override; std::unique_ptr<IGeometry> Build() override; diff --git a/src/platform/graphics/Geometry.cpp b/src/platform/graphics/Geometry.cpp index 859a7bd6..e92ea3c9 100644 --- a/src/platform/graphics/Geometry.cpp +++ b/src/platform/graphics/Geometry.cpp @@ -1,10 +1,14 @@ #include "cru/platform/graphics/Geometry.h" - #include "cru/base/StringUtil.h" +#include "cru/base/log/Logger.h" +#include "cru/platform/GraphicsBase.h" +#include "cru/platform/Matrix.h" #include "cru/platform/graphics/Factory.h" #include <cmath> +#include <numbers> #include <unordered_set> +#include <utility> namespace cru::platform::graphics { bool IGeometry::StrokeContains(float width, const Point& point) { @@ -19,6 +23,48 @@ std::unique_ptr<IGeometry> IGeometry::CreateStrokeGeometry( "not supported on this platform."); } +IGeometryBuilder::ArcInfo IGeometryBuilder::CalculateArcInfo( + const Point& start_point, const Point& radius, float angle, + bool is_large_arc, bool is_clockwise, const Point& end_point) { + using std::swap; + + auto matrix = + Matrix::Rotation(-angle) * Matrix::Scale(1 / radius.x, 1 / radius.y); + auto s1 = matrix.TransformPoint(start_point), + s2 = matrix.TransformPoint(end_point); + + if (!is_clockwise) { + std::swap(s1, s2); + } + + auto mid = (s1 + s2) / 2; + auto d = s1.Distance(s2) / 2; + if (d > 1) { + CruLogWarn(kLogTag, "Invalid Arc."); + return {}; + } + + auto dc = std::sqrt(1 - d * d); + auto a = std::atan2(s1.x - s2.x, s2.y - s1.y); + Point center(mid.x - dc * std::cos(a), mid.y - dc * std::sin(a)); + + if (std::abs(center.x) < 0.000001) { + center.x = 0.f; + } + if (std::abs(center.y) < 0.000001) { + center.y = 0.f; + } + + auto a1 = std::atan2(s1.y - center.y, s1.x - center.x); + auto a2 = std::atan2(s2.y - center.y, s2.x - center.x); + auto a3 = a2 > a1 ? a2 - a1 : std::numbers::pi_v<float> * 2 - (a1 - a2); + if ((a3 > std::numbers::pi_v<float>) != is_large_arc) { + std::swap(a1, a2); + } + + return {matrix.Inverted()->TransformPoint(center), a1, a2}; +} + void IGeometryBuilder::RelativeMoveTo(const Point& offset) { MoveTo(GetCurrentPosition() + offset); } @@ -233,9 +279,9 @@ void IGeometryBuilder::ParseAndApplySvgPathData(std::string_view path_d) { ++position; } - auto result = cru::string::ParseToNumber<float>( - path_d.substr(position), cru::string::ParseToNumberFlags::AllowTrailingJunk); + path_d.substr(position), + cru::string::ParseToNumberFlags::AllowTrailingJunk); if (!result.valid) throw Exception("Invalid svg path data number."); diff --git a/src/platform/graphics/cairo/CairoGeometry.cpp b/src/platform/graphics/cairo/CairoGeometry.cpp index 1f680c34..0b46b4eb 100644 --- a/src/platform/graphics/cairo/CairoGeometry.cpp +++ b/src/platform/graphics/cairo/CairoGeometry.cpp @@ -1,8 +1,10 @@ #include "cru/platform/graphics/cairo/CairoGeometry.h" +#include "cru/base/log/Logger.h" #include "cru/platform/graphics/Geometry.h" #include "cru/platform/graphics/cairo/CairoGraphicsFactory.h" #include <cairo/cairo.h> +#include <numbers> namespace cru::platform::graphics::cairo { CairoGeometry::CairoGeometry(CairoGraphicsFactory* factory, @@ -115,6 +117,36 @@ void CairoGeometryBuilder::QuadraticBezierTo(const Point& control_point, CubicBezierTo(control_point, control_point, end_point); } +namespace { +bool Near(const Point& p1, const Point& p2) { + return std::abs(p1.x - p2.x) < 0.0001 && std::abs(p1.y - p2.y) < 0.0001; +} +} // namespace + +void CairoGeometryBuilder::ArcTo(const Point& radius, float angle, + bool is_large_arc, bool is_clockwise, + const Point& end_point) { + auto pos = GetCurrentPosition(); + auto info = CalculateArcInfo(pos, radius, angle, is_large_arc, is_clockwise, + end_point); + + CruLogDebug( + kLogTag, + "Arc to {}, radius {}, angle {}, is_large_arc {}, is_clockwise {}, " + "end_point {}. Calculated, center {}, start_angle {}, end_angle {}.", + pos, radius, angle, is_large_arc, is_clockwise, end_point, info.center, + info.start_angle, info.end_angle); + + cairo_new_sub_path(cairo_); + cairo_save(cairo_); + cairo_translate(cairo_, info.center.x, info.center.y); + cairo_scale(cairo_, radius.x, radius.y); + cairo_rotate(cairo_, angle * std::numbers::pi / 180.0); + cairo_arc(cairo_, 0, 0, 1, info.start_angle, info.end_angle); + cairo_restore(cairo_); + cairo_move_to(cairo_, end_point.x, end_point.y); +} + void CairoGeometryBuilder::CloseFigure(bool close) { if (close) cairo_close_path(cairo_); } diff --git a/src/platform/graphics/quartz/Geometry.cpp b/src/platform/graphics/quartz/Geometry.cpp index 4c2f90a6..5335d3f6 100644 --- a/src/platform/graphics/quartz/Geometry.cpp +++ b/src/platform/graphics/quartz/Geometry.cpp @@ -1,15 +1,17 @@ #include "cru/platform/graphics/quartz/Geometry.h" +#include <cstdlib> #include <memory> +#include "cru/base/log/Logger.h" namespace cru::platform::graphics::quartz { -QuartzGeometry::QuartzGeometry(IGraphicsFactory *graphics_factory, +QuartzGeometry::QuartzGeometry(IGraphicsFactory* graphics_factory, CGPathRef cg_path) : OsxQuartzResource(graphics_factory), cg_path_(cg_path) {} QuartzGeometry::~QuartzGeometry() { CGPathRelease(cg_path_); } -bool QuartzGeometry::FillContains(const Point &point) { +bool QuartzGeometry::FillContains(const Point& point) { return CGPathContainsPoint(cg_path_, nullptr, CGPoint{point.x, point.y}, kCGPathFill); } @@ -20,7 +22,7 @@ Rect QuartzGeometry::GetBounds() { return Convert(bounds); } -std::unique_ptr<IGeometry> QuartzGeometry::Transform(const Matrix &matrix) { +std::unique_ptr<IGeometry> QuartzGeometry::Transform(const Matrix& matrix) { auto cg_matrix = Convert(matrix); auto cg_path = CGPathCreateCopyByTransformingPath(cg_path_, &cg_matrix); return std::make_unique<QuartzGeometry>(GetGraphicsFactory(), cg_path); @@ -32,7 +34,7 @@ std::unique_ptr<IGeometry> QuartzGeometry::CreateStrokeGeometry(float width) { return std::make_unique<QuartzGeometry>(GetGraphicsFactory(), cg_path); } -QuartzGeometryBuilder::QuartzGeometryBuilder(IGraphicsFactory *graphics_factory) +QuartzGeometryBuilder::QuartzGeometryBuilder(IGraphicsFactory* graphics_factory) : OsxQuartzResource(graphics_factory) { cg_mutable_path_ = CGPathCreateMutable(); } @@ -45,28 +47,56 @@ Point QuartzGeometryBuilder::GetCurrentPosition() { return Convert(CGPathGetCurrentPoint(cg_mutable_path_)); } -void QuartzGeometryBuilder::MoveTo(const Point &point) { +void QuartzGeometryBuilder::MoveTo(const Point& point) { CGPathMoveToPoint(cg_mutable_path_, nullptr, point.x, point.y); } -void QuartzGeometryBuilder::LineTo(const Point &point) { +void QuartzGeometryBuilder::LineTo(const Point& point) { CGPathAddLineToPoint(cg_mutable_path_, nullptr, point.x, point.y); } -void QuartzGeometryBuilder::CubicBezierTo(const Point &start_control_point, - const Point &end_control_point, - const Point &end_point) { +void QuartzGeometryBuilder::CubicBezierTo(const Point& start_control_point, + const Point& end_control_point, + const Point& end_point) { CGPathAddCurveToPoint(cg_mutable_path_, nullptr, start_control_point.x, start_control_point.y, end_control_point.x, end_control_point.y, end_point.x, end_point.y); } -void QuartzGeometryBuilder::QuadraticBezierTo(const Point &control_point, - const Point &end_point) { +void QuartzGeometryBuilder::QuadraticBezierTo(const Point& control_point, + const Point& end_point) { CGPathAddQuadCurveToPoint(cg_mutable_path_, nullptr, control_point.x, control_point.y, end_point.x, end_point.y); } +namespace { +bool Near(const Point& p1, const Point& p2) { + return std::abs(p1.x - p2.x) < 0.0001 && std::abs(p1.y - p2.y) < 0.0001; +} +} // namespace + +void QuartzGeometryBuilder::ArcTo(const Point& radius, float angle, + bool is_large_arc, bool is_clockwise, + const Point& end_point) { + auto pos = GetCurrentPosition(); + auto info = CalculateArcInfo(pos, radius, angle, is_large_arc, is_clockwise, + end_point); + CruLogDebug( + kLogTag, + "Arc to {}, radius {}, angle {}, is_large_arc {}, is_clockwise {}, " + "end_point {}. Calculated, center {}, start_angle {}, end_angle {}.", + pos, radius, angle, is_large_arc, is_clockwise, end_point, info.center, + info.start_angle, info.end_angle); + + auto matrix = Matrix::Scale(radius.x, radius.y); + CGAffineTransform transform = Convert(matrix); + + CGPathAddArc(cg_mutable_path_, &transform, info.center.x, info.center.y, 1, + info.start_angle, info.end_angle, true); + + assert(Near(GetCurrentPosition(), end_point)); +} + void QuartzGeometryBuilder::CloseFigure(bool close) { if (close) CGPathCloseSubpath(cg_mutable_path_); } diff --git a/test/platform/CMakeLists.txt b/test/platform/CMakeLists.txt index 187068b8..6e8b8646 100644 --- a/test/platform/CMakeLists.txt +++ b/test/platform/CMakeLists.txt @@ -6,6 +6,8 @@ add_executable(CruPlatformBaseTest ) target_link_libraries(CruPlatformBaseTest PRIVATE CruPlatformBase CruTestBase) +add_subdirectory(graphics) + if (WIN32) add_subdirectory(graphics/direct2d) endif() diff --git a/test/platform/graphics/CMakeLists.txt b/test/platform/graphics/CMakeLists.txt new file mode 100644 index 00000000..22be21e6 --- /dev/null +++ b/test/platform/graphics/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(CruPlatformGraphicsTest + GeometryTest.cpp +) +target_link_libraries(CruPlatformGraphicsTest PRIVATE CruPlatformGraphics CruTestBase) + +cru_catch_discover_tests(CruPlatformGraphicsTest) diff --git a/test/platform/graphics/GeometryTest.cpp b/test/platform/graphics/GeometryTest.cpp new file mode 100644 index 00000000..55c39795 --- /dev/null +++ b/test/platform/graphics/GeometryTest.cpp @@ -0,0 +1,53 @@ +#include "cru/platform/graphics/Geometry.h" + +#include <catch2/catch_test_macros.hpp> +#include <numbers> +#include "catch2/catch_approx.hpp" + +using Catch::Approx; +using cru::platform::graphics::IGeometryBuilder; + +constexpr float margin = 0.000001; + +TEST_CASE("IGeometryBuilder CalculateArcInfo", "[graphics][geometry]") { + auto test = [](float start_x, float start_y, float radius_x, float radius_y, + float angle, bool is_large_arc, bool is_clockwise, float end_x, + float end_y, float expect_center_x, float expect_center_y, + float expect_start_angle, float expect_end_angle) { + auto info = IGeometryBuilder::CalculateArcInfo( + {start_x, start_y}, {radius_x, radius_y}, angle, is_large_arc, + is_clockwise, {end_x, end_y}); + REQUIRE(info.center.x == Approx(expect_center_x).margin(margin)); + REQUIRE(info.center.y == Approx(expect_center_y).margin(margin)); + REQUIRE(info.start_angle == Approx(expect_start_angle).margin(margin)); + REQUIRE(info.end_angle == Approx(expect_end_angle).margin(margin)); + }; + + test(1, 0, 1, 1, 0, false, true, 0, 1, 0, 0, 0, std::numbers::pi / 2); + test(1, 0, 1, 1, 0, true, true, 0, 1, 0, 0, std::numbers::pi / 2, 0); + test(1, 0, 1, 1, 0, false, false, 0, 1, 1, 1, std::numbers::pi, + -std::numbers::pi / 2); + test(1, 0, 1, 1, 0, true, false, 0, 1, 1, 1, -std::numbers::pi / 2, + std::numbers::pi); + + test(0, 1, 1, 1, 0, false, true, -1, 0, 0, 0, std::numbers::pi / 2, + std::numbers::pi); + test(0, 1, 1, 1, 0, true, true, -1, 0, 0, 0, std::numbers::pi, + std::numbers::pi / 2); + test(0, 1, 1, 1, 0, false, false, -1, 0, -1, 1, -std::numbers::pi / 2, 0); + test(0, 1, 1, 1, 0, true, false, -1, 0, -1, 1, 0, -std::numbers::pi / 2); + + test(-1, 0, 1, 1, 0, false, true, 0, -1, 0, 0, std::numbers::pi, + -std::numbers::pi / 2); + test(-1, 0, 1, 1, 0, true, true, 0, -1, 0, 0, -std::numbers::pi / 2, + std::numbers::pi); + test(-1, 0, 1, 1, 0, false, false, 0, -1, -1, -1, 0, std::numbers::pi / 2); + test(-1, 0, 1, 1, 0, true, false, 0, -1, -1, -1, std::numbers::pi / 2, 0); + + test(0, -1, 1, 1, 0, false, true, 1, 0, 0, 0, -std::numbers::pi / 2, 0); + test(0, -1, 1, 1, 0, true, true, 1, 0, 0, 0, 0, -std::numbers::pi / 2); + test(0, -1, 1, 1, 0, false, false, 1, 0, 1, -1, std::numbers::pi / 2, + std::numbers::pi); + test(0, -1, 1, 1, 0, true, false, 1, 0, 1, -1, std::numbers::pi, + std::numbers::pi / 2); +} |
