From 138dc37f7ac12378576e0c8666517afc1bb7ed3f Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 20 Aug 2026 20:08:32 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=AE=8C=E6=89=80=E6=9C=89?= =?UTF-8?q?=E7=9A=84=E8=BD=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kernel/double_buffer/Dependency_Graph.hpp | 3 +- .../Dependency_Graph_Storage.hpp | 26 +- kernel/src/kernel/double_buffer/mechanism.hpp | 41 ++- kernel/src/kernel/double_buffer/model.hpp | 93 +++--- kernel/src/kernel/event.hpp | 127 ++++++++ kernel/src/kernel/renderable.cpp | 4 + kernel/src/kernel/renderable.hpp | 16 +- kernel/src/kernel/renderable.ipp | 46 ++- kernel/src/kernel/scene.ipp | 3 +- kernel/src/test/Dependency_Graph_Test.cpp | 102 ++++--- kernel/src/test/object_test.cpp | 107 +++++-- kernel/src/test/render_test.cpp | 107 ++++--- render_2D/main.cmake | 3 +- render_2D/render_2D/axis/Abs_Axis.cpp | 74 +++++ render_2D/render_2D/axis/Abs_Axis.hpp | 68 +++++ render_2D/render_2D/axis/Abs_Axis.ipp | 226 ++++++++++++++ render_2D/render_2D/axis/Axis.hpp | 5 + render_2D/render_2D/axis/Axis_Types.hpp | 35 +++ render_2D/render_2D/axis/Frequency_Axis.hpp | 18 ++ render_2D/render_2D/axis/Frequency_Axis.ipp | 16 + render_2D/render_2D/axis/Numeric_Axis.hpp | 23 ++ render_2D/render_2D/axis/Numeric_Axis.ipp | 112 +++++++ render_2D/render_2D/axis/Time_Axis.cpp | 59 ++++ render_2D/render_2D/axis/Time_Axis.hpp | 39 +++ render_2D/render_2D/axis/Time_Axis.ipp | 95 ++++++ render_2D/render_2D/base/Types.hpp | 165 ++++++++++ render_2D/render_2D/event/Event.hpp | 8 + render_2D/render_2D/main.cpp | 4 +- render_2D/render_2D/render/Blend2D_Cache.cpp | 286 ++++++++++++++++++ render_2D/render_2D/render/Blend2D_Cache.hpp | 75 +++++ render_2D/render_2D/render/Blend2D_Cache.ipp | 43 +++ render_2D/tests/Axis_Test.cpp | 131 ++++++++ 32 files changed, 1967 insertions(+), 193 deletions(-) create mode 100644 kernel/src/kernel/event.hpp create mode 100644 render_2D/render_2D/axis/Abs_Axis.cpp create mode 100644 render_2D/render_2D/axis/Abs_Axis.hpp create mode 100644 render_2D/render_2D/axis/Abs_Axis.ipp create mode 100644 render_2D/render_2D/axis/Axis.hpp create mode 100644 render_2D/render_2D/axis/Axis_Types.hpp create mode 100644 render_2D/render_2D/axis/Frequency_Axis.hpp create mode 100644 render_2D/render_2D/axis/Frequency_Axis.ipp create mode 100644 render_2D/render_2D/axis/Numeric_Axis.hpp create mode 100644 render_2D/render_2D/axis/Numeric_Axis.ipp create mode 100644 render_2D/render_2D/axis/Time_Axis.cpp create mode 100644 render_2D/render_2D/axis/Time_Axis.hpp create mode 100644 render_2D/render_2D/axis/Time_Axis.ipp create mode 100644 render_2D/render_2D/base/Types.hpp create mode 100644 render_2D/render_2D/event/Event.hpp create mode 100644 render_2D/render_2D/render/Blend2D_Cache.cpp create mode 100644 render_2D/render_2D/render/Blend2D_Cache.hpp create mode 100644 render_2D/render_2D/render/Blend2D_Cache.ipp create mode 100644 render_2D/tests/Axis_Test.cpp diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp index 1637113..cffbc53 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp @@ -292,7 +292,7 @@ private: return [](Node* node, Root* root) { auto* object = static_cast(root); if constexpr (requires { object->bind_dependency_graph_object(object); }) object->bind_dependency_graph_object(object); - node->data = static_cast(&object->d); + node->data = static_cast(object->d); }; } else { @@ -629,5 +629,6 @@ inline Root::~Root() { dependency_graphs.pop_back(); const_cast(dependency_graph)->detach_destroyed(this); } + delete d; } } diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp index a945aef..d09885e 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp @@ -236,10 +236,14 @@ public: template struct Root::Builder { using Prop = typename Object::Prop; - std::unique_ptr object; - detail::Dependency_Graph_Build_Storage dependency_graph_storage; - template requires std::constructible_from - explicit Builder(Args&&... args) : object(std::make_unique(std::forward(args)...)), + using Buffer_Build = detail::Buffer_Build_Storage; + using Dependency_Graph_Build = detail::Dependency_Graph_Build_Storage; + std::unique_ptr object; /* 尚未挂接最终 Private 的对象外壳。 */ + Prop prop{}; /* build() 前暂存的发布属性写入面。 */ + Buffer_Build buffer_storage; /* build() 前暂存的普通 Buffer 初值。 */ + Dependency_Graph_Build dependency_graph_storage; /* build() 前独立编辑并验证的依赖图。 */ + template + explicit Builder(Args&&... args) : object(new Object(std::forward(args)...)), dependency_graph_storage(object->memory_resource()) {} Builder& set_pmr(Pmr pmr) { object->set_pmr_resource(pmr); @@ -253,12 +257,12 @@ struct Root::Builder { } template Value> Builder& set(Member Owner::* member, Value&& value) { - object->set(member, std::forward(value)); + prop.*member = std::forward(value); return *this; } template Tag, detail::Buffer_Value_Settable Value> Builder& set(Value&& value) { - object->template initialize_buffer(std::forward(value)); + buffer_storage.template set(std::forward(value)); return *this; } template ... Dependency_Graph_Tags, detail::Dependency_Graph_Edit_Callback_For Callback> @@ -325,7 +329,15 @@ struct Root::Builder { std::expected, Dependency_Graph_Error> build() { auto validate_result = validate(); if (!validate_result) return std::unexpected(validate_result.error()); - dependency_graph_storage.commit(object->d.dependency_graph_storage); + auto private_data = std::make_unique(object->memory_resource()); + *private_data->current = prop; + buffer_storage.commit(private_data->buffer_storage); + dependency_graph_storage.commit(private_data->dependency_graph_storage); + object->d = private_data.release(); + // d 已指向最终 Private 后,定义层可按最终 Object 类型安装公开薄壳所需的 CRTP 分派。 + if constexpr (requires { object->bind_private_crtp(object.get()); }) { + object->bind_private_crtp(object.get()); + } return std::move(object); } std::expected validate() const { diff --git a/kernel/src/kernel/double_buffer/mechanism.hpp b/kernel/src/kernel/double_buffer/mechanism.hpp index 590cd7f..6aa673d 100644 --- a/kernel/src/kernel/double_buffer/mechanism.hpp +++ b/kernel/src/kernel/double_buffer/mechanism.hpp @@ -469,6 +469,32 @@ public: ); } }; +/* Builder 在最终 Private 创建前暂存普通 Buffer 初值,并在 build() 时写入正式双缓冲。 */ +template +struct Buffer_Build_Storage; +template +struct Buffer_Build_Storage> { +private: + std::tuple values; /* build() 前暂存的各 Tagged_Buffer 初值。 */ +public: + template > Tag, Buffer_Value_Settable> Value> + void set(Value&& value) { + std::get>>(values) = std::forward(value); + } + void commit(Buffer_Storage>& storage) const { + [&](std::index_sequence) { + ( + [&] { + using Buffer = std::tuple_element_t>; + auto& target = storage.template get(); + *target.pending = std::get(values); + *target.current = *target.pending; + }(), + ... + ); + }(std::index_sequence_for{}); + } +}; } /* * State_Access 是 State 继承链的按 Tag 访问视图。 @@ -535,6 +561,10 @@ enum class Dependency_Graph_Error { }; struct Root_State_Tag {}; struct Root { + /* 所有实现层 Private 的公共析构基类;Root 通过该类型唯一拥有最终 Private。 */ + struct Private { + virtual ~Private() = default; + }; using Buffers = std::tuple<>; using Dependency_Graph_Types = std::tuple<>; using States = std::tuple>; @@ -548,6 +578,9 @@ private: detail::Pmr_Resource pmr_resource; std::pmr::vector dependency_graphs; std::pmr::vector dirty_tags; +protected: + Private* d{}; /* Builder::build() 挂接的最终 Private;由 Root 析构释放。 */ +private: void set_pmr_resource(Pmr pmr) noexcept { pmr_resource.set_resource(pmr.resource()); } @@ -574,6 +607,10 @@ public: Root() : pmr_resource(), dependency_graphs(&pmr_resource), dirty_tags(&pmr_resource) {} + Root(const Root&) = delete; + Root& operator=(const Root&) = delete; + Root(Root&&) = delete; + Root& operator=(Root&&) = delete; ~Root(); [[nodiscard]] std::pmr::memory_resource* memory_resource() const noexcept { return const_cast(&pmr_resource); @@ -605,7 +642,6 @@ public: } template struct Builder; - struct Private {}; }; template concept Object_Root = requires { @@ -629,9 +665,8 @@ concept Dependency_Object = Attached && requires { requires Buffer_List; }; template -concept Bound_Dependency_Object = Dependency_Object && requires(T* object) { +concept Bound_Dependency_Object = Dependency_Object && requires { typename T::Private; - object->d; }; template concept Bound_Dependency_Graph_Target = Bound_Dependency_Object && std::derived_from; diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 6f82435..06a01be 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -103,7 +103,7 @@ struct Def : Base { static void walk_private(Object_T* object, Callback& callback) { if constexpr (requires { typename Layer::Prev_Object; }) { if constexpr (!Reverse) walk_private(object, callback); - callback(static_cast(object->d)); + callback(static_cast(*object->d)); if constexpr (Reverse) walk_private(object, callback); } } @@ -128,25 +128,32 @@ struct Impl : Obj { state(std::allocator_arg, Allocator{resource}), buffer_storage(std::allocator_arg, Allocator{resource}), dependency_graph_storage(std::allocator_arg, Allocator{resource}) {} - } d; + }; +private: + friend struct Root::Builder; template requires std::constructible_from - explicit Impl(Args&&... args) : Obj(std::forward(args)...), d(this->memory_resource()) { + explicit Impl(Args&&... args) : Obj(std::forward(args)...) { this->template bind_object_crtp(); } -private: friend struct Root; mutable Lock lock; + [[nodiscard]] Private& data() noexcept { + return static_cast(*this->d); + } + [[nodiscard]] const Private& data() const noexcept { + return static_cast(*this->d); + } template void walk_private(Callback& callback) { if constexpr (requires { typename Layer::Prev_Object; }) { if constexpr (!Reverse) walk_private(callback); - callback(static_cast(d)); + callback(static_cast(data())); if constexpr (Reverse) walk_private(callback); } } template void before_state_set(Member Owner::* member) { - State_Access pending_states{*d.state.pending}; + State_Access pending_states{*data().state.pending}; auto callback = [&](auto& private_data) { private_data.before_state_set(this, member, pending_states); }; @@ -154,35 +161,35 @@ private: } template void after_state_set(Member Owner::* member) { - State_Access pending_states{*d.state.pending}; + State_Access pending_states{*data().state.pending}; auto callback = [&](auto& private_data) { private_data.after_state_set(this, member, pending_states); }; walk_private(callback); } void before_advance() { - State_Access pending_states{*d.state.pending}; - State_Access current_states{std::as_const(*d.state.current)}; + State_Access pending_states{*data().state.pending}; + State_Access current_states{std::as_const(*data().state.current)}; auto callback = [&](auto& private_data) { private_data.before_advance( this, - d.pending, + data().pending, pending_states, - d.current, + data().current, current_states ); }; walk_private(callback); } void after_advance() { - State_Access pending_states{*d.state.pending}; - State_Access current_states{*d.state.current}; + State_Access pending_states{*data().state.pending}; + State_Access current_states{*data().state.current}; auto callback = [&](auto& private_data) { private_data.after_advance( this, - d.pending, + data().pending, pending_states, - d.current, + data().current, current_states ); }; @@ -191,54 +198,54 @@ private: void advance_unlocked() { before_advance(); // State 从 pending 向内部 current 提交;Prop 从内部 current 向 pending 发布,两者推进后的写入侧都同步为最新基线。 - static_cast&>(d).advance(); - d.state.advance(); - d.buffer_storage.advance(); - d.dependency_graph_storage.advance(); + static_cast&>(data()).advance(); + data().state.advance(); + data().buffer_storage.advance(); + data().dependency_graph_storage.advance(); after_advance(); // after_advance 只修改已提交的 current;由状态缓冲统一刷新下一次编辑基线,具体机制不得自行同步两份状态。 - *d.state.pending = *d.state.current; + *data().state.pending = *data().state.current; } public: template Tag> auto& pending_buffer() { this->emit_dependency_source(detail::dependency_id>()); - return *d.buffer_storage.template get().pending; + return *data().buffer_storage.template get().pending; } template Tag> const auto& current_buffer() const { - return *d.buffer_storage.template get().current; + return *data().buffer_storage.template get().current; } template Tag, detail::Buffer_Value_Settable Value> void initialize_buffer(Value&& value) { - auto& buffer = d.buffer_storage.template get(); + auto& buffer = data().buffer_storage.template get(); *buffer.pending = std::forward(value); *buffer.current = *buffer.pending; } template Tag> [[nodiscard]] auto pending_dependency_graph() const { using Bound_Object = detail::Dependency_Graph_Bound_Object; - const Dependency_Graph* graph = d.dependency_graph_storage.template get().pending; + const Dependency_Graph* graph = data().dependency_graph_storage.template get().pending; return graph->typed_view(); } template Tag> [[nodiscard]] auto current_dependency_graph() const { using Bound_Object = detail::Dependency_Graph_Bound_Object; - const Dependency_Graph* graph = d.dependency_graph_storage.template get().current; + const Dependency_Graph* graph = data().dependency_graph_storage.template get().current; return graph->typed_view(); } template ... Tags, detail::Dependency_Graph_Access_Callback_For Callback> void access_pending_dependency_graph(Callback&& callback) { - d.dependency_graph_storage.template access(std::forward(callback)); + data().dependency_graph_storage.template access(std::forward(callback)); } // 依赖图只允许在编辑侧修改;回调完成后先验证 DAG,再把新结构提交到 pending。 template ... Tags, detail::Dependency_Graph_Edit_Callback_For Callback> std::expected edit_dependency_graph(Callback&& callback) { std::lock_guard guard(lock); - return d.dependency_graph_storage.template edit(std::forward(callback)); + return data().dependency_graph_storage.template edit(std::forward(callback)); } std::expected validate_dependency_graph() const { - return d.dependency_graph_storage.validate_pending(); + return data().dependency_graph_storage.validate_pending(); } template Tag, detail::Dependency_Graph_View_Node_Callback> Callback> std::expected for_each_dependency_graph_topological(Callback&& callback) const { @@ -246,48 +253,60 @@ public: } template void for_each_current_dependency_graph(Callback&& callback) const { - d.dependency_graph_storage.for_each_current(std::forward(callback)); + data().dependency_graph_storage.for_each_current(std::forward(callback)); } template Value> Impl& set(Member Owner::* member, Value&& value) { std::lock_guard guard(lock); - d.current->*member = std::forward(value); + data().current->*member = std::forward(value); return *this; } template Tag, detail::State_Callback_For Callback> void set_state_callback(Callback&& callback) { std::lock_guard guard(lock); - d.state_callbacks.template set(std::forward(callback)); + data().state_callbacks.template set(std::forward(callback)); } template Tag> void clear_state_callback() { std::lock_guard guard(lock); - d.state_callbacks.template clear(); + data().state_callbacks.template clear(); } template Tag, detail::State_Callback_For Callback> void access_state(Callback&& callback) const { std::lock_guard guard(lock); using Layer = detail::State_Value; - std::invoke(std::forward(callback), static_cast(*d.state.current)); + std::invoke(std::forward(callback), static_cast(*data().state.current)); } template Tag> void notify_state() { - d.state_callbacks.template notify(*d.state.current); + data().state_callbacks.template notify(*data().state.current); } // State 写入 pending,随后向依赖图发出对应成员的 dirty 信号;真正进入 current 发生在 advance/process 边界。 template Value> void update_state(Value&& value) { std::lock_guard guard(lock); before_state_set(Member); - d.state.pending->*Member = std::forward(value); + data().state.pending->*Member = std::forward(value); after_state_set(Member); this->emit_dependency_source(detail::dependency_id>()); } + // 在同一锁作用域内按 Tag 原子修改一组相关 State 成员,并逐项触发生命周期钩子和依赖通知。 + template requires + (sizeof...(Members) > 0) && + (detail::State_Member && ...) && + std::invocable> + void update_state(Callback&& callback) { + std::lock_guard guard(lock); + (before_state_set(Members), ...); + std::invoke(std::forward(callback), State_Access{*data().state.pending}); + (after_state_set(Members), ...); + (this->emit_dependency_source(detail::dependency_id>()), ...); + } template Callback> void process(Callback&& callback) { std::lock_guard guard(lock); advance_unlocked(); - d.process(this, std::forward(callback)); + data().process(this, std::forward(callback)); } void advance() { std::lock_guard guard(lock); @@ -297,7 +316,7 @@ public: void advance(Callback&& callback) { std::lock_guard guard(lock); advance_unlocked(); - std::invoke(std::forward(callback), std::as_const(*d.state.current)); + std::invoke(std::forward(callback), std::as_const(*data().state.current)); } }; } diff --git a/kernel/src/kernel/event.hpp b/kernel/src/kernel/event.hpp new file mode 100644 index 0000000..72bddf7 --- /dev/null +++ b/kernel/src/kernel/event.hpp @@ -0,0 +1,127 @@ +#pragma once +#include +#include +#include +namespace aethera { +enum class Event_Type : std::uint8_t { + resize, + show, + hide, + leave, + pointer_move, + pointer_press, + pointer_release, + wheel, + key_press, + key_release +}; +/* 所有输入事件的公共业务基类。 */ +struct Event { + explicit Event(Event_Type value) : type(value) {} + virtual ~Event() = default; + void accept() const noexcept { + accepted = true; + } + [[nodiscard]] bool is_accepted() const noexcept { + return accepted; + } + Event_Type type; /* 事件种类;构造后保持不变。 */ +private: + mutable bool accepted{}; /* 处理链是否已经消费事件。 */ +}; +template +concept Event_Object = std::derived_from, Event>; +template +concept Event_Point = std::default_initializable && requires(T value) { + value.x; + value.y; +}; +template +concept Event_Size = std::default_initializable && requires(T value) { + value.width; + value.height; +}; +enum class Mouse_Button : std::uint8_t { none, left, right, middle }; +using Mouse_Button_Mask = std::uint8_t; +enum class Keyboard_Modifier : std::uint8_t { + none = 0, + control = 1 << 0, + shift = 1 << 1, + alt = 1 << 2, + meta = 1 << 3 +}; +[[nodiscard]] constexpr Keyboard_Modifier operator|(Keyboard_Modifier left, + Keyboard_Modifier right) noexcept { + return static_cast(static_cast(left) | static_cast(right)); +} +/* 不依赖具体坐标类型的指针事件查询能力。 */ +struct Pointer_Event_Capability { + virtual ~Pointer_Event_Capability() = default; + [[nodiscard]] virtual double position_x() const noexcept = 0; + [[nodiscard]] virtual double position_y() const noexcept = 0; + [[nodiscard]] virtual Mouse_Button pointer_button() const noexcept = 0; + [[nodiscard]] virtual Keyboard_Modifier keyboard_modifiers() const noexcept = 0; +}; +/* 不依赖具体坐标类型的滚轮增量查询能力。 */ +struct Wheel_Event_Capability { + virtual ~Wheel_Event_Capability() = default; + [[nodiscard]] virtual double pixel_delta_x_value() const noexcept = 0; + [[nodiscard]] virtual double pixel_delta_y_value() const noexcept = 0; + [[nodiscard]] virtual double angle_delta_x_value() const noexcept = 0; + [[nodiscard]] virtual double angle_delta_y_value() const noexcept = 0; +}; +/* 带局部位置、全局位置、按键和修饰键的指针事件。 */ +template +struct Basic_Pointer_Event : Event, Pointer_Event_Capability { + explicit Basic_Pointer_Event(Event_Type value = Event_Type::pointer_move) : Event(value) {} + Point position{}; /* 事件接收对象局部坐标。 */ + Point global_position{}; /* 全局窗口坐标。 */ + Mouse_Button button{Mouse_Button::none}; /* 本次按下或释放的按键。 */ + Mouse_Button_Mask buttons{}; /* 事件发生时保持按下的按键集合。 */ + Keyboard_Modifier modifiers{Keyboard_Modifier::none}; /* 事件发生时的键盘修饰键集合。 */ + [[nodiscard]] double position_x() const noexcept override { return static_cast(position.x); } + [[nodiscard]] double position_y() const noexcept override { return static_cast(position.y); } + [[nodiscard]] Mouse_Button pointer_button() const noexcept override { return button; } + [[nodiscard]] Keyboard_Modifier keyboard_modifiers() const noexcept override { return modifiers; } +}; +/* 带角度增量和像素增量的滚轮事件。 */ +template +struct Basic_Wheel_Event : Basic_Pointer_Event, Wheel_Event_Capability { + Basic_Wheel_Event() : Basic_Pointer_Event(Event_Type::wheel) {} + double angle_delta_x{}; /* 水平方向滚轮角度增量。 */ + double angle_delta_y{}; /* 垂直方向滚轮角度增量。 */ + double pixel_delta_x{}; /* 水平方向高精度像素增量。 */ + double pixel_delta_y{}; /* 垂直方向高精度像素增量。 */ + [[nodiscard]] double pixel_delta_x_value() const noexcept override { return pixel_delta_x; } + [[nodiscard]] double pixel_delta_y_value() const noexcept override { return pixel_delta_y; } + [[nodiscard]] double angle_delta_x_value() const noexcept override { return angle_delta_x; } + [[nodiscard]] double angle_delta_y_value() const noexcept override { return angle_delta_y; } +}; +/* 带旧尺寸和新尺寸的调整事件。 */ +template +struct Basic_Resize_Event : Event { + Basic_Resize_Event() : Event(Event_Type::resize) {} + Size old_size{}; /* 调整前尺寸。 */ + Size new_size{}; /* 调整后尺寸。 */ +}; +enum class Key : std::uint16_t { + unknown, + escape, + enter, + space, + delete_key, + backspace, + left, + right, + up, + down +}; +/* 键盘按下或释放事件。 */ +struct Key_Event : Event { + explicit Key_Event(Event_Type value) : Event(value) {} + Key key{Key::unknown}; /* 标准化按键。 */ + std::uint32_t native_key{}; /* 平台原生按键编码。 */ + Keyboard_Modifier modifiers{Keyboard_Modifier::none}; /* 事件发生时的修饰键集合。 */ + bool auto_repeat{}; /* 是否由系统自动重复产生。 */ +}; +} diff --git a/kernel/src/kernel/renderable.cpp b/kernel/src/kernel/renderable.cpp index 18c1175..065d0c8 100644 --- a/kernel/src/kernel/renderable.cpp +++ b/kernel/src/kernel/renderable.cpp @@ -2,6 +2,10 @@ #include #include namespace aethera { +void Renderable::dispatch_event(const Event& event) { + const auto& data = static_cast(*d); + if (data.event_run) data.event_run(this, event); +} namespace { class Stage_Registry : Pinned { private: diff --git a/kernel/src/kernel/renderable.hpp b/kernel/src/kernel/renderable.hpp index 4cccd38..7b2a7b0 100644 --- a/kernel/src/kernel/renderable.hpp +++ b/kernel/src/kernel/renderable.hpp @@ -1,11 +1,17 @@ #pragma once +#include "event.hpp" #include "render_common.hpp" namespace aethera { -/* Renderable 对外发布的颜色缓存 Buffer 类型。 */ +/* Renderable 派生实现声明颜色缓存时使用的业务标签。 */ struct Color_Cache {}; /* Renderable 状态标签,用于访问和订阅 Renderable::State。 */ struct Renderable_State_Tag {}; struct Renderable; +/* 最终 Private 提供 handle_event(T*, const Event&) 时具备事件处理能力。 */ +template +concept Event_Renderable = Attached && requires(typename T::Private& private_data, T* object, const Event& event) { + { private_data.handle_event(object, event) } -> std::same_as; +}; /* * Prepare 数据模式定制点。 * 最终对象的 Private 提供 void prepare_data(T* object) 即满足;若同时存在 build_prepare_graph(...),子图模式优先。 @@ -55,7 +61,7 @@ concept Renderable_Object = Attached && std::derived_from && r * Renderable 定义 Scene 可调度对象的公共机制。 * 用户继续派生该定义,在派生类型的 Private 中提供 Prepare/Paint 定制点,并最终使用 Impl 创建可运行实例。 */ -struct Renderable : Def, Tagged_Buffer> { +struct Renderable : Def> { /* Renderable 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */ struct Prop : Prev_Prop {}; /* Renderable 每次 Scene 执行后发布的阶段状态与统计。 */ @@ -75,6 +81,12 @@ struct Renderable : Def, Tagg }; /* 完整声明、内部派发以及 Prepare/Paint CRTP 能力契约见 renderable.ipp 中的 Renderable::Private。 */ struct Private; + /* 将事件交给最终 Private 的可选 handle_event(...) 能力。 */ + void dispatch_event(const Event& event); +protected: + /* Builder 挂接最终 Private 后,为事件公开薄壳绑定最终对象类型。 */ + template + void bind_private_crtp(Object* object); private: /* 数据模式的内部调度入口:执行最终对象 prepare_data(...),再触发各 CRTP 层 after_prepare_data(...)。 */ template diff --git a/kernel/src/kernel/renderable.ipp b/kernel/src/kernel/renderable.ipp index 8960a73..86a499b 100644 --- a/kernel/src/kernel/renderable.ipp +++ b/kernel/src/kernel/renderable.ipp @@ -14,6 +14,7 @@ struct Renderable::Private : Prev_Private { using Graph_Builder = tf::Taskflow (*)(Root*); using State_Get = State* (*)(Root*); using State_Notify = void (*)(Root*); + using Event_Run = void (*)(Root*, const Event&); struct Stage_Dispatch { Run_Predicate predicate; /* 判断该阶段本次是否执行。 */ Rebuild_Predicate rebuild_predicate; /* 判断已有阶段子图是否重建。 */ @@ -30,6 +31,7 @@ struct Renderable::Private : Prev_Private { State_Dispatch state; /* Renderable 状态访问与发布分派。 */ }; const Dispatch* dispatch{}; /* 绑定最终对象类型后指向其静态分派表。 */ + Event_Run event_run{}; /* 最终 Private 具备事件能力时的无虚函数入口。 */ std::unique_ptr prepare_graph; /* Prepare 子图模式的当前构建产物。 */ std::unique_ptr paint_graph; /* Paint 子图模式的当前构建产物。 */ bool prepare_graph_built{}; /* Prepare 子图是否至少成功构建过一次。 */ @@ -48,7 +50,7 @@ struct Renderable::Private : Prev_Private { }; template void Renderable::run_prepare_data(Object* object) { - auto& private_data = static_cast(object->d); + auto& private_data = static_cast(*object->d); private_data.prepare_data(object); auto callback = [&](auto& value) { if constexpr (requires { value.after_prepare_data(object); }) value.after_prepare_data(object); @@ -68,21 +70,32 @@ inline bool Renderable::Private::should_paint(Attached auto*, const State&, bool return dirty; } inline void Renderable::Private::after_prepare_data(Attached auto*) {} +template +void Renderable::bind_private_crtp(Object* object) { + auto& data = static_cast(*object->d); + if constexpr (Event_Renderable) { + data.event_run = [](Root* root, const Event& event) { + auto* value = static_cast(root); + auto& private_data = static_cast(*value->d); + private_data.handle_event(value, event); + }; + } +} inline void Renderable::bind_dependency_graph_object(Attached auto* object) { using Object = std::remove_pointer_t; static_assert(Renderable_Object); - auto& data = static_cast(object->d); + auto& data = static_cast(*object->d); static const Private::Dispatch dispatch{ { [](Root* root, bool dirty) { auto* value = static_cast(root); - auto& private_data = static_cast(value->d); - return private_data.should_prepare(value, *value->d.state.current, dirty); + auto& private_data = static_cast(*value->d); + return private_data.should_prepare(value, *private_data.state.current, dirty); }, [](Root* root) { auto* value = static_cast(root); - auto& private_data = static_cast(value->d); - return private_data.should_rebuild_prepare_graph(value, *value->d.state.current); + auto& private_data = static_cast(*value->d); + return private_data.should_rebuild_prepare_graph(value, *private_data.state.current); }, []() -> Private::Stage_Run { if constexpr (Prepare_Data_Renderable) { @@ -99,8 +112,8 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { if constexpr (Prepare_Graph_Renderable) { return [](Root* root) { auto* value = static_cast(root); - auto& private_data = static_cast(value->d); - return private_data.build_prepare_graph(value, *value->d.state.current); + auto& private_data = static_cast(*value->d); + return private_data.build_prepare_graph(value, *private_data.state.current); }; } else { @@ -111,19 +124,19 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { { [](Root* root, bool dirty) { auto* value = static_cast(root); - auto& private_data = static_cast(value->d); - return private_data.should_paint(value, *value->d.state.current, dirty); + auto& private_data = static_cast(*value->d); + return private_data.should_paint(value, *private_data.state.current, dirty); }, [](Root* root) { auto* value = static_cast(root); - auto& private_data = static_cast(value->d); - return private_data.should_rebuild_paint_graph(value, *value->d.state.current); + auto& private_data = static_cast(*value->d); + return private_data.should_rebuild_paint_graph(value, *private_data.state.current); }, []() -> Private::Stage_Run { if constexpr (Paint_Data_Renderable) { return [](Root* root) { auto* value = static_cast(root); - auto& private_data = static_cast(value->d); + auto& private_data = static_cast(*value->d); private_data.paint(value); }; } @@ -135,8 +148,8 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { if constexpr (Paint_Graph_Renderable) { return [](Root* root) { auto* value = static_cast(root); - auto& private_data = static_cast(value->d); - return private_data.build_paint_graph(value, *value->d.state.current); + auto& private_data = static_cast(*value->d); + return private_data.build_paint_graph(value, *private_data.state.current); }; } else { @@ -147,7 +160,8 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { { [](Root* root) { auto* value = static_cast(root); - return static_cast(value->d.state.current); + auto& private_data = static_cast(*value->d); + return static_cast(private_data.state.current); }, [](Root* root) { auto* value = static_cast(root); diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index 24fa983..f356e09 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -27,7 +27,8 @@ struct Scene::Private::Runtime { }; template void Scene::Private::process(Object* object, Callback&& callback) requires std::invocable { - auto& state = static_cast(*object->d.state.current); + auto& private_data = static_cast(*this); + auto& state = static_cast(*private_data.state.current); state.taskflow_execution_time_ns = 0; if (runtime->taskflow && !runtime->taskflow->empty()) state.taskflow_execution_time_ns = detail::run_taskflow(*runtime->taskflow); object->template notify_state(); diff --git a/kernel/src/test/Dependency_Graph_Test.cpp b/kernel/src/test/Dependency_Graph_Test.cpp index 1c2617c..9d28445 100644 --- a/kernel/src/test/Dependency_Graph_Test.cpp +++ b/kernel/src/test/Dependency_Graph_Test.cpp @@ -19,60 +19,70 @@ struct Graph_Object : double_buffer::Def; +inline auto& Graph_Object::data_for_test() { + return static_cast(*d); +} +template +std::unique_ptr build_object() { + auto result = typename Object_Type::Builder{}.build(); + if (!result) std::terminate(); + return std::move(result).value(); +} static_assert(!double_buffer::detail::Dependency_Object); static_assert(double_buffer::detail::Dependency_Object); static_assert(!double_buffer::detail::State_Dependency_Source); } TEST(dependency_graph_storage, edited_graph_commits_and_stays_synchronized) { - Node first; - Node second; - Graph graph; - auto result = graph.edit_dependency_graph( + auto first = build_object(); + auto second = build_object(); + auto graph = build_object(); + auto result = graph->edit_dependency_graph( [&](auto& editor) { - editor.add(&first); - editor.add(&second); - editor.template add_dependency<&Node_Object::State::value>(&second, &first); + editor.add(first.get()); + editor.add(second.get()); + editor.template add_dependency<&Node_Object::State::value>(second.get(), first.get()); } ); ASSERT_TRUE(result.has_value()); - auto& buffer = graph.d.dependency_graph_storage.get(); + auto& buffer = graph->data_for_test().dependency_graph_storage.get(); auto* pending_before = buffer.pending; auto* current_before = buffer.current; - graph.advance(); + graph->advance(); EXPECT_EQ(buffer.current, pending_before); EXPECT_EQ(buffer.pending, current_before); EXPECT_EQ(buffer.current->size(), 2u); EXPECT_EQ(buffer.pending->size(), 2u); - EXPECT_TRUE(buffer.current->depends_on(&second, &first)); - EXPECT_TRUE(buffer.pending->depends_on(&second, &first)); + EXPECT_TRUE(buffer.current->depends_on(second.get(), first.get())); + EXPECT_TRUE(buffer.pending->depends_on(second.get(), first.get())); } TEST(dependency_graph_storage, unchanged_graph_does_not_advance_again) { - Node node; - Graph graph; - ASSERT_TRUE(graph.edit_dependency_graph([&](auto& editor) { editor.add(&node); }).has_value()); - graph.advance(); - auto& buffer = graph.d.dependency_graph_storage.get(); + auto node = build_object(); + auto graph = build_object(); + ASSERT_TRUE(graph->edit_dependency_graph([&](auto& editor) { editor.add(node.get()); }).has_value()); + graph->advance(); + auto& buffer = graph->data_for_test().dependency_graph_storage.get(); auto* pending = buffer.pending; auto* current = buffer.current; - graph.advance(); + graph->advance(); EXPECT_EQ(buffer.pending, pending); EXPECT_EQ(buffer.current, current); } TEST(dependency_graph, topological_order_and_state_dirty_propagation) { - Node source; - Node target; - Graph graph; - ASSERT_TRUE(graph.edit_dependency_graph( + auto source = build_object(); + auto target = build_object(); + auto graph = build_object(); + ASSERT_TRUE(graph->edit_dependency_graph( [&](auto& editor) { - editor.add(&source); - editor.add(&target); - editor.template add_dependency<&Node_Object::State::value>(&target, &source); + editor.add(source.get()); + editor.add(target.get()); + editor.template add_dependency<&Node_Object::State::value>(target.get(), source.get()); } ).has_value()); - graph.advance(); - auto view = graph.current_dependency_graph(); + graph->advance(); + auto view = graph->current_dependency_graph(); std::vector order; auto result = view.for_each_topological_view( [&](const auto&, const auto& node) { @@ -81,34 +91,34 @@ TEST(dependency_graph, topological_order_and_state_dirty_propagation) { ); ASSERT_TRUE(result.has_value()); ASSERT_EQ(order.size(), 2u); - EXPECT_EQ(order[0], &source); - EXPECT_EQ(order[1], &target); - source.update_state<&Node_Object::State::value>(31); - EXPECT_TRUE(target.dirty()); + EXPECT_EQ(order[0], source.get()); + EXPECT_EQ(order[1], target.get()); + source->update_state<&Node_Object::State::value>(31); + EXPECT_TRUE(target->dirty()); } TEST(dependency_graph, cycle_is_rejected_before_pending_graph_commit) { - Node first; - Node second; - Graph graph; - auto result = graph.edit_dependency_graph( + auto first = build_object(); + auto second = build_object(); + auto graph = build_object(); + auto result = graph->edit_dependency_graph( [&](auto& editor) { - editor.add(&first); - editor.add(&second); - editor.template add_dependency<&Node_Object::State::value>(&first, &second); - editor.template add_dependency<&Node_Object::State::value>(&second, &first); + editor.add(first.get()); + editor.add(second.get()); + editor.template add_dependency<&Node_Object::State::value>(first.get(), second.get()); + editor.template add_dependency<&Node_Object::State::value>(second.get(), first.get()); } ); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), double_buffer::Dependency_Graph_Error::cycle); - EXPECT_TRUE(graph.pending_dependency_graph().empty()); + EXPECT_TRUE(graph->pending_dependency_graph().empty()); } TEST(dependency_graph_lifetime, referenced_object_can_be_destroyed_before_graph_owner) { - Graph graph; - auto node = std::make_unique(); - ASSERT_TRUE(graph.edit_dependency_graph([&](auto& editor) { editor.add(node.get()); }).has_value()); - graph.advance(); - ASSERT_EQ(graph.current_dependency_graph().size(), 1u); + auto graph = build_object(); + auto node = build_object(); + ASSERT_TRUE(graph->edit_dependency_graph([&](auto& editor) { editor.add(node.get()); }).has_value()); + graph->advance(); + ASSERT_EQ(graph->current_dependency_graph().size(), 1u); node.reset(); - EXPECT_TRUE(graph.current_dependency_graph().empty()); - EXPECT_TRUE(graph.pending_dependency_graph().empty()); + EXPECT_TRUE(graph->current_dependency_graph().empty()); + EXPECT_TRUE(graph->pending_dependency_graph().empty()); } diff --git a/kernel/src/test/object_test.cpp b/kernel/src/test/object_test.cpp index 7be52da..ad6f7ff 100644 --- a/kernel/src/test/object_test.cpp +++ b/kernel/src/test/object_test.cpp @@ -2,7 +2,8 @@ #include namespace { struct Object_State_Tag {}; -struct Test_Object : double_buffer::Def> { +struct Object_Buffer_Tag {}; +struct Test_Object : double_buffer::Def, double_buffer::Tagged_Buffer> { struct Prop : Prev_Prop { int first{}; int second{}; @@ -13,8 +14,12 @@ struct Test_Object : double_buffer::Def; +inline auto& Test_Object::data_for_test() { + return static_cast(*d); +} struct Derived_State_Tag {}; struct Derived_Object : double_buffer::Def> { struct Prop : Prev_Prop {}; @@ -25,44 +30,84 @@ struct Derived_Object : double_buffer::Def; +struct Lifetime_State_Tag {}; +struct Lifetime_Object : double_buffer::Def> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { + bool operator==(const State&) const = default; + }; + struct Private : Prev_Private { + inline static int destruction_count{}; /* 测试最终 Private 是否经公共虚析构链释放。 */ + ~Private() override { + ++destruction_count; + } + }; +}; +using Lifetime = double_buffer::Impl; +template +std::unique_ptr build_object() { + auto result = typename Object_Type::Builder{}.build(); + if (!result) std::terminate(); + return std::move(result).value(); +} +static_assert(!std::constructible_from); +} +TEST(object_lifetime, root_destroys_attached_private_through_virtual_base) { + Lifetime_Object::Private::destruction_count = 0; + { + auto object = build_object(); + EXPECT_EQ(Lifetime_Object::Private::destruction_count, 0); + } + EXPECT_EQ(Lifetime_Object::Private::destruction_count, 1); +} +TEST(object_builder, build_attaches_private_and_commits_staged_values) { + Object::Builder builder; + builder.set(&Test_Object::Prop::first, 29).set(31); + auto result = builder.build(); + ASSERT_TRUE(result.has_value()); + auto object = std::move(result).value(); + EXPECT_EQ(object->data_for_test().current->first, 29); + EXPECT_EQ(object->data_for_test().pending->first, 0); + EXPECT_EQ(object->current_buffer(), 31); + EXPECT_EQ(object->pending_buffer(), 31); } TEST(object_buffer, state_commits_and_keeps_incremental_baseline) { - Object object; - object.update_state<&Test_Object::State::first>(3); - object.advance(); - EXPECT_EQ(object.d.state.current->first, 3); - EXPECT_EQ(object.d.state.pending->first, 3); - object.update_state<&Test_Object::State::second>(5); - object.advance(); - EXPECT_EQ(object.d.state.current->first, 3); - EXPECT_EQ(object.d.state.current->second, 5); + auto object = build_object(); + object->update_state<&Test_Object::State::first>(3); + object->advance(); + EXPECT_EQ(object->data_for_test().state.current->first, 3); + EXPECT_EQ(object->data_for_test().state.pending->first, 3); + object->update_state<&Test_Object::State::second>(5); + object->advance(); + EXPECT_EQ(object->data_for_test().state.current->first, 3); + EXPECT_EQ(object->data_for_test().state.current->second, 5); } TEST(object_buffer, prop_publishes_and_keeps_incremental_baseline) { - Object object; - object.set(&Test_Object::Prop::first, 17); - EXPECT_EQ(object.d.current->first, 17); - EXPECT_EQ(object.d.pending->first, 0); - object.advance(); - EXPECT_EQ(object.d.pending->first, 17); - EXPECT_EQ(object.d.current->first, 17); - object.set(&Test_Object::Prop::second, 19); - object.advance(); - EXPECT_EQ(object.d.pending->first, 17); - EXPECT_EQ(object.d.pending->second, 19); + auto object = build_object(); + object->set(&Test_Object::Prop::first, 17); + EXPECT_EQ(object->data_for_test().current->first, 17); + EXPECT_EQ(object->data_for_test().pending->first, 0); + object->advance(); + EXPECT_EQ(object->data_for_test().pending->first, 17); + EXPECT_EQ(object->data_for_test().current->first, 17); + object->set(&Test_Object::Prop::second, 19); + object->advance(); + EXPECT_EQ(object->data_for_test().pending->first, 17); + EXPECT_EQ(object->data_for_test().pending->second, 19); } TEST(state_tag, callback_publishes_only_requested_layer) { - Object object; + auto object = build_object(); int calls = 0; - object.set_state_callback( + object->set_state_callback( [&](const auto& state) { ++calls; EXPECT_EQ(state.first, 23); } ); - object.update_state<&Test_Object::State::first>(23); - object.advance(); + object->update_state<&Test_Object::State::first>(23); + object->advance(); EXPECT_EQ(calls, 0); - object.notify_state(); + object->notify_state(); EXPECT_EQ(calls, 1); } static_assert(requires(Object& object) { @@ -71,15 +116,15 @@ static_assert(requires(Object& object) { struct Missing_State_Tag {}; static_assert(!double_buffer::detail::State_Tag_In); TEST(state_tag, inherited_tags_remain_independently_addressable) { - Derived object; + auto object = build_object(); int base_calls = 0; int derived_calls = 0; - object.set_state_callback([&](const auto&) { ++base_calls; }); - object.set_state_callback([&](const auto&) { ++derived_calls; }); - object.notify_state(); + object->set_state_callback([&](const auto&) { ++base_calls; }); + object->set_state_callback([&](const auto&) { ++derived_calls; }); + object->notify_state(); EXPECT_EQ(base_calls, 1); EXPECT_EQ(derived_calls, 0); - object.notify_state(); + object->notify_state(); EXPECT_EQ(base_calls, 1); EXPECT_EQ(derived_calls, 1); } diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index 8683f78..9f47454 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -18,6 +18,7 @@ struct Direct_Renderable : double_buffer::Def> { struct Prop : Prev_Prop {}; @@ -42,10 +43,17 @@ struct Graph_Renderable : double_buffer::Def; using Graph = double_buffer::Impl; using Scene = double_buffer::Impl; +inline Direct_Renderable::Private& Direct_Renderable::data_for_test() { + return static_cast(*d); +} +inline Graph_Renderable::Private& Graph_Renderable::data_for_test() { + return static_cast(*d); +} struct Dependency_State_Tag {}; struct Dependency_Renderable : double_buffer::Def> { struct Prop : Prev_Prop {}; @@ -59,14 +67,25 @@ struct Dependency_Renderable : double_buffer::Deftemplate update_state<&State::revision>(object->d.state.pending->revision + 1); + auto& private_data = static_cast::Private&>(*this); + if (std::exchange(publish_revision, false)) object->template update_state<&State::revision>(private_data.state.pending->revision + 1); } void paint(double_buffer::Attached auto*) { ++paint_calls; } }; + Private& data_for_test(); }; using Dependency = double_buffer::Impl; +inline Dependency_Renderable::Private& Dependency_Renderable::data_for_test() { + return static_cast(*d); +} +template +std::unique_ptr build_object() { + auto result = typename Object_Type::Builder{}.build(); + if (!result) std::terminate(); + return std::move(result).value(); +} template void add_renderable(Scene& scene, Renderable* renderable) { ASSERT_TRUE((scene.edit_dependency_graph( @@ -79,55 +98,55 @@ void add_renderable(Scene& scene, Renderable* renderable) { } TEST(renderable_capability, direct_stages_do_not_allocate_subgraphs) { aethera::initialize_runtime(2); - Direct renderable; - Scene scene; - add_renderable(scene, &renderable); - scene.process([](const auto&) {}); - auto& data = static_cast(renderable.d); - auto& base = static_cast(renderable.d); + auto renderable = build_object(); + auto scene = build_object(); + add_renderable(*scene, renderable.get()); + scene->process([](const auto&) {}); + auto& data = renderable->data_for_test(); + auto& base = static_cast(data); EXPECT_EQ(data.prepare_calls, 1); EXPECT_EQ(data.paint_calls, 1); EXPECT_EQ(base.prepare_graph, nullptr); EXPECT_EQ(base.paint_graph, nullptr); - scene.process([](const auto&) {}); + scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_calls, 1); EXPECT_EQ(data.paint_calls, 1); } TEST(renderable_capability, graph_stage_builds_lazily_and_rebuilds_inside_condition) { aethera::initialize_runtime(2); - Graph renderable; - Scene scene; - add_renderable(scene, &renderable); - auto& data = static_cast(renderable.d); - auto& base = static_cast(renderable.d); + auto renderable = build_object(); + auto scene = build_object(); + add_renderable(*scene, renderable.get()); + auto& data = renderable->data_for_test(); + auto& base = static_cast(data); EXPECT_EQ(base.prepare_graph, nullptr); - scene.process([](const auto&) {}); + scene->process([](const auto&) {}); ASSERT_NE(base.prepare_graph, nullptr); EXPECT_EQ(data.prepare_builds, 1); EXPECT_EQ(data.prepare_calls, 1); - scene.process([](const auto&) {}); + scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_builds, 1); EXPECT_EQ(data.prepare_calls, 1); data.rebuild_prepare = true; - renderable.mark_dirty(); - scene.process([](const auto&) {}); + renderable->mark_dirty(); + scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_builds, 2); EXPECT_EQ(data.prepare_calls, 2); } TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundaries) { aethera::initialize_runtime(2); - Direct renderable; - Scene scene; - add_renderable(scene, &renderable); + auto renderable = build_object(); + auto scene = build_object(); + add_renderable(*scene, renderable.get()); int renderable_updates = 0; int scene_updates = 0; int runtime_updates = 0; - renderable.set_state_callback([&](const auto& state) { + renderable->set_state_callback([&](const auto& state) { ++renderable_updates; EXPECT_TRUE(state.prepare_executed); EXPECT_TRUE(state.paint_executed); }); - scene.set_state_callback([&](const auto& state) { + scene->set_state_callback([&](const auto& state) { ++scene_updates; EXPECT_GT(state.taskflow_task_count, 0u); }); @@ -136,7 +155,7 @@ TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundarie EXPECT_GE(state.completed_taskflow_count, 1u); EXPECT_GT(state.observed_task_count, 0u); }); - scene.process([](const auto&) {}); + scene->process([](const auto&) {}); EXPECT_EQ(renderable_updates, 1); EXPECT_EQ(scene_updates, 1); EXPECT_EQ(runtime_updates, 1); @@ -144,13 +163,13 @@ TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundarie } TEST(scene_state, structural_statistics_survive_a_process_without_rebuild) { aethera::initialize_runtime(2); - Direct renderable; - Scene scene; - add_renderable(scene, &renderable); + auto renderable = build_object(); + auto scene = build_object(); + add_renderable(*scene, renderable.get()); std::size_t task_count{}; std::size_t dependency_count{}; int updates{}; - scene.set_state_callback([&](const auto& state) { + scene->set_state_callback([&](const auto& state) { EXPECT_EQ(state.renderable_count, 1u); EXPECT_GT(state.taskflow_task_count, 0u); if (updates == 0) { @@ -165,35 +184,35 @@ TEST(scene_state, structural_statistics_survive_a_process_without_rebuild) { } ++updates; }); - scene.process([](const auto&) {}); - scene.process([](const auto&) {}); + scene->process([](const auto&) {}); + scene->process([](const auto&) {}); EXPECT_EQ(updates, 2); } TEST(scene_condition, upstream_change_makes_downstream_run_in_same_taskflow) { aethera::initialize_runtime(2); - Dependency source; - Dependency target; - Scene scene; - ASSERT_TRUE((scene.edit_dependency_graph( + auto source = build_object(); + auto target = build_object(); + auto scene = build_object(); + ASSERT_TRUE((scene->edit_dependency_graph( [&](auto& prepare, auto& paint) { - prepare.add(&source); - prepare.add(&target); - prepare.template add_dependency<&Dependency_Renderable::State::revision>(&target, &source); - paint.add(&source); - paint.add(&target); + prepare.add(source.get()); + prepare.add(target.get()); + prepare.template add_dependency<&Dependency_Renderable::State::revision>(target.get(), source.get()); + paint.add(source.get()); + paint.add(target.get()); } ).has_value())); - scene.process([](const auto&) {}); - auto& source_data = static_cast(source.d); - auto& target_data = static_cast(target.d); + scene->process([](const auto&) {}); + auto& source_data = source->data_for_test(); + auto& target_data = target->data_for_test(); EXPECT_EQ(source_data.prepare_calls, 1); EXPECT_EQ(target_data.prepare_calls, 1); - scene.process([](const auto&) {}); + scene->process([](const auto&) {}); EXPECT_EQ(source_data.prepare_calls, 1); EXPECT_EQ(target_data.prepare_calls, 1); source_data.publish_revision = true; - source.mark_dirty(); - scene.process([](const auto&) {}); + source->mark_dirty(); + scene->process([](const auto&) {}); EXPECT_EQ(source_data.prepare_calls, 2); EXPECT_EQ(target_data.prepare_calls, 2); } diff --git a/render_2D/main.cmake b/render_2D/main.cmake index 3c03d49..5db018a 100644 --- a/render_2D/main.cmake +++ b/render_2D/main.cmake @@ -42,7 +42,7 @@ install(TARGETS Aethera_render_2D RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") install(DIRECTORY "${Aethera_render_2D_source_dir}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/render_2D" - FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp") + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp") if (RENDERIVE_BUILD_TESTS) set(Aethera_render_2D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Aethera_render_2D_test_sources "${Aethera_render_2D_test_dir}") @@ -56,7 +56,6 @@ if (RENDERIVE_BUILD_TESTS) set(Aethera_render_2D_test_target "Aethera_render_2D_${Aethera_render_2D_test_name}") add_executable("${Aethera_render_2D_test_target}" "${Aethera_render_2D_test_source}") target_link_libraries("${Aethera_render_2D_test_target}" PRIVATE Aethera_render_2D GTest::gtest_main) - renderive_stage_kernel_runtime("${Aethera_render_2D_test_target}") add_test(NAME "${Aethera_render_2D_test_target}" COMMAND "${Aethera_render_2D_test_target}") set_tests_properties("${Aethera_render_2D_test_target}" PROPERTIES LABELS "Aethera_render_2D" diff --git a/render_2D/render_2D/axis/Abs_Axis.cpp b/render_2D/render_2D/axis/Abs_Axis.cpp new file mode 100644 index 0000000..a60e47b --- /dev/null +++ b/render_2D/render_2D/axis/Abs_Axis.cpp @@ -0,0 +1,74 @@ +#include "Abs_Axis.hpp" +#include +#include +#include +#include +namespace aethera::render_2d { +Axis_Range Abs_Axis::Private::coordinate_range(const Root* object) const { + return dispatch->coordinate_range(object); +} +double Abs_Axis::Private::coordinate_to_pixel(const Root* object, double coordinate) const { + return dispatch->coordinate_to_pixel(object, coordinate); +} +double Abs_Axis::Private::pixel_to_coordinate(const Root* object, double pixel) const { + return dispatch->pixel_to_coordinate(object, pixel); +} +double Abs_Axis::Private::point_to_coordinate(const Root* object, Point_F point) const { + return dispatch->point_to_coordinate(object, point); +} +int Abs_Axis::Private::pixel_sample_count(const Root* object, Axis_Range coordinate_range) const { + return dispatch->pixel_sample_count(object, coordinate_range); +} +double Abs_Axis::Private::tick_step(const Root* object, Axis_Range coordinate_range) const { + return dispatch->tick_step(object, coordinate_range); +} +std::string Abs_Axis::Private::tick_label(const Root* object, double tick) const { + return dispatch->tick_label(object, tick); +} +int Abs_Axis::Private::sub_tick_count(const Root* object, double major_step) const { + return dispatch->sub_tick_count(object, major_step); +} +double Abs_Axis::Private::nice_tick_step(Axis_Range coordinate_range) { + const double raw_step = coordinate_range.size() / 5.0; + if (!(raw_step > 0.0) || !std::isfinite(raw_step)) return 1.0; + const double scale = std::pow(10.0, std::floor(std::log10(raw_step))); + const double normalized = raw_step / scale; + const double nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 : normalized <= 5.0 ? 5.0 : 10.0; + return nice * scale; +} +std::string Abs_Axis::Private::localized_number(double value, int precision, Number_Locale locale) { + std::ostringstream stream; + stream << std::fixed << std::setprecision(std::clamp(precision, 0, 12)) << value; + std::string result = stream.str(); + if (result.find('.') != std::string::npos) { + while (!result.empty() && result.back() == '0') result.pop_back(); + if (!result.empty() && result.back() == '.') result.pop_back(); + } + if (locale.decimal_point != '.') std::replace(result.begin(), result.end(), '.', locale.decimal_point); + return result; +} +Axis_Range Abs_Axis::coordinate_range() const { + return static_cast(*d).coordinate_range(this); +} +double Abs_Axis::coordinate_to_pixel(double coordinate) const { + return static_cast(*d).coordinate_to_pixel(this, coordinate); +} +double Abs_Axis::pixel_to_coordinate(double pixel) const { + return static_cast(*d).pixel_to_coordinate(this, pixel); +} +double Abs_Axis::point_to_coordinate(Point_F point) const { + return static_cast(*d).point_to_coordinate(this, point); +} +int Abs_Axis::pixel_sample_count(Axis_Range coordinate_range) const { + return static_cast(*d).pixel_sample_count(this, coordinate_range); +} +double Abs_Axis::tick_step(Axis_Range coordinate_range) const { + return static_cast(*d).tick_step(this, coordinate_range); +} +std::string Abs_Axis::tick_label(double tick) const { + return static_cast(*d).tick_label(this, tick); +} +int Abs_Axis::sub_tick_count(double major_step) const { + return static_cast(*d).sub_tick_count(this, major_step); +} +} diff --git a/render_2D/render_2D/axis/Abs_Axis.hpp b/render_2D/render_2D/axis/Abs_Axis.hpp new file mode 100644 index 0000000..97d557a --- /dev/null +++ b/render_2D/render_2D/axis/Abs_Axis.hpp @@ -0,0 +1,68 @@ +#pragma once +#include "Axis_Types.hpp" +#include "../render/Blend2D_Cache.hpp" +#include +#include +namespace aethera::render_2d { +/* Abs_Axis 状态标签,用于访问和订阅坐标轴布局状态。 */ +struct Abs_Axis_State_Tag {}; +struct Abs_Axis; +/* 最终轴 Private 的完整计算能力契约;所有结果直接来自当前 State。 */ +template +concept Axis_Object = Renderable_Object && std::derived_from && requires( + const typename T::Private& private_data, + const T* object, + Axis_Range coordinate_range, + double tick +) { + { private_data.coordinate_range(object) } -> std::same_as; + { private_data.tick_step(object, coordinate_range) } -> std::same_as; + { private_data.tick_label(object, tick) } -> std::same_as; + { private_data.sub_tick_count(object, tick) } -> std::same_as; +}; +/* 所有二维坐标轴共享的定义层;最终通过 Impl 创建运行时对象。 */ +struct Abs_Axis : Def, + Tagged_Buffer> { + /* Abs_Axis 不发布额外属性。 */ + struct Prop : Prev_Prop {}; + /* 双缓冲交换后供轴计算直接读取的布局权威状态。 */ + struct State : Prev_State { + Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */ + Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */ + double pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */ + Axis_Orientation orientation{Axis_Orientation::horizontal}; /* 从二维点取轴向分量时使用的方向。 */ + double tick_length{10.0}; /* 主刻度线长度,单位为像素。 */ + double sub_tick_length{5.0}; /* 次刻度线长度,单位为像素。 */ + Pen axis_pen{Color::white()}; /* 轴线、主刻度和次刻度的描边样式。 */ + std::string unit_text{}; /* 坐标轴末端显示的单位文本;空值表示不绘制。 */ + Font unit_text_font{}; /* 刻度标签和单位文本使用的字体。 */ + Pen unit_text_pen{Color::white()}; /* 刻度标签和单位文本使用的前景样式。 */ + Brush unit_text_background_brush{}; /* 单位文本背景填充;none 表示不填充。 */ + double label_rotation_degrees{}; /* 刻度标签顺时针旋转角度,单位为度。 */ + bool operator==(const State&) const = default; + }; + /* 完整声明及派生轴 CRTP 能力契约见 Abs_Axis.ipp。 */ + struct Private; + /* 返回由最终轴 State 计算得到的当前有向坐标区间。 */ + [[nodiscard]] Axis_Range coordinate_range() const; + /* 将坐标值映射到当前轴向像素位置。 */ + [[nodiscard]] double coordinate_to_pixel(double coordinate) const; + /* 将当前轴向像素位置反算为坐标值。 */ + [[nodiscard]] double pixel_to_coordinate(double pixel) const; + /* 按轴方向从二维像素点取值并反算为坐标。 */ + [[nodiscard]] double point_to_coordinate(Point_F point) const; + /* 返回给定坐标区间覆盖的整数像素样本数量。 */ + [[nodiscard]] int pixel_sample_count(Axis_Range coordinate_range) const; + /* 返回给定坐标区间使用的主刻度步长。 */ + [[nodiscard]] double tick_step(Axis_Range coordinate_range) const; + /* 返回指定主刻度的显示文本。 */ + [[nodiscard]] std::string tick_label(double tick) const; + /* 返回相邻主刻度之间的次刻度数量。 */ + [[nodiscard]] int sub_tick_count(double major_step) const; +protected: + /* Builder 挂接最终 Private 后,为公开薄壳绑定最终轴类型的无虚函数分派。 */ + template + void bind_private_crtp(Object* object); +}; +} +#include "Abs_Axis.ipp" diff --git a/render_2D/render_2D/axis/Abs_Axis.ipp b/render_2D/render_2D/axis/Abs_Axis.ipp new file mode 100644 index 0000000..871aeef --- /dev/null +++ b/render_2D/render_2D/axis/Abs_Axis.ipp @@ -0,0 +1,226 @@ +#pragma once +#include +#include +#include +namespace aethera::render_2d { +struct Abs_Axis::Private : Prev_Private { + /* + * 派生轴 Private 必须继承 Prev_Private,并提供以下 CRTP 能力: + * Axis_Range coordinate_range(const T* object) const:从最终轴当前 State 返回权威坐标区间。 + * double tick_step(const T* object, Axis_Range coordinate_range) const:返回主刻度步长。 + * std::string tick_label(const T* object, double tick) const:返回主刻度显示文本。 + * int sub_tick_count(const T* object, double major_step) const:返回次刻度数量;默认固定返回 4。 + * 坐标映射直接读取 Abs_Axis_State_Tag 状态并调用最终 Private 的 coordinate_range(...),不保存变换快照。 + */ + using Coordinate_Range_Call = Axis_Range (*)(const Root*); + using Scalar_Call = double (*)(const Root*, double); + using Point_Call = double (*)(const Root*, Point_F); + using Range_Count_Call = int (*)(const Root*, Axis_Range); + using Range_Scalar_Call = double (*)(const Root*, Axis_Range); + using Count_Call = int (*)(const Root*, double); + using Label_Call = std::string (*)(const Root*, double); + struct Dispatch { + Coordinate_Range_Call coordinate_range; /* 读取最终轴权威坐标区间。 */ + Scalar_Call coordinate_to_pixel; /* 坐标到像素的最终类型分派。 */ + Scalar_Call pixel_to_coordinate; /* 像素到坐标的最终类型分派。 */ + Point_Call point_to_coordinate; /* 二维点到坐标的最终类型分派。 */ + Range_Count_Call pixel_sample_count; /* 像素样本数量的最终类型分派。 */ + Range_Scalar_Call tick_step; /* 主刻度步长的最终类型分派。 */ + Label_Call tick_label; /* 主刻度标签的最终类型分派。 */ + Count_Call sub_tick_count; /* 次刻度数量的最终类型分派。 */ + }; + struct Prepared_Line { + Point_F first{}; /* 线段起点,单位为画布像素。 */ + Point_F second{}; /* 线段终点,单位为画布像素。 */ + }; + struct Prepared_Label { + Point_F position{}; /* 标签左上角位置,单位为画布像素。 */ + std::string text{}; /* 已按最终轴规则格式化的标签文本。 */ + }; + struct Prepared_Axis { + std::vector lines{}; /* 本轮 Prepare 生成的轴线与刻度线。 */ + std::vector labels{}; /* 本轮 Prepare 生成的刻度标签。 */ + Point_F unit_position{}; /* 单位文本左上角位置,单位为画布像素。 */ + double unit_width{}; /* 单位文本背景的估算宽度,单位为像素。 */ + bool valid{}; /* 本轮 Prepare 是否生成了可绘制内容。 */ + }; + const Dispatch* dispatch{}; /* Builder 绑定最终轴类型后指向静态分派表。 */ + Prepared_Axis prepared{}; /* 由当前 State 推导、仅供紧随其后的 Paint 消费。 */ + [[nodiscard]] Axis_Range coordinate_range(const Root* object) const; + [[nodiscard]] double coordinate_to_pixel(const Root* object, double coordinate) const; + [[nodiscard]] double pixel_to_coordinate(const Root* object, double pixel) const; + [[nodiscard]] double point_to_coordinate(const Root* object, Point_F point) const; + [[nodiscard]] int pixel_sample_count(const Root* object, Axis_Range coordinate_range) const; + [[nodiscard]] double tick_step(const Root* object, Axis_Range coordinate_range) const; + [[nodiscard]] std::string tick_label(const Root* object, double tick) const; + [[nodiscard]] int sub_tick_count(const Root* object, double major_step) const; + template + [[nodiscard]] static const Dispatch& dispatch_for(); + /* 供派生轴复用的 1/2/5 十进制主刻度算法。 */ + [[nodiscard]] static double nice_tick_step(Axis_Range coordinate_range); + /* 供派生轴复用的定点数值标签格式化算法。 */ + [[nodiscard]] static std::string localized_number(double value, int precision, Number_Locale locale); + /* CRTP 实现:从最终轴当前 State 生成本轮 Paint 使用的线段与标签。 */ + void prepare_data(Attached auto* object); + /* CRTP 实现:将本轮 Prepared_Axis 绘制到 Color_Cache 的写缓冲。 */ + void paint(Attached auto* object); + /* CRTP 默认:每两个主刻度之间生成 4 个次刻度。 */ + [[nodiscard]] int sub_tick_count(const Attached auto* object, double major_step) const; +}; +template +const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { + static const Dispatch result{ + [](const Root* root) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + return private_data.coordinate_range(object); + }, + [](const Root* root, double coordinate) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + const auto& axis_state = static_cast(*private_data.state.current); + const Axis_Range range = private_data.coordinate_range(object); + const double coordinate_length = range.length(); + const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal + ? axis_state.position.x : axis_state.position.y; + if (coordinate_length == 0.0) return pixel_origin; + return pixel_origin + (coordinate - range.origin) / coordinate_length * axis_state.pixel_length; + }, + [](const Root* root, double pixel) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + const auto& axis_state = static_cast(*private_data.state.current); + const Axis_Range range = private_data.coordinate_range(object); + if (axis_state.pixel_length == 0.0) return range.origin; + const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal + ? axis_state.position.x : axis_state.position.y; + return range.origin + (pixel - pixel_origin) / axis_state.pixel_length * range.length(); + }, + [](const Root* root, Point_F point) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + const auto& axis_state = static_cast(*private_data.state.current); + const double pixel = axis_state.orientation == Axis_Orientation::horizontal ? point.x : point.y; + const Axis_Range range = private_data.coordinate_range(object); + if (axis_state.pixel_length == 0.0) return range.origin; + const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal + ? axis_state.position.x : axis_state.position.y; + return range.origin + (pixel - pixel_origin) / axis_state.pixel_length * range.length(); + }, + [](const Root* root, Axis_Range coordinate_range) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + const auto& axis_state = static_cast(*private_data.state.current); + const Axis_Range range = private_data.coordinate_range(object); + const auto map = [&](double coordinate) { + const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal + ? axis_state.position.x : axis_state.position.y; + if (range.length() == 0.0) return pixel_origin; + return pixel_origin + (coordinate - range.origin) / range.length() * axis_state.pixel_length; + }; + const double first_pixel = map(coordinate_range.origin); + const double last_pixel = map(coordinate_range.target); + return std::max(0, static_cast(std::abs(last_pixel - first_pixel)) + 1); + }, + [](const Root* root, Axis_Range coordinate_range) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + return private_data.tick_step(object, coordinate_range); + }, + [](const Root* root, double tick) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + return private_data.tick_label(object, tick); + }, + [](const Root* root, double major_step) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + return private_data.sub_tick_count(object, major_step); + } + }; + return result; +} +template +void Abs_Axis::bind_private_crtp(Object* object) { + Renderable::bind_private_crtp(object); + static_cast(*object->d).dispatch = &Private::dispatch_for(); +} +inline void Abs_Axis::Private::prepare_data(Attached auto* object) { + using Object = std::remove_pointer_t; + auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.state.current); + auto& output = prepared; + output = {}; + if (state.pixel_length == 0.0 || state.canvas_size.empty()) return; + const Axis_Range coordinates = private_data.coordinate_range(object); + const double step = private_data.tick_step(object, coordinates); + if (!(step > 0.0) || !std::isfinite(step)) return; + const Point_F first = state.position; + const Point_F last = state.orientation == Axis_Orientation::horizontal + ? Point_F{first.x + state.pixel_length, first.y} + : Point_F{first.x, first.y + state.pixel_length}; + output.lines.push_back({first, last}); + const auto [low, high] = std::minmax(coordinates.origin, coordinates.target); + const double initial = std::ceil(low / step) * step; + for (int tick_index = 0; tick_index < 1000; ++tick_index) { + const double tick = initial + tick_index * step; + if (tick > high + step * 1e-6) break; + const double pixel = coordinate_to_pixel(object, tick); + Point_F tick_start{}; + Point_F tick_end{}; + Point_F label{}; + if (state.orientation == Axis_Orientation::horizontal) { + tick_start = {pixel, state.position.y}; + tick_end = {pixel, state.position.y + state.tick_length}; + label = {pixel + 2.0, state.position.y + state.tick_length + 2.0}; + } else { + tick_start = {state.position.x, pixel}; + tick_end = {state.position.x + state.tick_length, pixel}; + label = {state.position.x + state.tick_length + 2.0, pixel - 7.0}; + } + output.lines.push_back({tick_start, tick_end}); + output.labels.push_back({label, private_data.tick_label(object, tick)}); + const int subdivisions = std::max(0, private_data.sub_tick_count(object, step)); + for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) { + const double sub_tick = tick + step * sub_index / (subdivisions + 1.0); + if (sub_tick >= high) break; + const double sub_pixel = coordinate_to_pixel(object, sub_tick); + if (state.orientation == Axis_Orientation::horizontal) + output.lines.push_back({{sub_pixel, state.position.y}, + {sub_pixel, state.position.y + state.sub_tick_length}}); + else + output.lines.push_back({{state.position.x, sub_pixel}, + {state.position.x + state.sub_tick_length, sub_pixel}}); + } + } + if (!state.unit_text.empty()) { + output.unit_position = {last.x + 4.0, last.y + 4.0}; + output.unit_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65); + } + output.valid = true; + object->template mark_dirty(); +} +inline void Abs_Axis::Private::paint(Attached auto* object) { + using Object = std::remove_pointer_t; + auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.state.current); + auto& cache = object->template pending_buffer(); + cache.ensure_size(state.canvas_size); + cache.clear(); + if (!prepared.valid) return; + detail::Painter painter(cache, state.canvas_size); + for (const auto& line : prepared.lines) painter.line(line.first, line.second, state.axis_pen); + for (const auto& label : prepared.labels) + painter.text(label.position, label.text, state.unit_text_font, state.unit_text_pen, + state.label_rotation_degrees); + if (!state.unit_text.empty()) { + painter.rect({prepared.unit_position.x - 2.0, prepared.unit_position.y - 2.0, + prepared.unit_width + 4.0, state.unit_text_font.size * 1.5 + 4.0}, + Pen{.style = Line_Style::none}, state.unit_text_background_brush); + painter.text(prepared.unit_position, state.unit_text, state.unit_text_font, state.unit_text_pen); + } +} +inline int Abs_Axis::Private::sub_tick_count(const Attached auto*, double) const { + return 4; +} +} diff --git a/render_2D/render_2D/axis/Axis.hpp b/render_2D/render_2D/axis/Axis.hpp new file mode 100644 index 0000000..927701c --- /dev/null +++ b/render_2D/render_2D/axis/Axis.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "Abs_Axis.hpp" +#include "Frequency_Axis.hpp" +#include "Numeric_Axis.hpp" +#include "Time_Axis.hpp" diff --git a/render_2D/render_2D/axis/Axis_Types.hpp b/render_2D/render_2D/axis/Axis_Types.hpp new file mode 100644 index 0000000..7212c3a --- /dev/null +++ b/render_2D/render_2D/axis/Axis_Types.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "../base/Types.hpp" +#include +#include +#include +namespace aethera::render_2d { +/* 坐标轴在二维画布中的方向。 */ +enum class Axis_Orientation : std::uint8_t { + horizontal, + vertical +}; +/* 坐标轴上的有向数值区间;origin 到 target 的顺序决定坐标增长方向。 */ +struct Axis_Range { + double origin{}; /* 区间起点坐标。 */ + double target{}; /* 区间终点坐标;可小于 origin 以表达反向坐标轴。 */ + /* 返回不带方向的区间跨度。 */ + [[nodiscard]] double size() const noexcept { + return std::abs(length()); + } + /* 返回保留方向的区间长度。 */ + [[nodiscard]] double length() const noexcept { + return target - origin; + } + /* 返回区间中点。 */ + [[nodiscard]] double center() const noexcept { + return origin + length() * 0.5; + } + /* 判断坐标是否落在区间内;正向和反向区间使用相同边界语义。 */ + [[nodiscard]] bool contains(double coordinate) const noexcept { + const auto [low, high] = std::minmax(origin, target); + return coordinate >= low - 1e-9 && coordinate <= high + 1e-9; + } + bool operator==(const Axis_Range&) const = default; +}; +} diff --git a/render_2D/render_2D/axis/Frequency_Axis.hpp b/render_2D/render_2D/axis/Frequency_Axis.hpp new file mode 100644 index 0000000..7f2fcd7 --- /dev/null +++ b/render_2D/render_2D/axis/Frequency_Axis.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "Numeric_Axis.hpp" +namespace aethera::render_2d { +/* Frequency_Axis 状态标签,用于独立订阅频率轴层。 */ +struct Frequency_Axis_State_Tag {}; +/* 根据数值量级自动选择 Hz、kHz 或 MHz 标签的数值轴。 */ +struct Frequency_Axis : Def> { + /* Frequency_Axis 不发布额外属性。 */ + struct Prop : Prev_Prop {}; + /* Frequency_Axis 没有重复保存数值轴状态,仅保留独立状态层。 */ + struct State : Prev_State { + bool operator==(const State&) const = default; + }; + /* 完整声明及频率标签 CRTP 覆盖见 Frequency_Axis.ipp。 */ + struct Private; +}; +} +#include "Frequency_Axis.ipp" diff --git a/render_2D/render_2D/axis/Frequency_Axis.ipp b/render_2D/render_2D/axis/Frequency_Axis.ipp new file mode 100644 index 0000000..fb14172 --- /dev/null +++ b/render_2D/render_2D/axis/Frequency_Axis.ipp @@ -0,0 +1,16 @@ +#pragma once +namespace aethera::render_2d { +struct Frequency_Axis::Private : Prev_Private { + /* CRTP 覆盖:按绝对量级选择 Hz、kHz 或 MHz,并复用数值轴的小数格式。 */ + [[nodiscard]] std::string tick_label(const Attached auto* object, double tick) const; +}; +inline std::string Frequency_Axis::Private::tick_label(const Attached auto* object, double tick) const { + using Object = std::remove_cv_t>; + const auto& final_private = static_cast(*this); + const auto& state = static_cast(*final_private.state.current); + const double absolute = std::abs(tick); + if (absolute >= 1'000'000.0) return localized_number(tick / 1'000'000.0, state.precision, state.locale) + " MHz"; + if (absolute >= 1'000.0) return localized_number(tick / 1'000.0, state.precision, state.locale) + " kHz"; + return localized_number(tick, state.precision, state.locale) + " Hz"; +} +} diff --git a/render_2D/render_2D/axis/Numeric_Axis.hpp b/render_2D/render_2D/axis/Numeric_Axis.hpp new file mode 100644 index 0000000..3346a81 --- /dev/null +++ b/render_2D/render_2D/axis/Numeric_Axis.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "Abs_Axis.hpp" +namespace aethera::render_2d { +/* Numeric_Axis 状态标签,用于访问和订阅数值轴状态。 */ +struct Numeric_Axis_State_Tag {}; +/* 具有显式数值范围和十进制标签的坐标轴定义层。 */ +struct Numeric_Axis : Def> { + /* Numeric_Axis 不发布额外属性。 */ + struct Prop : Prev_Prop {}; + /* 数值轴的权威状态;范围和标签格式都由该层直接提供。 */ + struct State : Prev_State { + Axis_Range coordinate_range{0.0, 20.0}; /* 当前有向数值区间;必须有限且非零。 */ + int precision{2}; /* 标签最大小数位数;格式化时限制到 0..12。 */ + Number_Locale locale{}; /* 数值标签的小数点规则。 */ + bool wheel_enabled{true}; /* 是否允许滚轮以指针位置为锚点缩放坐标范围。 */ + bool drag_enabled{true}; /* 是否允许按住鼠标左键拖动坐标范围。 */ + bool operator==(const State&) const = default; + }; + /* 完整声明及数值轴 CRTP 能力见 Numeric_Axis.ipp。 */ + struct Private; +}; +} +#include "Numeric_Axis.ipp" diff --git a/render_2D/render_2D/axis/Numeric_Axis.ipp b/render_2D/render_2D/axis/Numeric_Axis.ipp new file mode 100644 index 0000000..e82ef0e --- /dev/null +++ b/render_2D/render_2D/axis/Numeric_Axis.ipp @@ -0,0 +1,112 @@ +#pragma once +#include +#include +#include +namespace aethera::render_2d { +struct Numeric_Axis::Private : Prev_Private { + std::mutex interaction_mutex{}; /* 保护跨事件保留的拖动手势状态。 */ + bool dragging{}; /* 左键拖动手势是否已经开始且尚未释放。 */ + Point_F last_pointer{}; /* 上一个拖动事件的位置,单位为画布局部像素。 */ + /* CRTP 实现:直接返回 Numeric_Axis_State_Tag 中唯一保存的数值范围。 */ + [[nodiscard]] Axis_Range coordinate_range(const Attached auto* object) const; + /* CRTP 实现:使用 Abs_Axis::Private 的 1/2/5 十进制算法计算主刻度。 */ + [[nodiscard]] double tick_step(const Attached auto* object, Axis_Range coordinate_range) const; + /* CRTP 可覆盖:按 Numeric_Axis 当前 precision 和 locale 格式化标签;Frequency_Axis 覆盖该能力。 */ + [[nodiscard]] std::string tick_label(const Attached auto* object, double tick) const; + /* CRTP 实现:处理滚轮缩放和左键拖动,并在消费事件后标记 Prepare dirty。 */ + void handle_event(Attached auto* object, const Event& event); + /* CRTP State 钩子:拒绝非有限、零长度范围以及 0..12 之外的精度,并恢复本次无效写入。 */ + template + void after_state_set(Object* object, Member Owner::* member, State_Access pending_states); +}; +inline Axis_Range Numeric_Axis::Private::coordinate_range(const Attached auto* object) const { + using Object = std::remove_cv_t>; + const auto& private_data = static_cast(*this); + return static_cast(*private_data.state.current).coordinate_range; +} +inline double Numeric_Axis::Private::tick_step(const Attached auto*, Axis_Range coordinate_range) const { + return nice_tick_step(coordinate_range); +} +inline std::string Numeric_Axis::Private::tick_label(const Attached auto* object, double tick) const { + using Object = std::remove_cv_t>; + const auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.state.current); + return localized_number(tick, state.precision, state.locale); +} +inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Event& event) { + using Object = std::remove_pointer_t; + auto& private_data = static_cast(*this); + const auto& numeric_state = static_cast(*private_data.state.current); + const auto& axis_state = static_cast(*private_data.state.current); + const auto* pointer = dynamic_cast(&event); + if (event.type == Event_Type::wheel && numeric_state.wheel_enabled) { + const auto* wheel = dynamic_cast(&event); + if (!pointer || !wheel) return; + const double anchor_pixel = axis_state.orientation == Axis_Orientation::horizontal + ? pointer->position_x() : pointer->position_y(); + const double anchor = pixel_to_coordinate(object, anchor_pixel); + const double factor = wheel->angle_delta_y_value() >= 0.0 ? 0.9 : 1.1; + const Axis_Range range = numeric_state.coordinate_range; + object->template update_state<&State::coordinate_range>(Axis_Range{ + anchor + (range.origin - anchor) * factor, + anchor + (range.target - anchor) * factor + }); + object->template mark_dirty(); + event.accept(); + return; + } + if (!numeric_state.drag_enabled || !pointer) return; + if (event.type == Event_Type::pointer_press && pointer->pointer_button() == Mouse_Button::left) { + std::lock_guard guard(interaction_mutex); + dragging = true; + last_pointer = {pointer->position_x(), pointer->position_y()}; + event.accept(); + return; + } + if (event.type == Event_Type::pointer_move) { + Point_F previous{}; + const Point_F current{pointer->position_x(), pointer->position_y()}; + { + std::lock_guard guard(interaction_mutex); + if (!dragging) return; + previous = last_pointer; + last_pointer = current; + } + const double delta = axis_state.orientation == Axis_Orientation::horizontal + ? current.x - previous.x : current.y - previous.y; + const double shift = axis_state.pixel_length == 0.0 + ? 0.0 : -delta * numeric_state.coordinate_range.length() / axis_state.pixel_length; + object->template update_state<&State::coordinate_range>(Axis_Range{ + numeric_state.coordinate_range.origin + shift, + numeric_state.coordinate_range.target + shift + }); + object->template mark_dirty(); + event.accept(); + return; + } + if (event.type == Event_Type::pointer_release) { + std::lock_guard guard(interaction_mutex); + const bool was_dragging = dragging; + dragging = false; + if (was_dragging) event.accept(); + } +} +template +void Numeric_Axis::Private::after_state_set(Object*, Member Owner::* member, State_Access pending_states) { + const auto& private_data = static_cast(*this); + auto& pending = pending_states.template get(); + const auto& current = static_cast(*private_data.state.current); + if constexpr (std::same_as && std::same_as) { + if (member != &State::coordinate_range) return; + const Axis_Range range = pending.coordinate_range; + if (std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0) return; + pending.coordinate_range = current.coordinate_range; + throw std::invalid_argument("numeric axis coordinate range must be finite and non-empty"); + } + else if constexpr (std::same_as && std::same_as) { + if (member != &State::precision || (pending.precision >= 0 && pending.precision <= 12)) return; + pending.precision = current.precision; + throw std::invalid_argument("numeric axis precision must be between 0 and 12"); + } +} +} diff --git a/render_2D/render_2D/axis/Time_Axis.cpp b/render_2D/render_2D/axis/Time_Axis.cpp new file mode 100644 index 0000000..de1696d --- /dev/null +++ b/render_2D/render_2D/axis/Time_Axis.cpp @@ -0,0 +1,59 @@ +#include "Time_Axis.hpp" +#include +#include +namespace aethera::render_2d { +std::size_t Time_Axis::Private::time_point_count(const Root* object) const { + return time_dispatch->time_point_count(object); +} +int Time_Axis::Private::append_time(Root* object, Time_Of_Day time) { + return time_dispatch->append_time(object, time); +} +Time_Of_Day Time_Axis::Private::tick_to_time(const Root* object, int tick) const { + return time_dispatch->tick_to_time(object, tick); +} +std::string Time_Axis::Private::formatted_time(Time_Of_Day time, std::string_view format) { + const auto digits = [](int value, int width) { + std::ostringstream stream; + stream << std::setfill('0') << std::setw(width) << value; + return stream.str(); + }; + const auto total = time.milliseconds; + const int hours = static_cast((total / 3'600'000) % 24); + const int minutes = static_cast((total / 60'000) % 60); + const int seconds = static_cast((total / 1'000) % 60); + const int milliseconds = static_cast(total % 1'000); + std::string result; + for (std::size_t index = 0; index < format.size();) { + const std::string_view rest = format.substr(index); + if (rest.starts_with("zzz")) { + result += digits(milliseconds, 3); + index += 3; + } + else if (rest.starts_with("hh") || rest.starts_with("HH")) { + result += digits(hours, 2); + index += 2; + } + else if (rest.starts_with("mm")) { + result += digits(minutes, 2); + index += 2; + } + else if (rest.starts_with("ss")) { + result += digits(seconds, 2); + index += 2; + } + else { + result.push_back(format[index++]); + } + } + return result; +} +std::size_t Time_Axis::time_point_count() const { + return static_cast(*d).time_point_count(this); +} +int Time_Axis::append_time(Time_Of_Day time) { + return static_cast(*d).append_time(this, time); +} +Time_Of_Day Time_Axis::tick_to_time(int tick) const { + return static_cast(*d).tick_to_time(this, tick); +} +} diff --git a/render_2D/render_2D/axis/Time_Axis.hpp b/render_2D/render_2D/axis/Time_Axis.hpp new file mode 100644 index 0000000..cf6cbff --- /dev/null +++ b/render_2D/render_2D/axis/Time_Axis.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "Abs_Axis.hpp" +#include +#include +#include +#include +namespace aethera::render_2d { +/* Time_Axis 状态标签,用于访问和订阅时间轴状态。 */ +struct Time_Axis_State_Tag {}; +/* 将连续样本序号显示为一天内时间文本的坐标轴。 */ +struct Time_Axis : Def> { + /* Time_Axis 不发布额外属性。 */ + struct Prop : Prev_Prop {}; + /* 时间轴样本与显示参数的唯一权威状态。 */ + struct State : Prev_State { + int visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */ + double tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */ + double estimated_label_width_px{48.0}; /* 当前字体下单个时间标签的估算像素宽度;小于 48 时按 48 计算。 */ + std::string format{"mm:ss.zzz"}; /* 标签格式;支持 hh、HH、mm、ss 和 zzz。 */ + bool newest_at_start{}; /* true 时最新样本位于坐标区间起点。 */ + int next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */ + std::deque> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */ + bool operator==(const State&) const = default; + }; + /* 完整声明、时间轴 CRTP 能力及公开薄壳分派见 Time_Axis.ipp。 */ + struct Private; + /* 返回当前已提交时间样本数量。 */ + [[nodiscard]] std::size_t time_point_count() const; + /* 向提交侧追加时间并返回分配的 tick;结果在下一次 advance 后进入当前读面。 */ + int append_time(Time_Of_Day time); + /* 在当前已提交样本中查找 tick;不存在时返回无效时间。 */ + [[nodiscard]] Time_Of_Day tick_to_time(int tick) const; +protected: + /* Builder 挂接最终 Private 后,同时绑定通用轴分派和时间轴公开接口分派。 */ + template + void bind_private_crtp(Object* object); +}; +} +#include "Time_Axis.ipp" diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp new file mode 100644 index 0000000..8b1ddb3 --- /dev/null +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -0,0 +1,95 @@ +#pragma once +#include +#include +namespace aethera::render_2d { +struct Time_Axis::Private : Prev_Private { + /* CRTP 实现:由 next_tick、visible_count 和 newest_at_start 即时计算范围,不保存镜像坐标区间。 */ + [[nodiscard]] Axis_Range coordinate_range(const Attached auto* object) const; + /* CRTP 实现:根据像素长度、估算标签宽度和间距计算至少为 1 的整数刻度步长。 */ + [[nodiscard]] double tick_step(const Attached auto* object, Axis_Range coordinate_range) const; + /* CRTP 实现:在当前样本中查找 tick,并按 State::format 生成时间标签。 */ + [[nodiscard]] std::string tick_label(const Attached auto* object, double tick) const; + using Count_Call = std::size_t (*)(const Root*); + using Append_Call = int (*)(Root*, Time_Of_Day); + using Lookup_Call = Time_Of_Day (*)(const Root*, int); + struct Time_Dispatch { + Count_Call time_point_count; /* 读取当前样本数量的最终类型分派。 */ + Append_Call append_time; /* 追加提交侧时间样本的最终类型分派。 */ + Lookup_Call tick_to_time; /* 查询当前 tick 时间的最终类型分派。 */ + }; + const Time_Dispatch* time_dispatch{}; /* Builder 绑定最终时间轴类型后指向静态分派表。 */ + [[nodiscard]] std::size_t time_point_count(const Root* object) const; + int append_time(Root* object, Time_Of_Day time); + [[nodiscard]] Time_Of_Day tick_to_time(const Root* object, int tick) const; + template + [[nodiscard]] static const Time_Dispatch& time_dispatch_for(); + /* 将一天内时间按支持的占位符格式化;未知字符原样保留。 */ + [[nodiscard]] static std::string formatted_time(Time_Of_Day time, std::string_view format); +}; +inline Axis_Range Time_Axis::Private::coordinate_range(const Attached auto* object) const { + using Object = std::remove_cv_t>; + const auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.state.current); + const int latest = std::max(1, state.next_tick - 1); + const int earliest = std::max(0, latest - std::max(2, state.visible_count) + 1); + if (state.newest_at_start) return {static_cast(latest), static_cast(earliest)}; + return {static_cast(earliest), static_cast(latest)}; +} +inline double Time_Axis::Private::tick_step(const Attached auto* object, Axis_Range coordinate_range) const { + using Object = std::remove_cv_t>; + const auto& private_data = static_cast(*this); + const auto& time_state = static_cast(*private_data.state.current); + const auto& axis_state = static_cast(*private_data.state.current); + const double label_width = std::max(48.0, time_state.estimated_label_width_px); + const double label_count = std::max(1.0, axis_state.pixel_length / (label_width + std::max(0.0, time_state.tick_label_spacing_px))); + return std::max(1.0, std::ceil(coordinate_range.size() / label_count)); +} +inline std::string Time_Axis::Private::tick_label(const Attached auto* object, double tick) const { + using Object = std::remove_cv_t>; + const auto& private_data = static_cast(*this); + const auto& state = static_cast(*private_data.state.current); + const int target = static_cast(std::llround(tick)); + const auto current = std::find_if(state.samples.begin(), state.samples.end(), [target](const auto& sample) { + return sample.first == target; + }); + return current == state.samples.end() ? std::string{} : formatted_time(current->second, state.format); +} +template +const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() { + static const Time_Dispatch result{ + [](const Root* root) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + const auto& state = static_cast(*private_data.state.current); + return state.samples.size(); + }, + [](Root* root, Time_Of_Day time) { + auto* object = static_cast(root); + int tick{}; + object->template update_state<&State::next_tick, &State::samples>([&](State_Access states) { + auto& state = states.template get(); + tick = state.next_tick++; + state.samples.emplace_back(tick, time); + const auto limit = static_cast(std::max(512, std::max(2, state.visible_count) * 4)); + while (state.samples.size() > limit) state.samples.pop_front(); + }); + return tick; + }, + [](const Root* root, int tick) { + auto* object = static_cast(root); + const auto& private_data = static_cast(*object->d); + const auto& state = static_cast(*private_data.state.current); + const auto current = std::find_if(state.samples.begin(), state.samples.end(), [tick](const auto& sample) { + return sample.first == tick; + }); + return current == state.samples.end() ? Time_Of_Day{} : current->second; + } + }; + return result; +} +template +void Time_Axis::bind_private_crtp(Object* object) { + Abs_Axis::bind_private_crtp(object); + static_cast(*object->d).time_dispatch = &Private::time_dispatch_for(); +} +} diff --git a/render_2D/render_2D/base/Types.hpp b/render_2D/render_2D/base/Types.hpp new file mode 100644 index 0000000..08f2be1 --- /dev/null +++ b/render_2D/render_2D/base/Types.hpp @@ -0,0 +1,165 @@ +#pragma once +#include +#include +#include +#include +namespace aethera::render_2d { +/* 整数二维坐标。 */ +struct Point { + int x{}; /* 水平方向坐标。 */ + int y{}; /* 垂直方向坐标。 */ + bool operator==(const Point&) const = default; +}; +/* 浮点二维坐标。 */ +struct Point_F { + double x{}; /* 水平方向坐标。 */ + double y{}; /* 垂直方向坐标。 */ + bool operator==(const Point_F&) const = default; +}; +/* 整数二维尺寸。 */ +struct Size { + int width{}; /* 水平方向像素数。 */ + int height{}; /* 垂直方向像素数。 */ + [[nodiscard]] bool empty() const noexcept { + return width <= 0 || height <= 0; + } + bool operator==(const Size&) const = default; +}; +/* 整数矩形。 */ +struct Rect { + int x{}; /* 左上角水平坐标。 */ + int y{}; /* 左上角垂直坐标。 */ + int width{}; /* 矩形宽度。 */ + int height{}; /* 矩形高度。 */ + [[nodiscard]] bool empty() const noexcept { + return width <= 0 || height <= 0; + } + [[nodiscard]] int right() const noexcept { + return x + width; + } + [[nodiscard]] int bottom() const noexcept { + return y + height; + } + [[nodiscard]] bool contains(Point point) const noexcept { + return point.x >= x && point.x <= right() && point.y >= y && point.y <= bottom(); + } + bool operator==(const Rect&) const = default; +}; +/* 浮点矩形。 */ +struct Rect_F { + double x{}; /* 左上角水平坐标。 */ + double y{}; /* 左上角垂直坐标。 */ + double width{}; /* 矩形宽度;可为负数表达反向输入。 */ + double height{}; /* 矩形高度;可为负数表达反向输入。 */ + [[nodiscard]] bool empty() const noexcept { + return width <= 0.0 || height <= 0.0; + } + [[nodiscard]] double right() const noexcept { + return x + width; + } + [[nodiscard]] double bottom() const noexcept { + return y + height; + } + [[nodiscard]] bool contains(Point_F point) const noexcept { + return point.x >= x && point.x <= right() && point.y >= y && point.y <= bottom(); + } + [[nodiscard]] Rect_F normalized() const noexcept { + Rect_F result = *this; + if (result.width < 0.0) { + result.x += result.width; + result.width = -result.width; + } + if (result.height < 0.0) { + result.y += result.height; + result.height = -result.height; + } + return result; + } + bool operator==(const Rect_F&) const = default; +}; +/* 非预乘 RGBA 颜色。 */ +struct Color { + std::uint8_t red{}; /* 红色通道。 */ + std::uint8_t green{}; /* 绿色通道。 */ + std::uint8_t blue{}; /* 蓝色通道。 */ + std::uint8_t alpha{255}; /* 不透明度通道。 */ + [[nodiscard]] static constexpr Color transparent() noexcept { return {0, 0, 0, 0}; } + [[nodiscard]] static constexpr Color black() noexcept { return {0, 0, 0, 255}; } + [[nodiscard]] static constexpr Color white() noexcept { return {255, 255, 255, 255}; } + [[nodiscard]] static constexpr Color red_color() noexcept { return {255, 0, 0, 255}; } + [[nodiscard]] static constexpr Color green_color() noexcept { return {0, 255, 0, 255}; } + [[nodiscard]] static constexpr Color yellow() noexcept { return {255, 255, 0, 255}; } + bool operator==(const Color&) const = default; +}; +using Pixel = std::uint32_t; +[[nodiscard]] constexpr Pixel premultiply(Color color) noexcept { + const auto red = static_cast((color.red * color.alpha + 127) / 255); + const auto green = static_cast((color.green * color.alpha + 127) / 255); + const auto blue = static_cast((color.blue * color.alpha + 127) / 255); + return (static_cast(color.alpha) << 24) | (red << 16) | (green << 8) | blue; +} +[[nodiscard]] constexpr Pixel pack_rgba(std::uint8_t red, + std::uint8_t green, + std::uint8_t blue, + std::uint8_t alpha = 255) noexcept { + return premultiply(Color{red, green, blue, alpha}); +} +enum class Line_Style : std::uint8_t { none, solid, dash, dot }; +enum class Line_Cap : std::uint8_t { butt, square, round }; +enum class Line_Join : std::uint8_t { miter, bevel, round }; +/* 线条和文字前景样式。 */ +struct Pen { + Color color{Color::white()}; /* 描边或文字颜色。 */ + double width{1.0}; /* 描边宽度,单位为像素。 */ + Line_Style style{Line_Style::solid}; /* 描边图案;none 表示禁用。 */ + Line_Cap cap{Line_Cap::butt}; /* 线段端点样式。 */ + Line_Join join{Line_Join::miter}; /* 折线连接样式。 */ + [[nodiscard]] bool enabled() const noexcept { + return style != Line_Style::none && width > 0.0 && color.alpha != 0; + } + bool operator==(const Pen&) const = default; +}; +enum class Brush_Style : std::uint8_t { none, solid }; +/* 区域填充样式。 */ +struct Brush { + Color color{Color::transparent()}; /* 填充颜色。 */ + Brush_Style style{Brush_Style::none}; /* 填充模式;none 表示禁用。 */ + [[nodiscard]] bool enabled() const noexcept { + return style != Brush_Style::none && color.alpha != 0; + } + bool operator==(const Brush&) const = default; +}; +/* 字体选择参数。 */ +struct Font { + double size{12.0}; /* 字号,单位为像素。 */ + int weight{400}; /* 字重;600 及以上选择粗体。 */ + bool italic{}; /* 是否选择斜体字形。 */ + bool operator==(const Font&) const = default; +}; +/* 数值标签使用的小数点规则。 */ +struct Number_Locale { + char decimal_point{'.'}; /* 替换默认小数点的字符。 */ + bool operator==(const Number_Locale&) const = default; +}; +/* 一天内的时间值。 */ +struct Time_Of_Day { + std::int64_t milliseconds{-1}; /* 自当天零点起的毫秒数;负值表示无效。 */ + [[nodiscard]] bool valid() const noexcept { + return milliseconds >= 0 && milliseconds < 24LL * 60LL * 60LL * 1000LL; + } + bool operator==(const Time_Of_Day&) const = default; +}; +enum class Image_Interpolation_Mode : std::uint8_t { nearest, bilinear, bicubic }; +enum class Pixel_Format : std::uint8_t { premultiplied_32 }; +/* 只读图像像素视图。 */ +struct Image_View { + const std::byte* data{}; /* 首行像素地址;不拥有内存。 */ + int width{}; /* 图像宽度,单位为像素。 */ + int height{}; /* 图像高度,单位为像素。 */ + int stride{}; /* 相邻两行起点的字节距离。 */ + Pixel_Format format{Pixel_Format::premultiplied_32}; /* 像素存储格式。 */ + [[nodiscard]] bool empty() const noexcept { + return data == nullptr || width <= 0 || height <= 0; + } +}; +} diff --git a/render_2D/render_2D/event/Event.hpp b/render_2D/render_2D/event/Event.hpp new file mode 100644 index 0000000..63fbeda --- /dev/null +++ b/render_2D/render_2D/event/Event.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "../base/Types.hpp" +#include +namespace aethera::render_2d { +using Pointer_Event = Basic_Pointer_Event; +using Wheel_Event = Basic_Wheel_Event; +using Resize_Event = Basic_Resize_Event; +} diff --git a/render_2D/render_2D/main.cpp b/render_2D/render_2D/main.cpp index 33c14ce..3604681 100644 --- a/render_2D/render_2D/main.cpp +++ b/render_2D/render_2D/main.cpp @@ -1,3 +1 @@ -int main() { - return 0; -} +#include "axis/Axis.hpp" diff --git a/render_2D/render_2D/render/Blend2D_Cache.cpp b/render_2D/render_2D/render/Blend2D_Cache.cpp new file mode 100644 index 0000000..9d233b3 --- /dev/null +++ b/render_2D/render_2D/render/Blend2D_Cache.cpp @@ -0,0 +1,286 @@ +#include "Blend2D_Cache.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace aethera::render_2d { +void Blend2D_Cache::Private::require_success(BLResult result, const char* operation) { + if (result == BL_SUCCESS) return; + throw std::runtime_error(std::string(operation) + " failed with Blend2D result " + std::to_string(result)); +} +void Blend2D_Cache::Private::clear_image(BLImage& image) { + if (image.is_empty()) return; + BLImageData data{}; + require_success(image.get_data(&data), "read Blend2D image data"); + for (int y = 0; y < data.size.h; ++y) { + auto* row = static_cast(data.pixel_data) + static_cast(y) * data.stride; + std::memset(row, 0, static_cast(data.size.w) * sizeof(Pixel)); + } +} +Blend2D_Cache::Private::Private(const Private& other) { + require_success(image.assign_deep(other.image), "copy Blend2D image"); +} +Blend2D_Cache::Private& Blend2D_Cache::Private::operator=(const Private& other) { + if (this == &other) return *this; + BLImage copy; + require_success(copy.assign_deep(other.image), "copy Blend2D image"); + image = std::move(copy); + return *this; +} +Blend2D_Cache::Blend2D_Cache() : d(std::make_unique()) {} +Blend2D_Cache::~Blend2D_Cache() = default; +Blend2D_Cache::Blend2D_Cache(const Blend2D_Cache& other) : d(std::make_unique(*other.d)) {} +Blend2D_Cache& Blend2D_Cache::operator=(const Blend2D_Cache& other) { + if (this != &other) d = std::make_unique(*other.d); + return *this; +} +Blend2D_Cache::Blend2D_Cache(Blend2D_Cache&& other) noexcept = default; +Blend2D_Cache& Blend2D_Cache::operator=(Blend2D_Cache&& other) noexcept = default; +void Blend2D_Cache::clear() { + Private::clear_image(d->image); +} +void Blend2D_Cache::composite(const Blend2D_Cache& source) { + if (source.d->image.is_empty()) return; + const Size source_size = source.size(); + if (d->image.is_empty() || size() != source_size) { + ensure_size(source_size); + clear(); + } + BLContext context; + Private::require_success(context.begin(d->image), "begin Blend2D cache composite"); + context.set_comp_op(BL_COMP_OP_SRC_OVER); + Private::require_success( + context.blit_image(BLRect(0.0, 0.0, source_size.width, source_size.height), + source.d->image, + BLRectI(0, 0, source_size.width, source_size.height)), + "composite Blend2D cache"); + Private::require_success(context.end(), "end Blend2D cache composite"); +} +void Blend2D_Cache::ensure_size(Size requested) { + if (requested.empty()) { + d->image.reset(); + return; + } + if (size() == requested) return; + Private::require_success(d->image.create(requested.width, requested.height, BL_FORMAT_PRGB32), + "create Blend2D cache"); + clear(); +} +Size Blend2D_Cache::size() const noexcept { + return {d->image.width(), d->image.height()}; +} +Image_View Blend2D_Cache::view() const { + if (d->image.is_empty()) return {}; + BLImageData data{}; + Private::require_success(d->image.get_data(&data), "read Blend2D cache"); + return {static_cast(data.pixel_data), data.size.w, data.size.h, + static_cast(data.stride), Pixel_Format::premultiplied_32}; +} +namespace detail { +void Painter::Private::load_font_face(BLFontFace& face, std::initializer_list candidates) { + for (const char* path : candidates) { + std::error_code error; + if (std::filesystem::exists(path, error) && face.create_from_file(path) == BL_SUCCESS) return; + } +} +BLFontFace& Painter::Private::default_font_face(const Font& font) { + static std::array faces; + static std::once_flag once; + std::call_once(once, [] { + load_font_face(faces[0], {"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simsun.ttc", + "C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf"}); + load_font_face(faces[1], {"C:/Windows/Fonts/msyhbd.ttc", "C:/Windows/Fonts/segoeuib.ttf", + "C:/Windows/Fonts/arialbd.ttf"}); + load_font_face(faces[2], {"C:/Windows/Fonts/segoeuii.ttf", "C:/Windows/Fonts/ariali.ttf"}); + load_font_face(faces[3], {"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arialbi.ttf"}); + }); + const std::size_t index = (font.weight >= 600 ? 1u : 0u) | (font.italic ? 2u : 0u); + return faces[index] ? faces[index] : faces[0]; +} +BLRgba Painter::Private::rgba(Color color) noexcept { + constexpr double scale = 1.0 / 255.0; + return BLRgba(color.red * scale, color.green * scale, color.blue * scale, color.alpha * scale); +} +BLStrokeCap Painter::Private::stroke_cap(Line_Cap cap) noexcept { + switch (cap) { + case Line_Cap::square: return BL_STROKE_CAP_SQUARE; + case Line_Cap::round: return BL_STROKE_CAP_ROUND; + default: return BL_STROKE_CAP_BUTT; + } +} +BLStrokeJoin Painter::Private::stroke_join(Line_Join join) noexcept { + switch (join) { + case Line_Join::bevel: return BL_STROKE_JOIN_BEVEL; + case Line_Join::round: return BL_STROKE_JOIN_ROUND; + default: return BL_STROKE_JOIN_MITER_CLIP; + } +} +void Painter::Private::apply_pen(const Pen& pen) { + context.set_stroke_style(rgba(pen.color)); + context.set_stroke_width(std::max(0.1, pen.width)); + context.set_stroke_caps(stroke_cap(pen.cap)); + context.set_stroke_join(stroke_join(pen.join)); + BLArray pattern; + if (pen.style == Line_Style::dash) { + pattern.append(std::max(1.0, pen.width * 4.0)); + pattern.append(std::max(1.0, pen.width * 2.0)); + } else if (pen.style == Line_Style::dot) { + pattern.append(std::max(1.0, pen.width)); + pattern.append(std::max(1.0, pen.width * 2.0)); + } + context.set_stroke_dash_array(pattern); +} +BLFont Painter::Private::make_font(const Font& font) { + BLFont result; + auto& face = default_font_face(font); + if (face) Blend2D_Cache::Private::require_success( + result.create_from_face(face, static_cast(std::max(1.0, font.size))), "create Blend2D font"); + return result; +} +double Painter::Private::cubic_weight(double distance) noexcept { + constexpr double a = -0.5; + distance = std::abs(distance); + if (distance < 1.0) return (a + 2.0) * distance * distance * distance - (a + 3.0) * distance * distance + 1.0; + if (distance < 2.0) return a * distance * distance * distance - 5.0 * a * distance * distance + 8.0 * a * distance - 4.0 * a; + return 0.0; +} +std::uint8_t Painter::Private::pixel_channel(Pixel pixel, int shift) noexcept { + return static_cast((pixel >> shift) & 0xffu); +} +std::vector Painter::Private::bicubic_resample(std::span source, int source_width, + int source_height, int destination_width, + int destination_height) { + std::vector destination(static_cast(destination_width) * destination_height); + for (int y = 0; y < destination_height; ++y) { + const double source_y = (y + 0.5) * source_height / destination_height - 0.5; + const int base_y = static_cast(std::floor(source_y)); + for (int x = 0; x < destination_width; ++x) { + const double source_x = (x + 0.5) * source_width / destination_width - 0.5; + const int base_x = static_cast(std::floor(source_x)); + double channels[4]{}; + double total_weight{}; + for (int offset_y = -1; offset_y <= 2; ++offset_y) { + const int sample_y = std::clamp(base_y + offset_y, 0, source_height - 1); + const double weight_y = cubic_weight(source_y - base_y - offset_y); + for (int offset_x = -1; offset_x <= 2; ++offset_x) { + const int sample_x = std::clamp(base_x + offset_x, 0, source_width - 1); + const double weight = weight_y * cubic_weight(source_x - base_x - offset_x); + const Pixel pixel = source[static_cast(sample_y) * source_width + sample_x]; + for (int channel = 0; channel < 4; ++channel) + channels[channel] += pixel_channel(pixel, channel * 8) * weight; + total_weight += weight; + } + } + const auto channel = [total_weight](double value) { + if (total_weight != 0.0) value /= total_weight; + return static_cast(std::clamp(std::lround(value), 0L, 255L)); + }; + destination[static_cast(y) * destination_width + x] = channel(channels[0]) | + (channel(channels[1]) << 8) | (channel(channels[2]) << 16) | (channel(channels[3]) << 24); + } + } + return destination; +} +Painter::Painter(Blend2D_Cache& cache, Size size) : d(std::make_unique()) { + cache.ensure_size(size); + if (cache.d->image.is_empty()) return; + Blend2D_Cache::Private::require_success(d->context.begin(cache.d->image), "begin Blend2D painter"); + d->context.set_comp_op(BL_COMP_OP_SRC_OVER); + d->active = true; +} +Painter::~Painter() { + if (d && d->active) d->context.end(); +} +Painter::Clip_Scope::Clip_Scope(Painter& owner, Rect_F rect) { + rect = rect.normalized(); + if (!owner.d->active || rect.empty()) return; + owner.d->context.save(); + owner.d->context.clip_to_rect(BLRect(rect.x, rect.y, rect.width, rect.height)); + painter = &owner; +} +Painter::Clip_Scope::~Clip_Scope() { + if (painter) painter->d->context.restore(); +} +Painter::Clip_Scope::Clip_Scope(Clip_Scope&& other) noexcept : painter(std::exchange(other.painter, nullptr)) {} +Painter::operator bool() const noexcept { return d->active; } +Painter::Clip_Scope Painter::scoped_clip(Rect_F rect) { return Clip_Scope(*this, rect); } +void Painter::clear() { if (d->active) d->context.clear_all(); } +void Painter::composite(const Blend2D_Cache& source) { + if (!d->active || source.d->image.is_empty()) return; + const Size value = source.size(); + d->context.blit_image(BLRect(0, 0, value.width, value.height), source.d->image, + BLRectI(0, 0, value.width, value.height)); +} +void Painter::line(Point_F first, Point_F second, const Pen& pen) { + if (!d->active || !pen.enabled()) return; + d->apply_pen(pen); d->context.stroke_line(first.x, first.y, second.x, second.y); +} +void Painter::polyline(std::span points, const Pen& pen) { + if (!d->active || !pen.enabled() || points.size() < 2) return; + BLPath path; path.move_to(points.front().x, points.front().y); + for (std::size_t index = 1; index < points.size(); ++index) path.line_to(points[index].x, points[index].y); + d->apply_pen(pen); d->context.stroke_path(path); +} +void Painter::polygon(std::span points, const Pen& pen, const Brush& brush) { + if (!d->active || points.size() < 3) return; + BLPath path; path.move_to(points.front().x, points.front().y); + for (std::size_t index = 1; index < points.size(); ++index) path.line_to(points[index].x, points[index].y); + path.close(); + if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_path(path); } + if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_path(path); } +} +void Painter::rect(Rect_F value, const Pen& pen, const Brush& brush) { + value = value.normalized(); if (!d->active || value.empty()) return; + const BLRect shape(value.x, value.y, value.width, value.height); + if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_rect(shape); } + if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_rect(shape); } +} +void Painter::circle(Point_F center, double radius, const Pen& pen, const Brush& brush) { + if (!d->active || radius <= 0.0) return; + BLPath path; path.add_ellipse(BLEllipse(center.x, center.y, radius, radius)); + if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_path(path); } + if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_path(path); } +} +void Painter::text(Point_F position, std::string_view value, const Font& font, const Pen& pen, + double rotation_degrees) { + if (!d->active || value.empty() || !pen.enabled()) return; + BLFont bl_font = Private::make_font(font); if (!bl_font) return; + d->context.set_fill_style(Private::rgba(pen.color)); + const double baseline = position.y + bl_font.metrics().ascent; + const bool rotated = std::isfinite(rotation_degrees) && rotation_degrees != 0.0; + if (rotated) { d->context.save(); d->context.rotate(rotation_degrees * std::numbers::pi / 180.0, position.x, baseline); } + d->context.fill_utf8_text(BLPoint(position.x, baseline), bl_font, value.data(), value.size()); + if (rotated) d->context.restore(); +} +void Painter::heatmap(Rect_F target, int width, int height, std::span pixels, + Image_Interpolation_Mode interpolation) { + target = target.normalized(); + if (!d->active || target.empty() || width <= 0 || height <= 0 || + pixels.size() < static_cast(width) * height) return; + std::vector resampled; + if (interpolation == Image_Interpolation_Mode::bicubic && + target.width <= std::numeric_limits::max() && target.height <= std::numeric_limits::max()) { + const int destination_width = std::max(1, static_cast(std::lround(target.width))); + const int destination_height = std::max(1, static_cast(std::lround(target.height))); + resampled = Private::bicubic_resample(pixels, width, height, destination_width, destination_height); + pixels = resampled; width = destination_width; height = destination_height; + } + BLImage image(width, height, BL_FORMAT_PRGB32); BLImageData image_data{}; + Blend2D_Cache::Private::require_success(image.get_data(&image_data), "write Blend2D heatmap image"); + for (int y = 0; y < height; ++y) { + auto* destination = reinterpret_cast(static_cast(image_data.pixel_data) + + static_cast(y) * image_data.stride); + std::copy_n(pixels.data() + static_cast(y) * width, width, destination); + } + d->context.set_pattern_quality(interpolation == Image_Interpolation_Mode::bilinear + ? BL_PATTERN_QUALITY_BILINEAR : BL_PATTERN_QUALITY_NEAREST); + d->context.blit_image(BLRect(target.x, target.y, target.width, target.height), image, + BLRectI(0, 0, width, height)); +} +} +} diff --git a/render_2D/render_2D/render/Blend2D_Cache.hpp b/render_2D/render_2D/render/Blend2D_Cache.hpp new file mode 100644 index 0000000..a43eae5 --- /dev/null +++ b/render_2D/render_2D/render/Blend2D_Cache.hpp @@ -0,0 +1,75 @@ +#pragma once +#include "../base/Types.hpp" +#include +#include +#include +namespace aethera::render_2d { +namespace detail { +struct Painter; +} +/* 可作为双缓冲值深拷贝的 Blend2D 像素缓存。 */ +struct Blend2D_Cache { + struct Private; + Blend2D_Cache(); + ~Blend2D_Cache(); + Blend2D_Cache(const Blend2D_Cache& other); + Blend2D_Cache& operator=(const Blend2D_Cache& other); + Blend2D_Cache(Blend2D_Cache&& other) noexcept; + Blend2D_Cache& operator=(Blend2D_Cache&& other) noexcept; + /* 清空为透明像素,保持当前尺寸。 */ + void clear(); + /* 以 source-over 方式合成同尺寸或不同尺寸的源缓存。 */ + void composite(const Blend2D_Cache& source); + /* 将缓存调整到指定尺寸;空尺寸释放图像。 */ + void ensure_size(Size size); + /* 返回当前图像尺寸。 */ + [[nodiscard]] Size size() const noexcept; + /* 返回由缓存拥有、在下次非 const 修改前有效的只读像素视图。 */ + [[nodiscard]] Image_View view() const; +private: + friend struct detail::Painter; + std::unique_ptr d; /* Blend2D 图像实现的唯一所有权。 */ +}; +namespace detail { +/* 只在 Renderable::Private 绘制实现中使用的 Blend2D 画笔。 */ +struct Painter { + struct Private; + struct Clip_Scope { + Clip_Scope(Painter& painter, Rect_F rect); + ~Clip_Scope(); + Clip_Scope(const Clip_Scope&) = delete; + Clip_Scope& operator=(const Clip_Scope&) = delete; + Clip_Scope(Clip_Scope&& other) noexcept; + Clip_Scope& operator=(Clip_Scope&& other) = delete; + private: + Painter* painter{}; /* 非空时析构恢复对应 Painter 的裁剪状态。 */ + }; + Painter(Blend2D_Cache& cache, Size size); + ~Painter(); + Painter(const Painter&) = delete; + Painter& operator=(const Painter&) = delete; + [[nodiscard]] explicit operator bool() const noexcept; + [[nodiscard]] Clip_Scope scoped_clip(Rect_F rect); + void clear(); + void composite(const Blend2D_Cache& source); + void line(Point_F first, Point_F second, const Pen& pen); + void polyline(std::span points, const Pen& pen); + void polygon(std::span points, const Pen& pen, const Brush& brush); + void rect(Rect_F rect, const Pen& pen, const Brush& brush = {}); + void circle(Point_F center, double radius, const Pen& pen, const Brush& brush = {}); + void text(Point_F position, + std::string_view value, + const Font& font, + const Pen& pen, + double rotation_degrees = 0.0); + void heatmap(Rect_F target, + int width, + int height, + std::span pixels, + Image_Interpolation_Mode interpolation); +private: + std::unique_ptr d; /* 当前 BLContext 及绘制辅助算法的唯一所有权。 */ +}; +} +} +#include "Blend2D_Cache.ipp" diff --git a/render_2D/render_2D/render/Blend2D_Cache.ipp b/render_2D/render_2D/render/Blend2D_Cache.ipp new file mode 100644 index 0000000..d4894f0 --- /dev/null +++ b/render_2D/render_2D/render/Blend2D_Cache.ipp @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include +namespace aethera::render_2d { +struct Blend2D_Cache::Private { + BLImage image{}; /* 缓存拥有的 PRGB32 Blend2D 图像。 */ + Private() = default; + Private(const Private& other); + Private& operator=(const Private& other); + /* 将 Blend2D 失败转换为 Unknown Failure。 */ + static void require_success(BLResult result, const char* operation); + /* 将图像全部清零为透明像素。 */ + static void clear_image(BLImage& image); +}; +namespace detail { +struct Painter::Private { + BLContext context{}; /* 当前缓存图像上的 Blend2D 绘制上下文。 */ + bool active{}; /* context 是否已经成功 begin 且尚未 end。 */ + /* 将项目颜色转换为 Blend2D 浮点颜色。 */ + [[nodiscard]] static BLRgba rgba(Color color) noexcept; + /* 按 Pen 配置当前描边样式。 */ + void apply_pen(const Pen& pen); + /* 从系统字体候选中创建对应字号和字形的 Blend2D 字体。 */ + [[nodiscard]] static BLFont make_font(const Font& font); + /* 加载第一个存在且可解析的字体文件。 */ + static void load_font_face(BLFontFace& face, std::initializer_list candidates); + /* 返回与字重和斜体匹配的进程级默认字体面。 */ + [[nodiscard]] static BLFontFace& default_font_face(const Font& font); + [[nodiscard]] static BLStrokeCap stroke_cap(Line_Cap cap) noexcept; + [[nodiscard]] static BLStrokeJoin stroke_join(Line_Join join) noexcept; + [[nodiscard]] static double cubic_weight(double distance) noexcept; + [[nodiscard]] static std::uint8_t pixel_channel(Pixel pixel, int shift) noexcept; + /* 对预乘像素执行 Catmull-Rom 双三次重采样。 */ + [[nodiscard]] static std::vector bicubic_resample(std::span source, + int source_width, + int source_height, + int destination_width, + int destination_height); +}; +} +} diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp new file mode 100644 index 0000000..e9e21ec --- /dev/null +++ b/render_2D/tests/Axis_Test.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +namespace { +using namespace aethera; +using namespace aethera::render_2d; +template +std::unique_ptr build_axis() { + auto result = typename Object::Builder{}.build(); + if (!result) std::terminate(); + return std::move(result).value(); +} +} +TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) { + using Object = Impl; + auto axis = build_axis(); + axis->update_state<&Abs_Axis::State::position>(Point_F{10.0, 0.0}); + axis->update_state<&Abs_Axis::State::pixel_length>(200.0); + axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 20.0}); + axis->advance(); + EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0})); + EXPECT_DOUBLE_EQ(axis->coordinate_to_pixel(5.0), 60.0); + EXPECT_DOUBLE_EQ(axis->pixel_to_coordinate(60.0), 5.0); + EXPECT_EQ(axis->pixel_sample_count({0.0, 20.0}), 201); + EXPECT_DOUBLE_EQ(axis->tick_step({0.0, 20.0}), 5.0); + EXPECT_EQ(axis->tick_label(2.5), "2.5"); + EXPECT_EQ(axis->sub_tick_count(5.0), 4); +} +TEST(axis_dispatch, point_conversion_uses_current_orientation) { + using Object = Impl; + auto axis = build_axis(); + axis->update_state<&Abs_Axis::State::position>(Point_F{0.0, 20.0}); + axis->update_state<&Abs_Axis::State::pixel_length>(100.0); + axis->update_state<&Abs_Axis::State::orientation>(Axis_Orientation::vertical); + axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 10.0}); + axis->advance(); + EXPECT_DOUBLE_EQ(axis->point_to_coordinate({90.0, 70.0}), 5.0); +} +TEST(render_cache, copy_owns_independent_pixels) { + Blend2D_Cache original; + { + aethera::render_2d::detail::Painter painter(original, {32, 16}); + painter.line({1.0, 1.0}, {30.0, 1.0}, Pen{Color::red_color(), 2.0}); + } + Blend2D_Cache copy = original; + original.clear(); + const Image_View view = copy.view(); + ASSERT_FALSE(view.empty()); + bool contains_color{}; + for (int y = 0; y < view.height && !contains_color; ++y) { + const auto* row = reinterpret_cast(view.data + static_cast(y) * view.stride); + for (int x = 0; x < view.width; ++x) contains_color = contains_color || row[x] != 0; + } + EXPECT_TRUE(contains_color); +} +TEST(axis_render, scene_prepares_and_paints_axis_cache) { + using Object = Impl; + using Scene_Object = Impl; + initialize_runtime(2); + auto axis = build_axis(); + auto scene = build_axis(); + axis->update_state<&Abs_Axis::State::position>(Point_F{8.0, 8.0}); + axis->update_state<&Abs_Axis::State::canvas_size>(Size{160, 64}); + axis->update_state<&Abs_Axis::State::pixel_length>(120.0); + ASSERT_TRUE((scene->edit_dependency_graph([&](auto& prepare, auto& paint) { + prepare.add(axis.get()); + paint.add(axis.get()); + }).has_value())); + scene->process([](const auto&) {}); + const Image_View view = axis->pending_buffer().view(); + ASSERT_FALSE(view.empty()); + bool contains_color{}; + for (int y = 0; y < view.height && !contains_color; ++y) { + const auto* row = reinterpret_cast(view.data + static_cast(y) * view.stride); + for (int x = 0; x < view.width; ++x) contains_color = contains_color || row[x] != 0; + } + EXPECT_TRUE(contains_color); +} +TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) { + using Object = Impl; + auto axis = build_axis(); + axis->update_state<&Abs_Axis::State::position>(Point_F{0.0, 0.0}); + axis->update_state<&Abs_Axis::State::pixel_length>(100.0); + axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 10.0}); + axis->advance(); + Wheel_Event wheel; + wheel.position = {50.0, 0.0}; + wheel.angle_delta_y = 120.0; + axis->dispatch_event(wheel); + EXPECT_TRUE(wheel.is_accepted()); + axis->advance(); + EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.5, 9.5})); + Pointer_Event press(Event_Type::pointer_press); + press.position = {50.0, 0.0}; + press.button = Mouse_Button::left; + axis->dispatch_event(press); + Pointer_Event move(Event_Type::pointer_move); + move.position = {60.0, 0.0}; + axis->dispatch_event(move); + EXPECT_TRUE(move.is_accepted()); + axis->advance(); + EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-0.4, 8.6})); +} +TEST(axis_state, invalid_numeric_range_is_rejected_without_poisoning_pending_state) { + using Object = Impl; + auto axis = build_axis(); + EXPECT_THROW((axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{1.0, 1.0})), std::invalid_argument); + axis->advance(); + EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0})); +} +TEST(axis_label, frequency_axis_selects_engineering_unit) { + using Object = Impl; + auto axis = build_axis(); + axis->advance(); + EXPECT_EQ(axis->tick_label(250.0), "250 Hz"); + EXPECT_EQ(axis->tick_label(2'500.0), "2.5 kHz"); + EXPECT_EQ(axis->tick_label(2'500'000.0), "2.5 MHz"); +} +TEST(axis_time, sample_state_drives_range_lookup_and_label_without_snapshot) { + using Object = Impl; + auto axis = build_axis(); + EXPECT_EQ(axis->append_time({3'723'004}), 0); + EXPECT_EQ(axis->time_point_count(), 0u); + axis->advance(); + EXPECT_EQ(axis->time_point_count(), 1u); + EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 1.0})); + EXPECT_EQ(axis->tick_to_time(0), (Time_Of_Day{3'723'004})); + EXPECT_EQ(axis->tick_label(0.0), "02:03.004"); + EXPECT_FALSE(axis->tick_to_time(99).valid()); +}