aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/cru/platform/GraphicsBase.h10
-rw-r--r--include/cru/platform/graphics/Geometry.h16
-rw-r--r--include/cru/platform/graphics/cairo/CairoGeometry.h7
-rw-r--r--include/cru/platform/graphics/quartz/Geometry.h7
-rw-r--r--src/platform/graphics/Geometry.cpp52
-rw-r--r--src/platform/graphics/cairo/CairoGeometry.cpp32
-rw-r--r--src/platform/graphics/quartz/Geometry.cpp52
-rw-r--r--test/platform/CMakeLists.txt2
-rw-r--r--test/platform/graphics/CMakeLists.txt6
-rw-r--r--test/platform/graphics/GeometryTest.cpp53
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);
+}