From 81204fc1888f68624a2985b11ed1bb72cce05d52 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 20 Aug 2026 23:04:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=A7=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E5=88=B0prop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project_detail_specification.md | 3 + .../kernel/double_buffer/Dependency_Graph.hpp | 18 +++ .../Dependency_Graph_Storage.hpp | 16 +++ kernel/src/kernel/double_buffer/mechanism.hpp | 133 ++++++++++++++++-- kernel/src/kernel/double_buffer/model.hpp | 98 +++++++++---- kernel/src/kernel/render_common.hpp | 1 + kernel/src/kernel/renderable.hpp | 19 ++- kernel/src/kernel/renderable.ipp | 18 +-- kernel/src/kernel/scene.hpp | 5 +- kernel/src/kernel/scene.ipp | 4 +- kernel/src/test/Dependency_Graph_Test.cpp | 39 +++-- kernel/src/test/object_test.cpp | 77 ++++++---- kernel/src/test/render_test.cpp | 25 ++-- render_2D/render_2D/axis/Abs_Axis.cpp | 1 + render_2D/render_2D/axis/Abs_Axis.hpp | 17 ++- render_2D/render_2D/axis/Abs_Axis.ipp | 14 +- render_2D/render_2D/axis/Frequency_Axis.hpp | 6 +- render_2D/render_2D/axis/Frequency_Axis.ipp | 2 +- render_2D/render_2D/axis/Numeric_Axis.cpp | 1 + render_2D/render_2D/axis/Numeric_Axis.hpp | 13 +- render_2D/render_2D/axis/Numeric_Axis.ipp | 34 ++--- render_2D/render_2D/axis/Time_Axis.cpp | 1 + render_2D/render_2D/axis/Time_Axis.hpp | 13 +- render_2D/render_2D/axis/Time_Axis.ipp | 17 ++- render_2D/render_2D/plottable/Afterglow.cpp | 3 +- render_2D/render_2D/plottable/Afterglow.hpp | 12 +- render_2D/render_2D/plottable/Afterglow.ipp | 28 ++-- .../plottable/Constellation_Diagram.cpp | 3 +- .../plottable/Constellation_Diagram.hpp | 10 +- .../plottable/Constellation_Diagram.ipp | 14 +- .../render_2D/plottable/Frequency_Trace.cpp | 1 + .../render_2D/plottable/Frequency_Trace.hpp | 11 +- .../render_2D/plottable/Frequency_Trace.ipp | 41 +++--- .../plottable/Selection_Rectangle_Overlay.cpp | 1 + .../plottable/Selection_Rectangle_Overlay.hpp | 10 +- .../plottable/Selection_Rectangle_Overlay.ipp | 29 ++-- render_2D/render_2D/plottable/Spectrum.cpp | 1 + render_2D/render_2D/plottable/Spectrum.hpp | 12 +- render_2D/render_2D/plottable/Spectrum.ipp | 102 ++++++++------ .../render_2D/plottable/Sweep_Spectrum.cpp | 1 + .../render_2D/plottable/Sweep_Spectrum.hpp | 12 +- .../render_2D/plottable/Sweep_Spectrum.ipp | 35 ++--- render_2D/render_2D/plottable/Waterfall.cpp | 3 +- render_2D/render_2D/plottable/Waterfall.hpp | 12 +- render_2D/render_2D/plottable/Waterfall.ipp | 28 ++-- render_2D/render_2D/scene/Render_Scene_2D.cpp | 1 + render_2D/render_2D/scene/Render_Scene_2D.hpp | 9 +- render_2D/render_2D/scene/Render_Scene_2D.ipp | 4 +- render_2D/tests/Axis_Test.cpp | 28 ++-- render_2D/tests/Plottable_Migration_Test.cpp | 56 ++++---- render_2D/tests/Spectrum_Test.cpp | 46 +++--- 51 files changed, 685 insertions(+), 403 deletions(-) diff --git a/Project_detail_specification.md b/Project_detail_specification.md index adbfc88..cb06e64 100644 --- a/Project_detail_specification.md +++ b/Project_detail_specification.md @@ -11,6 +11,9 @@ ## 状态与接口 * 每个状态只能有一个权威来源。双缓冲交换后的当前结构就是稳定读面,跨对象直接读取该结构;禁止为无锁访问再复制一份快照、View、镜像字段或同步缓存。 +* `Prop` 保存外部可读写的业务属性,`State` 保存实现向外发布的运行结果,`Private` 保存实现细节;三者不得互相复制并手工同步。 +* 每个 `Def` 定义层自动以自身类型生成 `Base_Tag`;Prop、State、Private 分别在隔离的标签空间中复用该标签,禁止再声明 `XXX_Prop_Tag`、`XXX_State_Tag` 或 `XXX_Private_Tag`。 +* 依赖可以选择 Prop/State 的单字段或整个 `Base_Tag` 层;字段写入必须同时发出字段级和所属层级变更,使用方按实际重建粒度选择一种依赖。 * `Root` 只保存一个最终 `Private` 指针;`Builder::build()` 校验成功后创建并挂接完整 Private,`Root` 通过公共 Private 基类的虚析构统一释放。禁止直接公开该指针。 * 能从权威结构查询或计算的数据即时获取,不保存为成员。类只保存自身职责需要且无法推导的状态,并检查每个新增成员的读写者和生命周期。 * 公共接口只表达业务语义,不暴露 `Private`、内部指针、线程状态或缓冲区角色;接口保持正交,不增加空配置、未完成接口、无消费者统计或只做转发的 getter/setter。 diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp index 1130935..8329242 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp @@ -157,6 +157,16 @@ public: void disconnect(Root* object) { edit_dependency_graph->disconnect_impl(object); } + /* 添加 Prop 字段级依赖;source 通过 set 修改该字段时目标阶段变脏。 */ + template Target, detail::Prop_Dependency_Source Source> + Node* add_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph->template add_dependency_runtime(target, source, detail::dependency_id>(), target_tag, target_dirty_key, bind_node_data); + } + /* 添加 Prop 声明层级依赖;Owner 声明的任一字段通过 set 修改时目标阶段变脏。 */ + template Target, detail::Prop_Layer_Dependency_Source Source> + Node* add_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph->template add_dependency_runtime(target, source, detail::dependency_id>(), target_tag, target_dirty_key, bind_node_data); + } template Target, detail::State_Dependency_Source Source> Node* add_dependency(Target* target, Source* source) { return edit_dependency_graph->template add_dependency_runtime( @@ -206,6 +216,14 @@ public: bool remove_dependency(Target* target, Source* source) { return edit_dependency_graph->remove_edge(target, source, detail::dependency_id>(), target_tag); } + template Target, detail::Prop_Dependency_Source Source> + bool remove_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph->remove_edge(target, source, detail::dependency_id>(), target_tag); + } + template Target, detail::Prop_Layer_Dependency_Source Source> + bool remove_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph->remove_edge(target, source, detail::dependency_id>(), target_tag); + } /* 移除指定 State Tag 的状态层级依赖,不影响同对象上的字段级依赖。 */ template Target, detail::State_Layer_Dependency_Source Source> bool remove_state_dependency(Target* target, Source* source) { diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp index 4a10d75..c06c4ee 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp @@ -286,6 +286,14 @@ struct Root::Builder { } ); } + template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Dependency_Source Source> + Builder& add_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph([&](auto& editor) { editor.template add_prop_dependency(target, source); }); + } + template Dependency_Graph_Tag, typename Prop_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Layer_Dependency_Source Source> + Builder& add_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph([&](auto& editor) { editor.template add_prop_dependency(target, source); }); + } template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::State_Dependency_Source Source> Builder& add_dependency(Target* target, Source* source) { return edit_dependency_graph( @@ -327,6 +335,14 @@ struct Root::Builder { } ); } + template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Dependency_Source Source> + Builder& remove_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph([&](auto& editor) { editor.template remove_prop_dependency(target, source); }); + } + template Dependency_Graph_Tag, typename Prop_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Layer_Dependency_Source Source> + Builder& remove_prop_dependency(Target* target, Source* source) { + return edit_dependency_graph([&](auto& editor) { editor.template remove_prop_dependency(target, source); }); + } /* 从构造期依赖图中移除指定 State Tag 的状态层级依赖。 */ template Dependency_Graph_Tag, typename State_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::State_Layer_Dependency_Source Source> Builder& remove_state_dependency(Target* target, Source* source) { diff --git a/kernel/src/kernel/double_buffer/mechanism.hpp b/kernel/src/kernel/double_buffer/mechanism.hpp index c7762d3..a8397ec 100644 --- a/kernel/src/kernel/double_buffer/mechanism.hpp +++ b/kernel/src/kernel/double_buffer/mechanism.hpp @@ -145,10 +145,19 @@ struct Publish_Double_Buffer : Double_Buffer { } }; namespace detail { +struct Prop_Root { + bool operator==(const Prop_Root&) const = default; +}; struct State_Root { bool operator==(const State_Root&) const = default; }; } +template +struct Prop_Type : Prev { + using Tag_Type = Tag; + using Prev_Prop = Prev; + bool operator==(const Prop_Type&) const = default; +}; // State_Type 用 Tag 标记每一层状态,Prev_State 把 CRTP 继承链上的状态按层串起来,供精确回调和类型约束使用。 template struct State_Type : Prev { @@ -207,19 +216,27 @@ struct Is_State_Type : std::false_type {}; template struct Is_State_Type> : std::true_type {}; template +struct Is_Prop_Type : std::false_type {}; +template +struct Is_Prop_Type> : std::true_type {}; +template concept Buffer_Type = Is_Tagged_Buffer::value; template concept Dependency_Graph_Mechanism = Is_Dependency_Graph_Type::value; template -concept State_Mechanism = Is_State_Type::value; -template -concept Mechanism_Type = Buffer_Type || Dependency_Graph_Mechanism || State_Mechanism; +concept Mechanism_Type = Buffer_Type || Dependency_Graph_Mechanism; template concept Tagged_State = Prop_State && requires { typename Value::Tag_Type; typename Value::Prev_State; requires std::derived_from>; }; +template +concept Tagged_Prop = Prop_State && requires { + typename Value::Tag_Type; + typename Value::Prev_Prop; + requires std::derived_from>; +}; template struct State_Layers { using Type = std::tuple<>; @@ -233,6 +250,16 @@ struct State_Layers using State_Layers_T = typename State_Layers::Type; +template +struct Prop_Layers { + using Type = std::tuple<>; +}; +template +struct Prop_Layers> { + using Type = decltype(std::tuple_cat(std::declval>(), std::declval::Type>())); +}; +template +using Prop_Layers_T = typename Prop_Layers::Type; template struct Member_Pointer_Traits; template @@ -250,6 +277,14 @@ concept State_Member = requires { }; template using State_Member_Tag = typename Member_Pointer_Traits::Owner_Type::Tag_Type; +template +concept Prop_Member = requires { + requires std::is_member_object_pointer_v; + requires std::derived_from::Owner_Type>; + requires Tagged_Prop::Owner_Type>; +}; +template +using Prop_Member_Tag = typename Member_Pointer_Traits::Owner_Type::Tag_Type; template concept State_Member_Settable = requires(State& state, Value&& value) { requires State_Member; @@ -259,8 +294,6 @@ template using Mechanism_Buffer_Tuple = std::conditional_t, std::tuple, std::tuple<>>; template using Mechanism_Dependency_Graph_Tuple = std::conditional_t, std::tuple, std::tuple<>>; -template -using Mechanism_State_Tuple = std::conditional_t, std::tuple, std::tuple<>>; template struct Has_Tag : std::false_type {}; template @@ -334,6 +367,19 @@ struct Is_State_List> : Tagged_List_Check< > {}; template concept State_List = Is_State_List::value; +template +struct Is_Prop_List : std::false_type {}; +template +struct Is_Prop_List> : Tagged_List_Check<(Tagged_Prop && ...), Props...> {}; +template +concept Prop_List = Is_Prop_List::value; +template +concept Prop_Chain = Tagged_Prop && Prop_List>; +template +concept Prop_Tag_In = requires { + requires Prop_List; + requires Has_Tag::value; +}; template concept State_Chain = Tagged_State && State_List>; template @@ -384,6 +430,15 @@ public: }; template using State_Value = typename State_By_Tag::Type; +template +struct Prop_By_Tag { +private: + using Layers = Prop_Layers_T; +public: + using Type = std::tuple_element_t, Layers>; +}; +template +using Prop_Value = typename Prop_By_Tag::Type; template concept State_Callback_For = State_Tag_In && State_Chain_Matches && std::invocable&>; template @@ -522,8 +577,53 @@ public: }; template requires detail::State_Chain> State_Access(State_Type&) -> State_Access; +/* Prop_Access 按定义层的 Base_Tag 选择 Prop;Prop 与 State 的标签空间彼此隔离。 */ +template requires detail::Prop_Chain> +class Prop_Access { +private: + using Value_Type = std::remove_const_t; + Prop_Type* prop; + template requires detail::Prop_Chain> + friend class Prop_Access; +public: + explicit Prop_Access(Prop_Type& value) noexcept : prop(&value) {} + template requires std::convertible_to + Prop_Access(const Prop_Access& source) noexcept : prop(source.prop) {} + template requires detail::Prop_Tag_In> + [[nodiscard]] decltype(auto) get() const noexcept { + using Layer = detail::Prop_Value; + if constexpr (std::is_const_v) return static_cast(*prop); + else return static_cast(*prop); + } +}; +template requires detail::Prop_Chain> +Prop_Access(Prop_Type&) -> Prop_Access; +/* Private_Access 使用同一个定义层 Base_Tag 选择 Private;该标签空间不与 Prop/State 混用。 */ +template +class Private_Access { +private: + Private_Type* private_data; + template + friend class Private_Access; +public: + explicit Private_Access(Private_Type& value) noexcept : private_data(&value) {} + template requires std::convertible_to + Private_Access(const Private_Access& source) noexcept : private_data(source.private_data) {} + template requires requires { typename Tag::Private; } && std::derived_from, typename Tag::Private> + [[nodiscard]] decltype(auto) get() const noexcept { + using Layer = typename Tag::Private; + if constexpr (std::is_const_v) return static_cast(*private_data); + else return static_cast(*private_data); + } +}; +template +Private_Access(Private_Type&) -> Private_Access; namespace detail { template +struct Prop_Dependency_Key {}; +template +struct Prop_Layer_Dependency_Key {}; +template struct State_Dependency_Key {}; template struct State_Layer_Dependency_Key {}; @@ -563,10 +663,10 @@ enum class Dependency_Graph_Error { missing_dependency, cycle }; -struct Root_State_Tag {}; struct Root { /* 所有实现层 Private 的公共析构基类;Root 通过该类型唯一拥有最终 Private。 */ struct Private { + using Tag_Type = Root; virtual ~Private() = default; /* CRTP 默认:Builder 挂接最终 Private 后按基类到派生类绑定最终对象类型;Root 层不处理。 */ template @@ -574,9 +674,12 @@ struct Root { }; using Buffers = std::tuple<>; using Dependency_Graph_Types = std::tuple<>; - using States = std::tuple>; - struct Prop {}; - struct State : State_Type { + using Base_Tag = Root; + using States = std::tuple>; + struct Prop : Prop_Type { + bool operator==(const Prop&) const = default; + }; + struct State : State_Type { bool operator==(const State&) const = default; }; private: @@ -666,8 +769,10 @@ namespace detail { template concept Dependency_Object = Attached && requires { requires Root_Derived; + typename T::Prop; typename T::State; typename T::Buffers; + requires Prop_State; requires Prop_State; requires Buffer_List; }; @@ -680,6 +785,16 @@ concept Bound_Dependency_Graph_Target = Bound_Dependency_Object && std::deriv template concept Dependency_Graph_Target = Root_Derived && std::derived_from; template +concept Prop_Dependency_Source = requires { + requires Dependency_Object; + requires Prop_Member; +}; +template +concept Prop_Layer_Dependency_Source = requires { + requires Dependency_Object; + requires Prop_Tag_In>; +}; +template concept State_Dependency_Source = requires { requires Dependency_Object; requires State_Member; diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 16ab355..73275fe 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -6,18 +6,20 @@ concept Object = requires { requires Object_Root; typename T::This_Object; typename T::Prev_Object; + typename T::Base_Tag; typename T::Prev_Prop; - typename T::State_Tag; - typename T::template Prev_State; + typename T::Prev_State; typename T::template Prev_Builder; typename T::Prev_Private; typename T::States; requires std::same_as; + requires std::same_as; requires Object_Root; requires std::derived_from; - requires detail::State_Tag_In; - requires detail::State_Chain>; - requires std::derived_from>; + requires detail::Tagged_Prop; + requires detail::State_Tag_In; + requires detail::State_Chain; + requires std::derived_from; requires detail::State_Chain_Matches; requires std::derived_from, typename T::template Prev_Builder>; requires std::derived_from; @@ -33,26 +35,16 @@ using Impl_Dependency_Graph_Types = decltype(std::tuple_cat( std::declval(), std::declval>()... )); -template -using Local_States = decltype(std::tuple_cat( - std::declval>()... -)); -template +template using Impl_States = decltype(std::tuple_cat( std::declval(), - std::declval>() + std::declval>>() )); -template -using Impl_State_Base = Rebind_State_T< - std::tuple_element_t<0, Local_States>, - typename Base::State ->; -template +template concept Impl_Mechanisms = Object_Root && (Mechanism_Type && ...) && requires { - requires std::tuple_size_v> == 1; requires Buffer_List>; requires Dependency_Graph_List>; - requires State_List>; + requires State_List>; }; template concept Process_Callback_For = requires(Private& private_data, Object_T* object, Callback&& callback) { @@ -61,27 +53,33 @@ concept Process_Callback_For = requires(Private& private_data, Object_T* object, } // Impl 在编译期把 Buffer/Dependency_Graph/State 三类机制叠加到继承链;每一层只声明自己的机制,最终类型汇总完整能力。 template requires - detail::Impl_Mechanisms + detail::Impl_Mechanisms struct Def : Base { using This_Object = Self; using Prev_Object = Base; - using Prev_Prop = typename Base::Prop; + using Base_Tag = Self; + using Prev_Prop = Prop_Type; template using Prev_Builder = typename Base::template Builder; - using State_Declaration = std::tuple_element_t<0, detail::Local_States>; - using State_Tag = typename State_Declaration::Tag_Type; - template Tag> - using Prev_State = detail::Rebind_State_T; + using Prev_State = State_Type; using Base_Private = typename Base::Private; using Buffers = detail::Impl_Buffers; using Dependency_Graph_Types = detail::Impl_Dependency_Graph_Types; - using States = detail::Impl_States; + using States = detail::Impl_States; struct Private : Base_Private { + using Tag_Type = Base_Tag; + using Prev_Private = Base_Private; /* CRTP 默认:继续调用上一 Private 层的最终对象绑定;派生 Private 覆盖时必须先调用此实现。 */ template void bind_private_crtp(Object* object) { Base_Private::bind_private_crtp(object); } + /* CRTP 可覆盖:写入 Prop 成员前按基类到派生类顺序调用。 */ + template + void before_prop_set(Self* object, Member Owner::* member, Prop_Access props) {} + /* CRTP 可覆盖:写入 Prop 成员后按派生类到基类顺序调用。 */ + template + void after_prop_set(Self* object, Member Owner::* member, Prop_Access props) {} /* CRTP 可覆盖:写入 State 成员前按基类到派生类顺序调用;pending_states.get() 返回对应可写状态层。 */ template void before_state_set(Self* object, Member Owner::* member, State_Access pending_states) {} @@ -157,6 +155,24 @@ private: } } template + void before_prop_set(Member Owner::* member) { + Prop_Access props{*data().current}; + auto callback = [&](auto& private_data) { private_data.before_prop_set(this, member, props); }; + walk_private(callback); + } + template + void after_prop_set(Member Owner::* member) { + Prop_Access props{*data().current}; + auto callback = [&](auto& private_data) { private_data.after_prop_set(this, member, props); }; + walk_private(callback); + } + template + void emit_prop_dependencies() { + this->emit_dependency_source(detail::dependency_id>()); + using Prop_Tag = detail::Prop_Member_Tag; + this->emit_dependency_source(detail::dependency_id>()); + } + template void before_state_set(Member Owner::* member) { State_Access pending_states{*data().state.pending}; auto callback = [&](auto& private_data) { @@ -267,12 +283,36 @@ public: void for_each_current_dependency_graph(Callback&& callback) const { data().dependency_graph_storage.for_each_current(std::forward(callback)); } - template Value> - Impl& set(Member Owner::* member, Value&& value) { + template requires detail::Prop_Member && requires(Prop& prop, Value&& value) { prop.*Member = std::forward(value); } + Impl& set(Value&& value) { std::lock_guard guard(lock); - data().current->*member = std::forward(value); + before_prop_set(Member); + data().current->*Member = std::forward(value); + after_prop_set(Member); + emit_prop_dependencies(); return *this; } + template requires detail::Prop_Dependency_Source + [[nodiscard]] auto get() const { + std::lock_guard guard(lock); + return data().current->*Member; + } + template requires detail::Prop_Tag_In> + [[nodiscard]] const auto& read_prop() const noexcept { + using Layer = detail::Prop_Value; + return static_cast(*data().current); + } + template requires + (sizeof...(Members) > 0) && + (detail::Prop_Member && ...) && + std::invocable> + void update_prop(Callback&& callback) { + std::lock_guard guard(lock); + (before_prop_set(Members), ...); + std::invoke(std::forward(callback), Prop_Access{*data().current}); + (after_prop_set(Members), ...); + (emit_prop_dependencies(), ...); + } template Tag, detail::State_Callback_For Callback> void set_state_callback(Callback&& callback) { std::lock_guard guard(lock); diff --git a/kernel/src/kernel/render_common.hpp b/kernel/src/kernel/render_common.hpp index 0a039d4..3805790 100644 --- a/kernel/src/kernel/render_common.hpp +++ b/kernel/src/kernel/render_common.hpp @@ -16,6 +16,7 @@ using double_buffer::Def; using double_buffer::Impl; using double_buffer::State_Type; using double_buffer::State_Access; +using double_buffer::Prop_Access; using double_buffer::Pmr; using double_buffer::Root; using double_buffer::Tagged_Buffer; diff --git a/kernel/src/kernel/renderable.hpp b/kernel/src/kernel/renderable.hpp index 80dc5fc..7c3bcc3 100644 --- a/kernel/src/kernel/renderable.hpp +++ b/kernel/src/kernel/renderable.hpp @@ -7,7 +7,6 @@ struct Color_Cache { virtual ~Color_Cache() = default; }; /* Renderable 状态标签,用于访问和订阅 Renderable::State。 */ -struct Renderable_State_Tag {}; struct Renderable; /* 最终 Private 提供 handle_event(T*, const Event&) 时具备事件处理能力。 */ template @@ -28,8 +27,8 @@ concept Prepare_Data_Renderable = Attached && requires(typename T::Private& p * 子图首次执行前一定构建,之后由 should_rebuild_prepare_graph(...) 决定是否重建。 */ template -concept Prepare_Graph_Renderable = Attached && requires(typename T::Private& private_data, T* object, const typename T::State& state) { - { private_data.build_prepare_graph(object, state) } -> std::same_as; +concept Prepare_Graph_Renderable = Attached && requires(typename T::Private& private_data, T* object, const typename T::Prop& prop) { + { private_data.build_prepare_graph(object, prop) } -> std::same_as; }; /* 最终 Private 声明 No_Prepare 时,该 Renderable 不参与 Prepare 阶段。 */ template @@ -48,29 +47,29 @@ concept Paint_Data_Renderable = Attached && requires(typename T::Private& pri * 子图首次执行前一定构建,之后由 should_rebuild_paint_graph(...) 决定是否重建。 */ template -concept Paint_Graph_Renderable = Attached && requires(typename T::Private& private_data, T* object, const typename T::State& state) { - { private_data.build_paint_graph(object, state) } -> std::same_as; +concept Paint_Graph_Renderable = Attached && requires(typename T::Private& private_data, T* object, const typename T::Prop& prop) { + { private_data.build_paint_graph(object, prop) } -> std::same_as; }; /* * 最终可交给 Scene 执行的 Renderable 契约。 * Prepare 与 Paint 各自至少提供数据模式或子图模式之一,并继承 Renderable 提供的默认阶段策略。 */ template -concept Renderable_Object = Attached && std::derived_from && requires(typename T::Private& private_data, T* object, const typename T::State& state, bool dirty) { +concept Renderable_Object = Attached && std::derived_from && requires(typename T::Private& private_data, T* object, const typename T::State& state, const typename T::Prop& prop, bool dirty) { { private_data.should_prepare(object, state, dirty) } -> std::same_as; { private_data.should_paint(object, state, dirty) } -> std::same_as; - { private_data.should_rebuild_prepare_graph(object, state) } -> std::same_as; - { private_data.should_rebuild_paint_graph(object, state) } -> std::same_as; + { private_data.should_rebuild_prepare_graph(object, prop) } -> std::same_as; + { private_data.should_rebuild_paint_graph(object, prop) } -> std::same_as; } && (No_Prepare_Renderable || Prepare_Data_Renderable || Prepare_Graph_Renderable) && (Paint_Data_Renderable || Paint_Graph_Renderable); /* * Renderable 定义 Scene 可调度对象的公共机制。 * 用户继续派生该定义,在派生类型的 Private 中提供 Prepare/Paint 定制点,并最终使用 Impl 创建可运行实例。 */ -struct Renderable : Def> { +struct Renderable : Def { /* Renderable 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */ struct Prop : Prev_Prop {}; /* Renderable 每次 Scene 执行后发布的阶段状态与统计。 */ - struct State : Prev_State { + struct State : Prev_State { bool prepare_dirty{}; /* Prepare 条件判断时观察到的 Prepare_Data_Tag dirty 状态。 */ bool paint_dirty{}; /* Paint 条件判断时观察到的 Paint_Tag dirty 状态。 */ bool prepare_executed{}; /* 本次 Scene 执行是否运行了 Prepare 数据函数或子图。 */ diff --git a/kernel/src/kernel/renderable.ipp b/kernel/src/kernel/renderable.ipp index bc5a949..2580446 100644 --- a/kernel/src/kernel/renderable.ipp +++ b/kernel/src/kernel/renderable.ipp @@ -40,9 +40,9 @@ struct Renderable::Private : Prev_Private { bool prepare_graph_built{}; /* Prepare 子图是否至少成功构建过一次。 */ bool paint_graph_built{}; /* Paint 子图是否至少成功构建过一次。 */ /* CRTP 可覆盖:决定已选中子图模式的 Prepare 子图是否重建;object 为最终对象,state 为当前发布状态;默认返回 false。 */ - bool should_rebuild_prepare_graph(Attached auto* object, const State& state); + bool should_rebuild_prepare_graph(Attached auto* object, const Prop& prop); /* CRTP 可覆盖:决定已选中子图模式的 Paint 子图是否重建;object 为最终对象,state 为当前发布状态;默认返回 false。 */ - bool should_rebuild_paint_graph(Attached auto* object, const State& state); + bool should_rebuild_paint_graph(Attached auto* object, const Prop& prop); /* CRTP 可覆盖:决定本次是否执行 Prepare;object 为最终对象,state 为当前发布状态,dirty 为 Prepare dirty;默认返回 dirty。 */ bool should_prepare(Attached auto* object, const State& state, bool dirty); /* CRTP 可覆盖:决定本次是否执行 Paint;object 为最终对象,state 为当前发布状态,dirty 为 Paint dirty;默认返回 dirty。 */ @@ -63,10 +63,10 @@ void Renderable::run_prepare_data(Object* object) { }; walk_private(object, callback); } -inline bool Renderable::Private::should_rebuild_prepare_graph(Attached auto*, const State&) { +inline bool Renderable::Private::should_rebuild_prepare_graph(Attached auto*, const Prop&) { return false; } -inline bool Renderable::Private::should_rebuild_paint_graph(Attached auto*, const State&) { +inline bool Renderable::Private::should_rebuild_paint_graph(Attached auto*, const Prop&) { return false; } inline bool Renderable::Private::should_prepare(Attached auto*, const State&, bool dirty) { @@ -117,7 +117,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { [](Root* root) { auto* value = static_cast(root); auto& private_data = static_cast(*value->d); - return private_data.should_rebuild_prepare_graph(value, *private_data.state.current); + return private_data.should_rebuild_prepare_graph(value, *private_data.current); }, []() -> Private::Stage_Run { if constexpr (Prepare_Data_Renderable && !No_Prepare_Renderable) { @@ -135,7 +135,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { return [](Root* root) { auto* value = static_cast(root); auto& private_data = static_cast(*value->d); - return private_data.build_prepare_graph(value, *private_data.state.current); + return private_data.build_prepare_graph(value, *private_data.current); }; } else { @@ -152,7 +152,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { [](Root* root) { auto* value = static_cast(root); auto& private_data = static_cast(*value->d); - return private_data.should_rebuild_paint_graph(value, *private_data.state.current); + return private_data.should_rebuild_paint_graph(value, *private_data.current); }, []() -> Private::Stage_Run { if constexpr (Paint_Data_Renderable) { @@ -171,7 +171,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { return [](Root* root) { auto* value = static_cast(root); auto& private_data = static_cast(*value->d); - return private_data.build_paint_graph(value, *private_data.state.current); + return private_data.build_paint_graph(value, *private_data.current); }; } else { @@ -187,7 +187,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { }, [](Root* root) { auto* value = static_cast(root); - value->template notify_state(); + value->template notify_state(); } } }; diff --git a/kernel/src/kernel/scene.hpp b/kernel/src/kernel/scene.hpp index 5425b54..ca31d40 100644 --- a/kernel/src/kernel/scene.hpp +++ b/kernel/src/kernel/scene.hpp @@ -2,16 +2,15 @@ #include "renderable.hpp" namespace aethera { /* Scene 状态标签,用于访问和订阅 Scene::State。 */ -struct Scene_State_Tag {}; /* * Scene 汇总 Prepare/Paint 两张 Dependency_Graph 图并构建总 Taskflow。 * 用户最终通过 Impl 创建可使用实例;编辑 Dependency_Graph 后调用 advance() 提交结构变化,再调用 process(...) 执行当前场景。 */ -struct Scene : Def, Dependency_Graph_Type, Dependency_Graph_Type> { +struct Scene : Def, Dependency_Graph_Type> { /* Scene 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */ struct Prop : Prev_Prop {}; /* Scene 每次 process(...) 后发布的总图结构与执行统计。 */ - struct State : Prev_State { + struct State : Prev_State { bool taskflow_rebuilt{}; /* 本次 process(...) 前的 advance 是否重新构建了总 Taskflow。 */ std::size_t renderable_count{}; /* Prepare/Paint 两张依赖图中去重后的 Renderable 数量。 */ std::size_t taskflow_task_count{}; /* 当前总 Taskflow 中的任务节点数量。 */ diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index f08355b..82da1f9 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -31,7 +31,7 @@ void Scene::Private::process(Object* object, Callback&& callback) requires std:: 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(); + object->template notify_state(); Result result; std::invoke(std::forward(callback), std::as_const(result)); } @@ -42,7 +42,7 @@ void Scene::Private::after_advance(Object* object, const Prop* current_prop, State_Access current_states) { auto* resource = detail::task_memory_resource(); - auto& scene_state = current_states.get(); + auto& scene_state = current_states.get(); scene_state.taskflow_rebuilt = false; std::pmr::unordered_set advanced_objects{resource}; advanced_objects.insert(object); diff --git a/kernel/src/test/Dependency_Graph_Test.cpp b/kernel/src/test/Dependency_Graph_Test.cpp index e358af1..2dce4ce 100644 --- a/kernel/src/test/Dependency_Graph_Test.cpp +++ b/kernel/src/test/Dependency_Graph_Test.cpp @@ -1,12 +1,10 @@ #include "double_buffer/model.hpp" #include namespace { -struct Node_State_Tag {}; -struct Graph_State_Tag {}; struct Graph_Tag {}; -struct Node_Object : double_buffer::Def> { - struct Prop : Prev_Prop {}; - struct State : Prev_State { +struct Node_Object : double_buffer::Def { + struct Prop : Prev_Prop { int value{}; int other{}; }; + struct State : Prev_State { int value{}; /* 字段级依赖测试使用的状态值。 */ int other{}; /* 状态层级依赖测试使用的同层其他值。 */ bool operator==(const State&) const = default; @@ -14,9 +12,9 @@ struct Node_Object : double_buffer::Def; -struct Graph_Object : double_buffer::Def, double_buffer::Dependency_Graph_Type> { +struct Graph_Object : double_buffer::Def> { struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct State : Prev_State { bool operator==(const State&) const = default; }; struct Private : Prev_Private {}; @@ -35,7 +33,8 @@ std::unique_ptr build_object() { static_assert(!double_buffer::detail::Dependency_Object); static_assert(double_buffer::detail::Dependency_Object); static_assert(!double_buffer::detail::State_Dependency_Source); -static_assert(double_buffer::detail::State_Layer_Dependency_Source); +static_assert(double_buffer::detail::State_Layer_Dependency_Source); +static_assert(double_buffer::detail::Prop_Layer_Dependency_Source); } TEST(dependency_graph_storage, edited_graph_commits_and_stays_synchronized) { auto first = build_object(); @@ -111,7 +110,7 @@ TEST(dependency_graph, state_dependency_granularity_is_selectable) { editor.add(layer_source.get()); editor.add(layer_target.get()); editor.template add_dependency<&Node_Object::State::value>(member_target.get(), member_source.get()); - editor.template add_state_dependency(layer_target.get(), layer_source.get()); + editor.template add_state_dependency(layer_target.get(), layer_source.get()); } ).has_value()); graph->advance(); @@ -122,6 +121,28 @@ TEST(dependency_graph, state_dependency_granularity_is_selectable) { layer_source->update_state<&Node_Object::State::other>(3); EXPECT_TRUE(layer_target->dirty()); } +TEST(dependency_graph, prop_dependency_granularity_is_selectable) { + auto member_source = build_object(); + auto member_target = build_object(); + auto layer_source = build_object(); + auto layer_target = build_object(); + auto graph = build_object(); + ASSERT_TRUE(graph->edit_dependency_graph([&](auto& editor) { + editor.add(member_source.get()); + editor.add(member_target.get()); + editor.add(layer_source.get()); + editor.add(layer_target.get()); + editor.template add_prop_dependency<&Node_Object::Prop::value>(member_target.get(), member_source.get()); + editor.template add_prop_dependency(layer_target.get(), layer_source.get()); + }).has_value()); + graph->advance(); + member_source->set<&Node_Object::Prop::other>(1); + EXPECT_FALSE(member_target->dirty()); + member_source->set<&Node_Object::Prop::value>(2); + EXPECT_TRUE(member_target->dirty()); + layer_source->set<&Node_Object::Prop::other>(3); + EXPECT_TRUE(layer_target->dirty()); +} TEST(dependency_graph, cycle_is_rejected_before_pending_graph_commit) { auto first = build_object(); auto second = build_object(); diff --git a/kernel/src/test/object_test.cpp b/kernel/src/test/object_test.cpp index 2c79d24..0f37b29 100644 --- a/kernel/src/test/object_test.cpp +++ b/kernel/src/test/object_test.cpp @@ -1,14 +1,13 @@ #include "double_buffer/model.hpp" #include namespace { -struct Object_State_Tag {}; struct Object_Buffer_Tag {}; -struct Test_Object : double_buffer::Def, double_buffer::Tagged_Buffer> { +struct Test_Object : double_buffer::Def> { struct Prop : Prev_Prop { int first{}; int second{}; }; - struct State : Prev_State { + struct State : Prev_State { int first{}; int second{}; bool operator==(const State&) const = default; @@ -20,20 +19,18 @@ using Object = double_buffer::Impl; inline auto& Test_Object::data_for_test() { return static_cast(*d); } -struct Derived_State_Tag {}; -struct Derived_Object : double_buffer::Def> { +struct Derived_Object : double_buffer::Def { struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct State : Prev_State { int derived{}; bool operator==(const State&) const = default; }; struct Private : Prev_Private {}; }; using Derived = double_buffer::Impl; -struct Lifetime_State_Tag {}; -struct Lifetime_Object : double_buffer::Def> { +struct Lifetime_Object : double_buffer::Def { struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct State : Prev_State { bool operator==(const State&) const = default; }; struct Private : Prev_Private { @@ -84,13 +81,13 @@ TEST(object_buffer, state_commits_and_keeps_incremental_baseline) { } TEST(object_buffer, prop_publishes_and_keeps_incremental_baseline) { auto object = build_object(); - object->set(&Test_Object::Prop::first, 17); + 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->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); @@ -98,7 +95,7 @@ TEST(object_buffer, prop_publishes_and_keeps_incremental_baseline) { TEST(state_tag, callback_publishes_only_requested_layer) { auto object = build_object(); int calls = 0; - object->set_state_callback( + object->set_state_callback( [&](const auto& state) { ++calls; EXPECT_EQ(state.first, 23); @@ -107,14 +104,14 @@ TEST(state_tag, callback_publishes_only_requested_layer) { 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) { - object.template access_state([](const auto&) {}); + object.template access_state([](const auto&) {}); }); static_assert(std::same_as< - decltype(std::declval().template read_state()), + decltype(std::declval().template read_state()), const Test_Object::State& >); struct Missing_State_Tag {}; @@ -123,12 +120,12 @@ TEST(state_tag, inherited_tags_remain_independently_addressable) { 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); } @@ -137,31 +134,31 @@ TEST(state_tag, committed_layer_is_directly_readable_by_tag) { object->update_state<&Test_Object::State::first>(41); object->update_state<&Derived_Object::State::derived>(43); object->advance(); - EXPECT_EQ(object->read_state().first, 41); - EXPECT_EQ(object->read_state().derived, 43); + EXPECT_EQ(object->read_state().first, 41); + EXPECT_EQ(object->read_state().derived, 43); } -static_assert(double_buffer::detail::State_Tag_In); -static_assert(double_buffer::detail::State_Tag_In); -static_assert(double_buffer::detail::State_Tag_In); +static_assert(double_buffer::detail::State_Tag_In); +static_assert(double_buffer::detail::State_Tag_In); +static_assert(double_buffer::detail::State_Tag_In); static_assert(std::same_as< - decltype(std::declval>().template get()), + decltype(std::declval>().template get()), Test_Object::State& >); static_assert(std::same_as< - decltype(std::declval>().template get()), + decltype(std::declval>().template get()), const Derived_Object::State& >); TEST(state_tag, state_access_selects_mutable_and_const_layers_by_tag) { Derived::State state; double_buffer::State_Access states{state}; - states.get().first = 31; - states.get().derived = 47; + states.get().first = 31; + states.get().derived = 47; double_buffer::State_Access base_states = states; - EXPECT_EQ(base_states.get().first, 31); + EXPECT_EQ(base_states.get().first, 31); const auto& const_state = state; double_buffer::State_Access current_states{const_state}; - EXPECT_EQ(current_states.get().first, 31); - EXPECT_EQ(current_states.get().derived, 47); + EXPECT_EQ(current_states.get().first, 31); + EXPECT_EQ(current_states.get().derived, 47); } TEST(state_tag, state_chain_keeps_default_equality_usable) { Test_Object::State first; @@ -170,3 +167,21 @@ TEST(state_tag, state_chain_keeps_default_equality_usable) { second.first = 1; EXPECT_FALSE(first == second); } +static_assert(std::same_as< + decltype(std::declval>().template get()), + Test_Object::Prop& +>); +static_assert(std::same_as< + decltype(std::declval>().template get()), + Test_Object::Private& +>); +TEST(base_tag, prop_state_and_private_domains_are_independently_addressable) { + Derived::Prop prop; + double_buffer::Prop_Access props{prop}; + props.get().first = 53; + EXPECT_EQ(props.get().first, 53); + Derived_Object::Private private_data; + double_buffer::Private_Access private_layers{private_data}; + EXPECT_EQ(&private_layers.get(), static_cast(&private_data)); + EXPECT_EQ(&private_layers.get(), static_cast(&private_data)); +} diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index 9f47454..d5fe943 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -1,11 +1,9 @@ #include "scene.hpp" #include namespace { -struct Direct_State_Tag {}; -struct Graph_State_Tag {}; -struct Direct_Renderable : double_buffer::Def> { +struct Direct_Renderable : double_buffer::Def { struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct State : Prev_State { bool operator==(const State&) const = default; }; struct Private : Prev_Private { @@ -20,9 +18,9 @@ struct Direct_Renderable : double_buffer::Def> { +struct Graph_Renderable : double_buffer::Def { struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct State : Prev_State { bool operator==(const State&) const = default; }; struct Private : Prev_Private { @@ -30,13 +28,13 @@ struct Graph_Renderable : double_buffer::Def(*d); } -struct Dependency_State_Tag {}; -struct Dependency_Renderable : double_buffer::Def> { +struct Dependency_Renderable : double_buffer::Def { struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct State : Prev_State { int revision{}; bool operator==(const State&) const = default; }; @@ -141,12 +138,12 @@ TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundarie 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); }); @@ -169,7 +166,7 @@ TEST(scene_state, structural_statistics_survive_a_process_without_rebuild) { 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) { diff --git a/render_2D/render_2D/axis/Abs_Axis.cpp b/render_2D/render_2D/axis/Abs_Axis.cpp index 1aa4202..2246b99 100644 --- a/render_2D/render_2D/axis/Abs_Axis.cpp +++ b/render_2D/render_2D/axis/Abs_Axis.cpp @@ -5,6 +5,7 @@ #include namespace aethera::render_2d { bool Abs_Axis::State::operator==(const State&) const = default; +bool Abs_Axis::Prop::operator==(const Prop&) const = default; Axis_Range Abs_Axis::Private::coordinate_range(const Root* object) const { return dispatch->coordinate_range(object); diff --git a/render_2D/render_2D/axis/Abs_Axis.hpp b/render_2D/render_2D/axis/Abs_Axis.hpp index e11e5b7..9a5880b 100644 --- a/render_2D/render_2D/axis/Abs_Axis.hpp +++ b/render_2D/render_2D/axis/Abs_Axis.hpp @@ -4,10 +4,8 @@ #include #include namespace aethera::render_2d { -/* Abs_Axis 状态标签,用于访问和订阅坐标轴布局状态。 */ -struct Abs_Axis_State_Tag {}; struct Abs_Axis; -/* 最终轴 Private 的完整计算能力契约;所有结果直接来自当前 State。 */ +/* 最终轴 Private 的完整计算能力契约;所有结果直接来自当前 Prop。 */ template concept Axis_Object = Renderable_Object && std::derived_from && requires( const typename T::Private& private_data, @@ -21,12 +19,9 @@ concept Axis_Object = Renderable_Object && std::derived_from && { private_data.sub_tick_count(object, tick) } -> std::same_as; }; /* 所有二维坐标轴共享的定义层;最终通过 Impl 创建运行时对象。 */ -struct Abs_Axis : Def, +struct Abs_Axis : Def> { - /* Abs_Axis 不发布额外属性。 */ - struct Prop : Prev_Prop {}; - /* 双缓冲交换后供轴计算直接读取的布局权威状态。 */ - struct State : Prev_State { + struct Prop : Prev_Prop { Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */ Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */ Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */ @@ -39,11 +34,15 @@ struct Abs_Axis : Def, Pen unit_text_pen{Color::white()}; /* 刻度标签和单位文本使用的前景样式。 */ Brush unit_text_background_brush{}; /* 单位文本背景填充;none 表示不填充。 */ Axis_Label_Rotation label_rotation_degrees{}; /* 刻度标签顺时针旋转角度,单位为度。 */ + bool operator==(const Prop&) const; + }; + /* 坐标轴当前没有额外发布状态。 */ + struct State : Prev_State { bool operator==(const State&) const; }; /* 完整声明及派生轴 CRTP 能力契约见 Abs_Axis.ipp。 */ struct Private; - /* 返回由最终轴 State 计算得到的当前有向坐标区间。 */ + /* 返回由最终轴 Prop 计算得到的当前有向坐标区间。 */ [[nodiscard]] Axis_Range coordinate_range() const; /* 将坐标值映射到当前轴向像素位置。 */ [[nodiscard]] Axis_Pixel_Position coordinate_to_pixel(Axis_Coordinate coordinate) const; diff --git a/render_2D/render_2D/axis/Abs_Axis.ipp b/render_2D/render_2D/axis/Abs_Axis.ipp index 666bef9..edaeac5 100644 --- a/render_2D/render_2D/axis/Abs_Axis.ipp +++ b/render_2D/render_2D/axis/Abs_Axis.ipp @@ -10,7 +10,7 @@ struct Abs_Axis::Private : Prev_Private { * 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(...),不保存变换快照。 + * 坐标映射直接读取 Abs_Axis::Base_Tag 状态并调用最终 Private 的 coordinate_range(...),不保存变换快照。 */ using Coordinate_Range_Call = Axis_Range (*)(const Root*); using Scalar_Call = double (*)(const Root*, double); @@ -81,7 +81,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { [](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 auto& axis_state = static_cast(*private_data.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 @@ -92,7 +92,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { [](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 auto& axis_state = static_cast(*private_data.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 @@ -102,7 +102,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { [](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 auto& axis_state = static_cast(*private_data.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; @@ -113,7 +113,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { [](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 auto& axis_state = static_cast(*private_data.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 @@ -151,7 +151,7 @@ void Abs_Axis::Private::bind_private_crtp(Object* object) { 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); + const auto& state = static_cast(*private_data.current); auto& output = prepared; output = {}; if (state.pixel_length == 0.0 || state.canvas_size.empty()) return; @@ -206,7 +206,7 @@ inline void Abs_Axis::Private::prepare_data(Attached auto* object) { 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); + const auto& state = static_cast(*private_data.current); auto& cache = object->template pending_buffer(); cache.ensure_size(state.canvas_size); cache.clear(); diff --git a/render_2D/render_2D/axis/Frequency_Axis.hpp b/render_2D/render_2D/axis/Frequency_Axis.hpp index b3843d1..2a89b0d 100644 --- a/render_2D/render_2D/axis/Frequency_Axis.hpp +++ b/render_2D/render_2D/axis/Frequency_Axis.hpp @@ -1,14 +1,12 @@ #pragma once #include "Numeric_Axis.hpp" namespace aethera::render_2d { -/* Frequency_Axis 状态标签,用于独立订阅频率轴层。 */ -struct Frequency_Axis_State_Tag {}; /* 根据数值量级自动选择 Hz、kHz 或 MHz 标签的数值轴。 */ -struct Frequency_Axis : Def> { +struct Frequency_Axis : Def { /* Frequency_Axis 不发布额外属性。 */ struct Prop : Prev_Prop {}; /* Frequency_Axis 没有重复保存数值轴状态,仅保留独立状态层。 */ - struct State : Prev_State { + struct State : Prev_State { bool operator==(const State&) const; }; /* 完整声明及频率标签 CRTP 覆盖见 Frequency_Axis.ipp。 */ diff --git a/render_2D/render_2D/axis/Frequency_Axis.ipp b/render_2D/render_2D/axis/Frequency_Axis.ipp index fb14172..ea58a6f 100644 --- a/render_2D/render_2D/axis/Frequency_Axis.ipp +++ b/render_2D/render_2D/axis/Frequency_Axis.ipp @@ -7,7 +7,7 @@ struct Frequency_Axis::Private : Prev_Private { 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 auto& state = static_cast(*final_private.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"; diff --git a/render_2D/render_2D/axis/Numeric_Axis.cpp b/render_2D/render_2D/axis/Numeric_Axis.cpp index 2b82141..1f39d15 100644 --- a/render_2D/render_2D/axis/Numeric_Axis.cpp +++ b/render_2D/render_2D/axis/Numeric_Axis.cpp @@ -2,4 +2,5 @@ namespace aethera::render_2d { bool Numeric_Axis::State::operator==(const State&) const = default; +bool Numeric_Axis::Prop::operator==(const Prop&) const = default; } diff --git a/render_2D/render_2D/axis/Numeric_Axis.hpp b/render_2D/render_2D/axis/Numeric_Axis.hpp index b29e3ad..2e47c98 100644 --- a/render_2D/render_2D/axis/Numeric_Axis.hpp +++ b/render_2D/render_2D/axis/Numeric_Axis.hpp @@ -1,19 +1,18 @@ #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 { +struct Numeric_Axis : Def { + struct Prop : Prev_Prop { Axis_Range coordinate_range{0.0, 20.0}; /* 当前有向数值区间;必须有限且非零。 */ Axis_Label_Precision precision{2}; /* 标签最大小数位数;格式化时限制到 0..12。 */ Number_Locale locale{}; /* 数值标签的小数点规则。 */ bool wheel_enabled{true}; /* 是否允许滚轮以指针位置为锚点缩放坐标范围。 */ bool drag_enabled{true}; /* 是否允许按住鼠标左键拖动坐标范围。 */ + bool operator==(const Prop&) const; + }; + /* 数值轴当前没有额外发布状态。 */ + struct State : Prev_State { bool operator==(const State&) const; }; /* 完整声明及数值轴 CRTP 能力见 Numeric_Axis.ipp。 */ diff --git a/render_2D/render_2D/axis/Numeric_Axis.ipp b/render_2D/render_2D/axis/Numeric_Axis.ipp index e82ef0e..10f4361 100644 --- a/render_2D/render_2D/axis/Numeric_Axis.ipp +++ b/render_2D/render_2D/axis/Numeric_Axis.ipp @@ -7,7 +7,7 @@ struct Numeric_Axis::Private : Prev_Private { std::mutex interaction_mutex{}; /* 保护跨事件保留的拖动手势状态。 */ bool dragging{}; /* 左键拖动手势是否已经开始且尚未释放。 */ Point_F last_pointer{}; /* 上一个拖动事件的位置,单位为画布局部像素。 */ - /* CRTP 实现:直接返回 Numeric_Axis_State_Tag 中唯一保存的数值范围。 */ + /* CRTP 实现:直接返回 Numeric_Axis::Base_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; @@ -16,13 +16,13 @@ struct Numeric_Axis::Private : Prev_Private { /* 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); + template + void after_prop_set(Object* object, Member Owner::* member, Prop_Access props); }; 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; + return static_cast(*private_data.current).coordinate_range; } inline double Numeric_Axis::Private::tick_step(const Attached auto*, Axis_Range coordinate_range) const { return nice_tick_step(coordinate_range); @@ -30,14 +30,14 @@ inline double Numeric_Axis::Private::tick_step(const Attached auto*, Axis_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); + const auto& state = static_cast(*private_data.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& numeric_state = static_cast(*private_data.current); + const auto& axis_state = static_cast(*private_data.current); const auto* pointer = dynamic_cast(&event); if (event.type == Event_Type::wheel && numeric_state.wheel_enabled) { const auto* wheel = dynamic_cast(&event); @@ -47,7 +47,7 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve 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{ + object->template set<&Prop::coordinate_range>(Axis_Range{ anchor + (range.origin - anchor) * factor, anchor + (range.target - anchor) * factor }); @@ -76,7 +76,7 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve ? 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{ + object->template set<&Prop::coordinate_range>(Axis_Range{ numeric_state.coordinate_range.origin + shift, numeric_state.coordinate_range.target + shift }); @@ -91,20 +91,20 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve if (was_dragging) event.accept(); } } -template -void Numeric_Axis::Private::after_state_set(Object*, Member Owner::* member, State_Access pending_states) { +template +void Numeric_Axis::Private::after_prop_set(Object*, Member Owner::* member, Prop_Access props) { 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; + auto& pending = props.template get(); + const auto& current = static_cast(*private_data.pending); + if constexpr (std::same_as && std::same_as) { + if (member != &Prop::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; + else if constexpr (std::same_as && std::same_as) { + if (member != &Prop::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 index d53213d..a522ef8 100644 --- a/render_2D/render_2D/axis/Time_Axis.cpp +++ b/render_2D/render_2D/axis/Time_Axis.cpp @@ -3,6 +3,7 @@ #include namespace aethera::render_2d { bool Time_Axis::State::operator==(const State&) const = default; +bool Time_Axis::Prop::operator==(const Prop&) const = default; std::size_t Time_Axis::Private::time_point_count(const Root* object) const { return time_dispatch->time_point_count(object); diff --git a/render_2D/render_2D/axis/Time_Axis.hpp b/render_2D/render_2D/axis/Time_Axis.hpp index 46b328e..e08869e 100644 --- a/render_2D/render_2D/axis/Time_Axis.hpp +++ b/render_2D/render_2D/axis/Time_Axis.hpp @@ -5,19 +5,18 @@ #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 { +struct Time_Axis : Def { + struct Prop : Prev_Prop { Axis_Visible_Count visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */ Axis_Pixel_Length tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */ Axis_Pixel_Length estimated_label_width_px{48.0}; /* 当前字体下单个时间标签的估算像素宽度;小于 48 时按 48 计算。 */ std::string format{"mm:ss.zzz"}; /* 标签格式;支持 hh、HH、mm、ss 和 zzz。 */ bool newest_at_start{}; /* true 时最新样本位于坐标区间起点。 */ + bool operator==(const Prop&) const; + }; + /* 时间样本是由 append_time 发布给观察方的运行状态。 */ + struct State : Prev_State { Axis_Time_Tick next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */ std::deque> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */ bool operator==(const State&) const; diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp index 8f309a7..4ad48e0 100644 --- a/render_2D/render_2D/axis/Time_Axis.ipp +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -33,16 +33,17 @@ inline Axis_Range Time_Axis::Private::coordinate_range(const Attached auto* obje using Object = std::remove_cv_t>; const auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.state.current); + const auto& prop = static_cast(*private_data.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)}; + const int earliest = std::max(0, latest - std::max(2, prop.visible_count) + 1); + if (prop.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 auto& time_state = static_cast(*private_data.current); + const auto& axis_state = static_cast(*private_data.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)); @@ -51,11 +52,12 @@ inline std::string Time_Axis::Private::tick_label(const Attached auto* object, d using Object = std::remove_cv_t>; const auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.state.current); + const auto& prop = static_cast(*private_data.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); + return current == state.samples.end() ? std::string{} : formatted_time(current->second, prop.format); } template const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() { @@ -68,12 +70,13 @@ const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() }, [](Root* root, Time_Of_Day time) { auto* object = static_cast(root); + const auto visible_count = object->template read_prop().visible_count; int tick{}; object->template update_state<&State::next_tick, &State::samples>([&](State_Access states) { - auto& state = states.template get(); + 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)); + const auto limit = static_cast(std::max(512, std::max(2, visible_count) * 4)); while (state.samples.size() > limit) state.samples.pop_front(); }); return tick; diff --git a/render_2D/render_2D/plottable/Afterglow.cpp b/render_2D/render_2D/plottable/Afterglow.cpp index cb63d1f..64253f8 100644 --- a/render_2D/render_2D/plottable/Afterglow.cpp +++ b/render_2D/render_2D/plottable/Afterglow.cpp @@ -1,2 +1,3 @@ #include "Afterglow.hpp" -namespace aethera::render_2d { bool Afterglow::State::operator==(const State&) const = default; void Afterglow::append_spectrum(std::span values) { static_cast(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector&& values) { append_spectrum(std::span(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } +namespace aethera::render_2d { bool Afterglow::Prop::operator==(const Prop&) const = default; +bool Afterglow::State::operator==(const State&) const = default; void Afterglow::append_spectrum(std::span values) { static_cast(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector&& values) { append_spectrum(std::span(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } diff --git a/render_2D/render_2D/plottable/Afterglow.hpp b/render_2D/render_2D/plottable/Afterglow.hpp index fd0491a..de52c6e 100644 --- a/render_2D/render_2D/plottable/Afterglow.hpp +++ b/render_2D/render_2D/plottable/Afterglow.hpp @@ -9,11 +9,9 @@ #include #include namespace aethera::render_2d { -struct Afterglow_State_Tag {}; -struct Afterglow : Def, Tagged_Buffer> { +struct Afterglow : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct Prop : Prev_Prop { Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */ Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ @@ -24,6 +22,12 @@ struct Afterglow : Def, T Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例,限制到 0..1。 */ Color_Map color_map{}; /* 强度到颜色的映射。 */ std::vector> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { + std::size_t history_count{}; /* 当前发布的历史频谱帧数。 */ + std::size_t latest_spectrum_point_count{}; /* 最新频谱包含的点数。 */ + std::size_t rendered_cell_count{}; /* 最近一次 Prepare 生成的色块数。 */ bool operator==(const State&) const; }; struct Private; diff --git a/render_2D/render_2D/plottable/Afterglow.ipp b/render_2D/render_2D/plottable/Afterglow.ipp index f29f2da..cd2ba92 100644 --- a/render_2D/render_2D/plottable/Afterglow.ipp +++ b/render_2D/render_2D/plottable/Afterglow.ipp @@ -30,34 +30,36 @@ struct Afterglow::Private : Prev_Private { void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); template [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:构建累加、归一化和着色三阶段 Prepare 子图。 */ - template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 覆盖:构建消费色块矩阵的 Paint 子图。 */ - template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ - template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); template void prepare_frame(Object* object); template void accumulate_partition(Object* object, Plot_Partition_Count index); void normalize_frame(); template void color_partition(Object* object, Plot_Partition_Count index); template void paint_frame(Object* object); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ - template void after_state_set(Object* object, Member Owner::* member, State_Access states); + template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); + template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; template Afterglow::Builder::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} template -std::expected, Dependency_Graph_Error> Afterglow::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast(*plot->d).bind_sources(scene, frequency_axis, power_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(plot.get()); prepare.template add_state_dependency(plot.get(), scene); prepare.template add_state_dependency(plot.get(), frequency_axis); prepare.template add_state_dependency(plot.get(), frequency_axis); prepare.template add_state_dependency(plot.get(), power_axis); prepare.template add_state_dependency(plot.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } +std::expected, Dependency_Graph_Error> Afterglow::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast(*plot->d).bind_sources(scene, frequency_axis, power_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(plot.get()); prepare.template add_prop_dependency(plot.get(), scene); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), power_axis); prepare.template add_prop_dependency(plot.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } template void Afterglow::append_spectrum(const Values& values) { append_spectrum(std::span(std::data(values), std::size(values))); } -template bool Afterglow::Private::should_rebuild_prepare_graph(Object*, const State& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); } +template bool Afterglow::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); } template -tf::Taskflow Afterglow::Private::build_prepare_graph(Object* object, const State& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("afterglow.prepare.frame"); auto normalize = graph.emplace([this] { normalize_frame(); }).name("afterglow.prepare.normalize"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto accumulate = graph.emplace([this, object, index] { accumulate_partition(object, index); }).name("afterglow.prepare.accumulate"); auto color = graph.emplace([this, object, index] { color_partition(object, index); }).name("afterglow.prepare.color"); begin.precede(accumulate); accumulate.precede(normalize); normalize.precede(color); } return graph; } -template tf::Taskflow Afterglow::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("afterglow.paint.frame"); return graph; } +tf::Taskflow Afterglow::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("afterglow.prepare.frame"); auto normalize = graph.emplace([this] { normalize_frame(); }).name("afterglow.prepare.normalize"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto accumulate = graph.emplace([this, object, index] { accumulate_partition(object, index); }).name("afterglow.prepare.accumulate"); auto color = graph.emplace([this, object, index] { color_partition(object, index); }).name("afterglow.prepare.color"); begin.precede(accumulate); accumulate.precede(normalize); normalize.precede(color); } return graph; } +template tf::Taskflow Afterglow::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("afterglow.paint.frame"); return graph; } template -void Afterglow::Private::prepare_frame(Object* object) { const auto& state = object->template read_state(); const auto& frequency_layout = frequency_axis->template read_state(); const auto& power_layout = power_axis->template read_state(); const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const int columns = static_cast(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); const int rows = static_cast(state.power_point_size ? state.power_point_size : std::max(1.0, std::abs(power_layout.pixel_length))); prepared = {}; prepared.canvas = scene->template read_state().viewport; prepared.layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; const std::size_t cells = static_cast(columns) * rows; prepared.intensity.assign(cells, 0.0); prepared.pixels.assign(cells, 0); prepared.valid = true; } +void Afterglow::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const int columns = static_cast(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); const int rows = static_cast(state.power_point_size ? state.power_point_size : std::max(1.0, std::abs(power_layout.pixel_length))); prepared = {}; prepared.canvas = scene->template read_prop().viewport; prepared.layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; const std::size_t cells = static_cast(columns) * rows; prepared.intensity.assign(cells, 0.0); prepared.pixels.assign(cells, 0); prepared.valid = true; } template -void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_state(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); Plot_Ratio attenuation{1.0}; const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); for (auto spectrum = state.spectra.rbegin(); spectrum != state.spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { const std::size_t count = std::min(columns, spectrum->size()); for (std::size_t column = first; column < std::min(last, count); ++column) { const int row = std::clamp(static_cast(detail::normalized_plot_value((*spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); prepared.intensity[static_cast(row) * columns + column] += attenuation; if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast(row + 1) * columns + column] += attenuation * 0.35; } } } +void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); Plot_Ratio attenuation{1.0}; const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); for (auto spectrum = state.spectra.rbegin(); spectrum != state.spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { const std::size_t count = std::min(columns, spectrum->size()); for (std::size_t column = first; column < std::min(last, count); ++column) { const int row = std::clamp(static_cast(detail::normalized_plot_value((*spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); prepared.intensity[static_cast(row) * columns + column] += attenuation; if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast(row + 1) * columns + column] += attenuation * 0.35; } } } inline void Afterglow::Private::normalize_frame() { if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end())); } template -void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_state(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); for (std::size_t column = first; column < last; ++column) for (int row = 0; row < rows; ++row) { const std::size_t cell = static_cast(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } } +void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); for (std::size_t column = first; column < last; ++column) for (int row = 0; row < rows; ++row) { const std::size_t cell = static_cast(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } } template void Afterglow::Private::paint_frame(Object* object) { auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); } -template void Afterglow::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template void Afterglow::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.history_count = prop.spectra.size(); state.latest_spectrum_point_count = prop.spectra.empty() ? 0 : prop.spectra.back().size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; } template -const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() { static const Dispatch value{[](Root* root, std::span values) { auto* object = static_cast(root); object->template update_state<&State::spectra>([values](State_Access states) { auto& state = states.template get(); state.spectra.emplace_back(values.begin(), values.end()); constexpr std::size_t history_limit = 64; while (state.spectra.size() > history_limit) state.spectra.erase(state.spectra.begin()); }); }, [](const Root* root) { return static_cast(root)->template read_state().spectra.size(); }, [](const Root* root) { const auto& spectra = static_cast(root)->template read_state().spectra; return spectra.empty() ? 0 : spectra.back().size(); }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } +const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() { static const Dispatch value{[](Root* root, std::span values) { auto* object = static_cast(root); object->template update_prop<&Prop::spectra>([values](Prop_Access props) { auto& state = props.template get(); state.spectra.emplace_back(values.begin(), values.end()); constexpr std::size_t history_limit = 64; while (state.spectra.size() > history_limit) state.spectra.erase(state.spectra.begin()); }); }, [](const Root* root) { return static_cast(root)->template read_prop().spectra.size(); }, [](const Root* root) { const auto& spectra = static_cast(root)->template read_prop().spectra; return spectra.empty() ? 0 : spectra.back().size(); }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } template void Afterglow::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } inline void Afterglow::Private::bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { scene = scene_value; frequency_axis = frequency_axis_value; power_axis = power_axis_value; } } diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/render_2D/plottable/Constellation_Diagram.cpp index c66bbae..e8375a3 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.cpp @@ -1,2 +1,3 @@ #include "Constellation_Diagram.hpp" -namespace aethera::render_2d { bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::State::operator==(const State&) const = default; void Constellation_Diagram::append_point(Point_F point) { static_cast(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast(*d).dispatch->fit(this); } } +namespace aethera::render_2d { bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::Prop::operator==(const Prop&) const = default; +bool Constellation_Diagram::State::operator==(const State&) const = default; void Constellation_Diagram::append_point(Point_F point) { static_cast(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast(*d).dispatch->fit(this); } } diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.hpp b/render_2D/render_2D/plottable/Constellation_Diagram.hpp index 667f50f..14b6ace 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.hpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.hpp @@ -8,12 +8,10 @@ #include namespace aethera::render_2d { enum class Constellation_Diagram_Type : std::uint8_t { psk4 = 4, psk8 = 8, psk16 = 16 }; -struct Constellation_Diagram_State_Tag {}; struct Constellation_Point { Point_F point{}; Plot_Duration_Milliseconds submitted_at_ms{}; bool operator==(const Constellation_Point&) const; }; -struct Constellation_Diagram : Def, Tagged_Buffer> { +struct Constellation_Diagram : Def> { using Scene_Object = Impl; using Axis_Object = Impl; - struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct Prop : Prev_Prop { Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */ Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */ Color point_color{Color::red_color()}; /* 接收点颜色。 */ @@ -22,6 +20,10 @@ struct Constellation_Diagram : Def points{}; /* 已提交且尚未过期的点。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { + std::size_t point_count{}; /* 当前发布且尚未过期的点数。 */ bool operator==(const State&) const; }; struct Private; diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.ipp b/render_2D/render_2D/plottable/Constellation_Diagram.ipp index 181198f..d085154 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.ipp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.ipp @@ -31,19 +31,21 @@ struct Constellation_Diagram::Private : Prev_Private { /* CRTP 覆盖:直接绘制已准备的星座点与锚点。 */ template void paint(Object* object); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ - template void after_state_set(Object* object, Member Owner::* member, State_Access states); + template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); + template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); [[nodiscard]] static Plot_Duration_Milliseconds now_ms(); }; template Constellation_Diagram::Builder::Builder(Scene_Object* scene_value, Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), scene(scene_value), i_axis(i_axis_value), q_axis(q_axis_value) {} template -std::expected, Dependency_Graph_Error> Constellation_Diagram::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast(*plot->d).bind_sources(scene, i_axis, q_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(i_axis); prepare.add(q_axis); prepare.add(plot.get()); prepare.template add_state_dependency(plot.get(), scene); prepare.template add_state_dependency(plot.get(), i_axis); prepare.template add_state_dependency(plot.get(), i_axis); prepare.template add_state_dependency(plot.get(), q_axis); prepare.template add_state_dependency(plot.get(), q_axis); paint.add(i_axis); paint.add(q_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } +std::expected, Dependency_Graph_Error> Constellation_Diagram::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast(*plot->d).bind_sources(scene, i_axis, q_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(i_axis); prepare.add(q_axis); prepare.add(plot.get()); prepare.template add_prop_dependency(plot.get(), scene); prepare.template add_prop_dependency(plot.get(), i_axis); prepare.template add_prop_dependency(plot.get(), i_axis); prepare.template add_prop_dependency(plot.get(), q_axis); prepare.template add_prop_dependency(plot.get(), q_axis); paint.add(i_axis); paint.add(q_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } inline Plot_Duration_Milliseconds Constellation_Diagram::Private::now_ms() { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); } template -void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_state(); const auto& i_layout = i_axis->template read_state(); const auto& q_layout = q_axis->template read_state(); prepared = {}; prepared.canvas = scene->template read_state().viewport; if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) return; const auto current = now_ms(); for (const auto& value : state.points) if (current - value.submitted_at_ms <= state.point_lifetime_ms) prepared.points.push_back(detail::map_plot_point(i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); const int count = static_cast(state.type); const Plot_Coordinate center_i = state.i_range.center(); const Plot_Coordinate center_q = state.q_range.center(); const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for (int index = 0; index < count; ++index) { const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); } prepared.valid = true; object->template mark_dirty(); } -template void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_state(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); } -template void Constellation_Diagram::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_prop(); const auto& i_layout = i_axis->template read_prop(); const auto& q_layout = q_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) return; const auto current = now_ms(); for (const auto& value : state.points) if (current - value.submitted_at_ms <= state.point_lifetime_ms) prepared.points.push_back(detail::map_plot_point(i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); const int count = static_cast(state.type); const Plot_Coordinate center_i = state.i_range.center(); const Plot_Coordinate center_q = state.q_range.center(); const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for (int index = 0; index < count; ++index) { const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); } prepared.valid = true; object->template mark_dirty(); } +template void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); } +template void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template void Constellation_Diagram::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { pending_states.template get().point_count = static_cast(*current_prop).points.size(); } template -const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() { static const Dispatch value{[](Root* root, Point_F point) { auto* object = static_cast(root); const auto submitted = Private::now_ms(); object->template update_state<&State::points>([=](State_Access states) { auto& state = states.template get(); state.points.erase(std::remove_if(state.points.begin(), state.points.end(), [=](const auto& value) { return submitted - value.submitted_at_ms > state.point_lifetime_ms; }), state.points.end()); state.points.push_back({point, submitted}); }); }, [](const Root* root) { return static_cast(root)->template read_state().points.size(); }, [](Root* root) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const auto& state = object->template read_state(); const Plot_Coordinate size = std::max(state.i_range.size(), state.q_range.size()); const Plot_Coordinate i_center = state.i_range.center(); const Plot_Coordinate q_center = state.q_range.center(); data.i_axis->template update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{i_center - size * 0.5, i_center + size * 0.5}); data.q_axis->template update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{q_center - size * 0.5, q_center + size * 0.5}); }}; return value; } +const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() { static const Dispatch value{[](Root* root, Point_F point) { auto* object = static_cast(root); const auto submitted = Private::now_ms(); object->template update_prop<&Prop::points>([=](Prop_Access props) { auto& state = props.template get(); state.points.erase(std::remove_if(state.points.begin(), state.points.end(), [=](const auto& value) { return submitted - value.submitted_at_ms > state.point_lifetime_ms; }), state.points.end()); state.points.push_back({point, submitted}); }); }, [](const Root* root) { return static_cast(root)->template read_prop().points.size(); }, [](Root* root) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const auto& state = object->template read_prop(); const Plot_Coordinate size = std::max(state.i_range.size(), state.q_range.size()); const Plot_Coordinate i_center = state.i_range.center(); const Plot_Coordinate q_center = state.q_range.center(); data.i_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{i_center - size * 0.5, i_center + size * 0.5}); data.q_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{q_center - size * 0.5, q_center + size * 0.5}); }}; return value; } template void Constellation_Diagram::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } inline void Constellation_Diagram::Private::bind_sources(Scene_Object* scene_value, Axis_Object* i_axis_value, Axis_Object* q_axis_value) { scene = scene_value; i_axis = i_axis_value; q_axis = q_axis_value; } } diff --git a/render_2D/render_2D/plottable/Frequency_Trace.cpp b/render_2D/render_2D/plottable/Frequency_Trace.cpp index a1fa9d9..40639e4 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.cpp @@ -1,6 +1,7 @@ #include "Frequency_Trace.hpp" namespace aethera::render_2d { bool Frequency_Trace_Sample::operator==(const Frequency_Trace_Sample&) const = default; +bool Frequency_Trace::Prop::operator==(const Prop&) const = default; bool Frequency_Trace::State::operator==(const State&) const = default; void Frequency_Trace::append_sample(Plot_Time_Tick tick, Plot_Value value) { static_cast(*d).dispatch->append(this, tick, value); } void Frequency_Trace::append_sample(Time_Of_Day time, Plot_Value value) { static_cast(*d).dispatch->append_time(this, time, value); } diff --git a/render_2D/render_2D/plottable/Frequency_Trace.hpp b/render_2D/render_2D/plottable/Frequency_Trace.hpp index 8f18f1e..f18fc03 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.hpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.hpp @@ -7,22 +7,25 @@ #include #include namespace aethera::render_2d { -struct Frequency_Trace_State_Tag {}; struct Frequency_Trace_Sample { Plot_Time_Tick tick{}; /* 时间轴上的单调样本序号。 */ Plot_Value value{}; /* 该时间点对应的频率值。 */ bool operator==(const Frequency_Trace_Sample&) const; }; -struct Frequency_Trace : Def, Tagged_Buffer> { +struct Frequency_Trace : Def> { using Scene_Object = Impl; using Time_Object = Impl; using Value_Object = Impl; - struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct Prop : Prev_Prop { Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */ Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ Plot_Partition_Count partition_count{1}; /* fixed 模式的分块数量。 */ std::vector samples{}; /* 已提交轨迹样本的唯一权威集合。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { + std::size_t sample_count{}; /* 当前发布的轨迹样本数。 */ + std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的折线点数。 */ bool operator==(const State&) const; }; struct Private; diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp index 9cb5cbe..1ffeab7 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -29,16 +29,17 @@ struct Frequency_Trace::Private : Prev_Private { void bind_sources(Scene_Object* scene_value, Time_Object* time_axis_value, Value_Object* value_axis_value); template [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:按当前样本规模构建分块 Prepare 子图。 */ - template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 覆盖:构建消费已准备曲线的 Paint 子图。 */ - template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ - template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); template void prepare_frame(Object* object, Plot_Partition_Count partition_count); template void prepare_partition(Object* object, Plot_Partition_Count partition_index); template void paint_frame(Object* object); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ - template void after_state_set(Object* object, Member Owner::* member, State_Access pending_states); + template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); + template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; template Frequency_Trace::Builder::Builder(Scene_Object* scene_value, Time_Object* time_axis_value, Value_Object* value_axis_value) : Base(), scene(scene_value), time_axis(time_axis_value), value_axis(value_axis_value) {} @@ -48,50 +49,52 @@ std::expected, Dependency_Graph_Error> Frequency_Trace:: static_cast(*trace->d).bind_sources(scene, time_axis, value_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(time_axis); prepare.add(value_axis); prepare.add(trace.get()); - prepare.template add_state_dependency(trace.get(), scene); - prepare.template add_state_dependency(trace.get(), time_axis); prepare.template add_state_dependency(trace.get(), time_axis); - prepare.template add_state_dependency(trace.get(), value_axis); prepare.template add_state_dependency(trace.get(), value_axis); + prepare.template add_prop_dependency(trace.get(), scene); + prepare.template add_prop_dependency(trace.get(), time_axis); prepare.template add_prop_dependency(trace.get(), time_axis); + prepare.template add_prop_dependency(trace.get(), value_axis); prepare.template add_prop_dependency(trace.get(), value_axis); paint.add(time_axis); paint.add(value_axis); paint.add(trace.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(trace); } template -bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const State& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); } +bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); } template -tf::Taskflow Frequency_Trace::Private::build_prepare_graph(Object* object, const State& state) { +tf::Taskflow Frequency_Trace::Private::build_prepare_graph(Object* object, const Prop& state) { graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object, graph_partition_count); }).name("frequency_trace.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("frequency_trace.prepare.partition"); begin.precede(task); } return graph; } template -tf::Taskflow Frequency_Trace::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("frequency_trace.paint.frame"); return graph; } +tf::Taskflow Frequency_Trace::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("frequency_trace.paint.frame"); return graph; } template void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Count partition_count) { - const auto& state = object->template read_state(); const auto& time_layout = time_axis->template read_state(); const auto& value_layout = value_axis->template read_state(); - prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_state().viewport; + const auto& state = object->template read_prop(); const auto& time_layout = time_axis->template read_prop(); const auto& value_layout = value_axis->template read_prop(); + prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_prop().viewport; if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation || state.samples.empty()) return; prepared.values.reserve(state.samples.size()); for (const auto& sample : state.samples) prepared.values.push_back(sample.value); prepared.domain = {static_cast(state.samples.front().tick), static_cast(state.samples.back().tick)}; prepared.valid = true; } template void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_Count partition_index) { - if (!prepared.valid) return; const auto& state = object->template read_state(); const auto& time_layout = time_axis->template read_state(); const auto& value_layout = value_axis->template read_state(); const auto& value_state = value_axis->template read_state(); + if (!prepared.valid) return; const auto& state = object->template read_prop(); const auto& time_layout = time_axis->template read_prop(); const auto& value_layout = value_axis->template read_prop(); const auto& value_state = value_axis->template read_prop(); const auto range = detail::curve_partition_range(prepared.values.size(), partition_index, prepared.partitions.size(), prepared.domain); prepared.partitions[partition_index] = detail::prepare_curve(std::span(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, Line_Interpolation_Mode::linear_value, true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation); } template void Frequency_Trace::Private::paint_frame(Object* object) { - const auto& state = object->template read_state(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); + const auto& state = object->template read_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); } -template -void Frequency_Trace::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template +void Frequency_Trace::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template +void Frequency_Trace::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.sample_count = prop.samples.size(); state.rendered_point_count = 0; for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.points.size(); } template const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for() { static const Dispatch value{ - [](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { auto* object = static_cast(root); object->template update_state<&State::samples>([=](State_Access states) { states.template get().samples.push_back({tick, sample_value}); }); }, - [](Root* root, Time_Of_Day time, Plot_Value sample_value) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const Plot_Time_Tick tick = data.time_axis->append_time(time); object->template update_state<&State::samples>([=](State_Access states) { states.template get().samples.push_back({tick, sample_value}); }); }, - [](const Root* root) { return static_cast(root)->template read_state().samples.size(); }, + [](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { auto* object = static_cast(root); object->template update_prop<&Prop::samples>([=](Prop_Access props) { props.template get().samples.push_back({tick, sample_value}); }); }, + [](Root* root, Time_Of_Day time, Plot_Value sample_value) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const Plot_Time_Tick tick = data.time_axis->append_time(time); object->template update_prop<&Prop::samples>([=](Prop_Access props) { props.template get().samples.push_back({tick, sample_value}); }); }, + [](const Root* root) { return static_cast(root)->template read_prop().samples.size(); }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; } }; return value; } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp index 8d3be69..50cac33 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -1,5 +1,6 @@ #include "Selection_Rectangle_Overlay.hpp" namespace aethera::render_2d { +bool Selection_Rectangle_Overlay::Prop::operator==(const Prop&) const = default; bool Selection_Rectangle_Overlay::State::operator==(const State&) const = default; std::vector Selection_Rectangle_Overlay::selected_regions() const { return static_cast(*d).dispatch->selected_regions(this); } void Selection_Rectangle_Overlay::clear_selected_regions() { static_cast(*d).dispatch->clear_selected_regions(this); } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp index 491e1ad..193d775 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp @@ -7,17 +7,19 @@ #include #include namespace aethera::render_2d { -struct Selection_Rectangle_Overlay_State_Tag {}; -struct Selection_Rectangle_Overlay : Def, Tagged_Buffer> { +struct Selection_Rectangle_Overlay : Def> { using Scene_Object = Impl; using Axis_Object = Impl; - struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct Prop : Prev_Prop { Font label_font{}; /* 选择范围标签使用的字体。 */ Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */ Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::solid}; /* 选择矩形内部填充。 */ Pen selection_border_pen{Color::white(), 1.0, Line_Style::dash}; /* 选择矩形边框样式。 */ std::vector selected_regions{}; /* 已完成选择的轴坐标矩形。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { + std::size_t selected_region_count{}; /* 当前已完成选择的矩形数量。 */ bool operator==(const State&) const; }; struct Private; diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp index 9a7c621..7a46fd8 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp @@ -26,7 +26,8 @@ struct Selection_Rectangle_Overlay::Private : Prev_Private { /* CRTP 覆盖:处理拖拽并更新权威选择区域状态。 */ template void handle_event(Object* object, const Event& event); /* CRTP 覆盖:本类状态写入后只标记 Paint 数据失效。 */ - template void after_state_set(Object* object, Member Owner::* member, State_Access pending_states); + template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); + template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; template Selection_Rectangle_Overlay::Builder::Builder(Scene_Object* scene_value, Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value) : Base(), scene(scene_value), horizontal_axis(horizontal_axis_value), vertical_axis(vertical_axis_value) {} @@ -38,11 +39,11 @@ std::expected, Dependency_Graph_Error> Selection_Rectang static_cast(*overlay->d).bind_sources(scene, horizontal_axis, vertical_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& paint) { paint.add(overlay.get()); - paint.template add_state_dependency(overlay.get(), scene); - paint.template add_state_dependency(overlay.get(), horizontal_axis); - paint.template add_state_dependency(overlay.get(), horizontal_axis); - paint.template add_state_dependency(overlay.get(), vertical_axis); - paint.template add_state_dependency(overlay.get(), vertical_axis); + paint.template add_prop_dependency(overlay.get(), scene); + paint.template add_prop_dependency(overlay.get(), horizontal_axis); + paint.template add_prop_dependency(overlay.get(), horizontal_axis); + paint.template add_prop_dependency(overlay.get(), vertical_axis); + paint.template add_prop_dependency(overlay.get(), vertical_axis); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(overlay); @@ -50,8 +51,8 @@ std::expected, Dependency_Graph_Error> Selection_Rectang template void Selection_Rectangle_Overlay::Private::paint(Object* object) { auto& data = static_cast(*this); - const auto& state = static_cast(*data.state.current); - const Size canvas = scene->template read_state().viewport; + const auto& state = static_cast(*data.current); + const Size canvas = scene->template read_prop().viewport; auto& cache = object->template pending_buffer(); cache.ensure_size(canvas); cache.clear(); if (canvas.empty()) return; @@ -76,16 +77,18 @@ void Selection_Rectangle_Overlay::Private::handle_event(Object* object, const Ev const Axis_Coordinate second_x = horizontal_axis->point_to_coordinate(drag_current); const Axis_Coordinate first_y = vertical_axis->point_to_coordinate(drag_origin); const Axis_Coordinate second_y = vertical_axis->point_to_coordinate(drag_current); - object->template update_state<&State::selected_regions>([=](State_Access states) { states.template get().selected_regions.push_back(Rect_F{first_x, first_y, second_x - first_x, second_y - first_y}.normalized()); }); + object->template update_prop<&Prop::selected_regions>([=](Prop_Access props) { props.template get().selected_regions.push_back(Rect_F{first_x, first_y, second_x - first_x, second_y - first_y}.normalized()); }); event.accept(); } -template -void Selection_Rectangle_Overlay::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template +void Selection_Rectangle_Overlay::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template +void Selection_Rectangle_Overlay::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { pending_states.template get().selected_region_count = static_cast(*current_prop).selected_regions.size(); } template const Selection_Rectangle_Overlay::Private::Dispatch& Selection_Rectangle_Overlay::Private::dispatch_for() { static const Dispatch value{ - [](const Root* root) { return static_cast(root)->template read_state().selected_regions; }, - [](Root* root) { auto* object = static_cast(root); object->template update_state<&State::selected_regions>(std::vector{}); } + [](const Root* root) { return static_cast(root)->template read_prop().selected_regions; }, + [](Root* root) { static_cast(root)->template set<&Prop::selected_regions>(std::vector{}); } }; return value; } diff --git a/render_2D/render_2D/plottable/Spectrum.cpp b/render_2D/render_2D/plottable/Spectrum.cpp index de18519..d8ab8ee 100644 --- a/render_2D/render_2D/plottable/Spectrum.cpp +++ b/render_2D/render_2D/plottable/Spectrum.cpp @@ -1,6 +1,7 @@ #include "Spectrum.hpp" namespace aethera::render_2d { bool Spectrum_Frame::operator==(const Spectrum_Frame&) const = default; +bool Spectrum::Prop::operator==(const Prop&) const = default; bool Spectrum::State::operator==(const State&) const = default; void Spectrum::update_samples(std::span values) { static_cast(*d).dispatch->update_samples(this, values); diff --git a/render_2D/render_2D/plottable/Spectrum.hpp b/render_2D/render_2D/plottable/Spectrum.hpp index f53c5a9..fb6dad7 100644 --- a/render_2D/render_2D/plottable/Spectrum.hpp +++ b/render_2D/render_2D/plottable/Spectrum.hpp @@ -15,7 +15,6 @@ using Spectrum_Power = Plot_Value; using Spectrum_Interpolation_Ratio = Plot_Ratio; using Spectrum_Marker_Index = Plot_Index; using Spectrum_Partition_Mode = Plot_Partition_Mode; -struct Spectrum_State_Tag {}; struct Spectrum_Frame_Tag {}; struct Spectrum_Frame { std::vector samples{}; /* 最近提交的当前频谱功率样本。 */ @@ -24,12 +23,11 @@ struct Spectrum_Frame { bool operator==(const Spectrum_Frame&) const; }; /* 使用频率轴和功率轴分块准备、绘制当前值、保持曲线及频率标记。 */ -struct Spectrum : Def, Tagged_Buffer, Tagged_Buffer> { +struct Spectrum : Def, Tagged_Buffer> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct Prop : Prev_Prop { Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */ Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */ @@ -54,6 +52,12 @@ struct Spectrum : Def, Tagg Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */ std::vector custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */ Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { + std::size_t sample_count{}; /* 最近一次发布帧的样本数。 */ + std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的曲线点数。 */ + std::size_t selectable_marker_count{}; /* 当前可选择的自定义标记数。 */ bool operator==(const State&) const; }; /* 完整声明、分块子图、内部状态操作和 CRTP 分派见 Spectrum.ipp。 */ diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index c442eb4..5f02ee9 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -80,18 +80,19 @@ struct Spectrum::Private : Prev_Private { template [[nodiscard]] Set_Marker_Frequency_Result set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency); template [[nodiscard]] Set_Current_Marker_Frequency_Result set_current_marker_frequency(Object* object, Spectrum_Frequency frequency); /* CRTP State 钩子:Spectrum 业务状态写入后标记自身 Prepare;其他继承层状态由各自 Private 负责。 */ - template void after_state_set(Object* object, Member Owner::* member, State_Access pending_states); + template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); + template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); /* CRTP 子图能力:按当前样本数和 State 分块策略构建并行 Prepare 图。 */ - template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 子图能力:构建背景、分块曲线和覆盖标记的 Paint 图。 */ - template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:样本规模或分块配置改变时重建 Prepare 子图。 */ - template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); - template [[nodiscard]] std::size_t desired_partition_count(const Object* object, const State& state) const; + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); + template [[nodiscard]] std::size_t desired_partition_count(const Object* object, const Prop& state) const; template void prepare_frame(Object* object, std::size_t partition_count); template void prepare_partition(Object* object, std::size_t partition_index); template void paint_frame(Object* object); - [[nodiscard]] static std::expected power_at(const State& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency); + [[nodiscard]] static std::expected power_at(const Prop& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency); }; template Spectrum::Builder::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} @@ -105,11 +106,11 @@ std::expected, Dependency_Graph_Error> Spectrum::Builder prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(spectrum.get()); - prepare.template add_state_dependency(spectrum.get(), scene); - prepare.template add_state_dependency(spectrum.get(), frequency_axis); - prepare.template add_state_dependency(spectrum.get(), frequency_axis); - prepare.template add_state_dependency(spectrum.get(), power_axis); - prepare.template add_state_dependency(spectrum.get(), power_axis); + prepare.template add_prop_dependency(spectrum.get(), scene); + prepare.template add_prop_dependency(spectrum.get(), frequency_axis); + prepare.template add_prop_dependency(spectrum.get(), frequency_axis); + prepare.template add_prop_dependency(spectrum.get(), power_axis); + prepare.template add_prop_dependency(spectrum.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(spectrum.get()); @@ -121,7 +122,7 @@ template void Spectrum::update_samples(const Values& values) { update_samples(std::span(std::data(values), std::size(values))); } -inline std::expected Spectrum::Private::power_at(const State& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency) { +inline std::expected Spectrum::Private::power_at(const Prop& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency) { if (frame.samples.empty()) return std::unexpected(Power_At_Result::no_samples); if (state.frequency_range.length() == 0.0) return std::unexpected(Power_At_Result::invalid_frequency_range); if (!state.frequency_range.contains(frequency)) return std::unexpected(Power_At_Result::frequency_out_of_range); @@ -133,15 +134,15 @@ inline std::expected Spectrum::Privat return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction; } template -std::size_t Spectrum::Private::desired_partition_count(const Object* object, const State& state) const { +std::size_t Spectrum::Private::desired_partition_count(const Object* object, const Prop& state) const { return detail::curve_partition_count(state.partition_mode, state.partition_count, object->template current_buffer().samples.size()); } template -bool Spectrum::Private::should_rebuild_prepare_graph(Object* object, const State& state) { +bool Spectrum::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { return prepare_graph_partition_count != desired_partition_count(object, state); } template -tf::Taskflow Spectrum::Private::build_prepare_graph(Object* object, const State& state) { +tf::Taskflow Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t partition_count = desired_partition_count(object, state); prepare_graph_partition_count = partition_count; tf::Taskflow graph; @@ -153,7 +154,7 @@ tf::Taskflow Spectrum::Private::build_prepare_graph(Object* object, const State& return graph; } template -tf::Taskflow Spectrum::Private::build_paint_graph(Object* object, const State&) { +tf::Taskflow Spectrum::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("spectrum.paint.frame"); return graph; @@ -161,12 +162,12 @@ tf::Taskflow Spectrum::Private::build_paint_graph(Object* object, const State&) template void Spectrum::Private::prepare_frame(Object* object, std::size_t partition_count) { auto& private_data = static_cast(*this); - const auto& state = static_cast(*private_data.state.current); + const auto& state = static_cast(*private_data.current); const auto& frame = object->template current_buffer(); - const auto& scene_state = scene->template read_state(); - const auto& frequency_layout = frequency_axis->template read_state(); - const auto& power_layout = power_axis->template read_state(); - const auto& power_state = power_axis->template read_state(); + const auto& scene_state = scene->template read_prop(); + const auto& frequency_layout = frequency_axis->template read_prop(); + const auto& power_layout = power_axis->template read_prop(); + const auto& power_state = power_axis->template read_prop(); prepared = {}; prepared.partitions.resize(partition_count); if (frequency_layout.orientation == power_layout.orientation) return; @@ -196,13 +197,13 @@ template void Spectrum::Private::prepare_partition(Object* object, std::size_t partition_index) { if (!prepared.valid || partition_index >= prepared.partitions.size()) return; auto& private_data = static_cast(*this); - const auto& state = static_cast(*private_data.state.current); + const auto& state = static_cast(*private_data.current); const auto& frame = object->template current_buffer(); if (frame.samples.empty()) return; - const auto& frequency_layout = frequency_axis->template read_state(); - const auto& power_layout = power_axis->template read_state(); - const auto& frequency_state = frequency_axis->template read_state(); - const auto& power_state = power_axis->template read_state(); + const auto& frequency_layout = frequency_axis->template read_prop(); + const auto& power_layout = power_axis->template read_prop(); + const auto& frequency_state = frequency_axis->template read_prop(); + const auto& power_state = power_axis->template read_prop(); const auto range = detail::curve_partition_range(frame.samples.size(), partition_index, prepared.partitions.size(), state.frequency_range); auto& partition = prepared.partitions[partition_index]; partition.clip = detail::map_plot_rect(frequency_axis, range.domain, power_axis, power_state.coordinate_range, frequency_layout.orientation); @@ -213,7 +214,7 @@ void Spectrum::Private::prepare_partition(Object* object, std::size_t partition_ template void Spectrum::Private::paint_frame(Object* object) { auto& private_data = static_cast(*this); - const auto& state = static_cast(*private_data.state.current); + const auto& state = static_cast(*private_data.current); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas_size); cache.clear(); @@ -252,13 +253,13 @@ void Spectrum::Private::update_samples(Object* object, std::span void Spectrum::Private::add_marker(Object* object, Spectrum_Frequency frequency) { - object->template update_state<&State::custom_markers>([frequency](State_Access states) { states.template get().custom_markers.push_back(frequency); }); + object->template update_prop<&Prop::custom_markers>([frequency](Prop_Access props) { props.template get().custom_markers.push_back(frequency); }); object->template mark_dirty(); } template void Spectrum::Private::remove_marker(Object* object, Spectrum_Frequency frequency) { - object->template update_state<&State::custom_markers, &State::selected_marker>([frequency](State_Access states) { - auto& state = states.template get(); + object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([frequency](Prop_Access props) { + auto& state = props.template get(); if (state.custom_markers.empty()) return; const auto closest = std::min_element(state.custom_markers.begin(), state.custom_markers.end(), [frequency](Spectrum_Frequency left, Spectrum_Frequency right) { return std::abs(left - frequency) < std::abs(right - frequency); }); const Spectrum_Marker_Index removed = std::distance(state.custom_markers.begin(), closest); @@ -270,8 +271,8 @@ void Spectrum::Private::remove_marker(Object* object, Spectrum_Frequency frequen } template void Spectrum::Private::remove_selected_marker(Object* object) { - object->template update_state<&State::custom_markers, &State::selected_marker>([](State_Access states) { - auto& state = states.template get(); + object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([](Prop_Access props) { + auto& state = props.template get(); if (state.selected_marker < 0 || static_cast(state.selected_marker) >= state.custom_markers.size()) return; state.custom_markers.erase(state.custom_markers.begin() + state.selected_marker); state.selected_marker = -1; @@ -280,40 +281,49 @@ void Spectrum::Private::remove_selected_marker(Object* object) { } template void Spectrum::Private::clear_markers(Object* object) { - object->template update_state<&State::custom_markers, &State::selected_marker>([](State_Access states) { auto& state = states.template get(); state.custom_markers.clear(); state.selected_marker = -1; }); + object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([](Prop_Access props) { auto& state = props.template get(); state.custom_markers.clear(); state.selected_marker = -1; }); object->template mark_dirty(); } template void Spectrum::Private::set_selected_marker(Object* object, Spectrum_Marker_Index index) { - object->template update_state<&State::selected_marker>([index](State_Access states) { auto& state = states.template get(); state.selected_marker = index >= 0 && static_cast(index) < state.custom_markers.size() ? index : -1; }); + object->template update_prop<&Prop::selected_marker>([index](Prop_Access props) { auto& state = props.template get(); state.selected_marker = index >= 0 && static_cast(index) < state.custom_markers.size() ? index : -1; }); object->template mark_dirty(); } template void Spectrum::Private::select_next_marker(Object* object) { - object->template update_state<&State::selected_marker>([](State_Access states) { auto& state = states.template get(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker + 1) % static_cast(state.custom_markers.size()); }); + object->template update_prop<&Prop::selected_marker>([](Prop_Access props) { auto& state = props.template get(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker + 1) % static_cast(state.custom_markers.size()); }); object->template mark_dirty(); } template void Spectrum::Private::select_previous_marker(Object* object) { - object->template update_state<&State::selected_marker>([](State_Access states) { auto& state = states.template get(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker <= 0 ? static_cast(state.custom_markers.size()) : state.selected_marker) - 1; }); + object->template update_prop<&Prop::selected_marker>([](Prop_Access props) { auto& state = props.template get(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker <= 0 ? static_cast(state.custom_markers.size()) : state.selected_marker) - 1; }); object->template mark_dirty(); } template Spectrum::Set_Marker_Frequency_Result Spectrum::Private::set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency) { - if (index < 0 || static_cast(index) >= object->template read_state().custom_markers.size()) return Set_Marker_Frequency_Result::index_out_of_range; - object->template update_state<&State::custom_markers>([index, frequency](State_Access states) { states.template get().custom_markers[static_cast(index)] = frequency; }); + if (index < 0 || static_cast(index) >= object->template read_prop().custom_markers.size()) return Set_Marker_Frequency_Result::index_out_of_range; + object->template update_prop<&Prop::custom_markers>([index, frequency](Prop_Access props) { props.template get().custom_markers[static_cast(index)] = frequency; }); object->template mark_dirty(); return Set_Marker_Frequency_Result::updated; } template Spectrum::Set_Current_Marker_Frequency_Result Spectrum::Private::set_current_marker_frequency(Object* object, Spectrum_Frequency frequency) { - const Spectrum_Marker_Index index = object->template read_state().selected_marker; + const Spectrum_Marker_Index index = object->template read_prop().selected_marker; if (index < 0) return Set_Current_Marker_Frequency_Result::no_selection; return set_marker_frequency(object, index, frequency) == Set_Marker_Frequency_Result::updated ? Set_Current_Marker_Frequency_Result::updated : Set_Current_Marker_Frequency_Result::no_selection; } -template -void Spectrum::Private::after_state_set(Object* object, Member Owner::*, State_Access) { - if constexpr (std::same_as) object->template mark_dirty(); +template +void Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { + if constexpr (std::same_as) object->template mark_dirty(); +} +template +void Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { + const auto& private_data = static_cast(*this); + auto& state = pending_states.template get(); + state.sample_count = private_data.buffer_storage.template get().pending->samples.size(); + state.rendered_point_count = 0; + for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.current.points.size(); + state.selectable_marker_count = static_cast(*current_prop).custom_markers.size(); } template const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() { @@ -321,17 +331,17 @@ const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() { [](Root* root, std::span values) { auto* object = static_cast(root); static_cast(*object->d).update_samples(object, values); }, [](const Root* root) { return static_cast(root)->template current_buffer().samples.size(); }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); std::size_t count{}; for (const auto& partition : data.prepared.partitions) count += partition.current.points.size(); return count; }, - [](const Root* root, Spectrum_Frequency frequency) { const auto* object = static_cast(root); const auto& data = static_cast(*object->d); return Private::power_at(static_cast(*data.state.current), object->template current_buffer(), frequency); }, + [](const Root* root, Spectrum_Frequency frequency) { const auto* object = static_cast(root); const auto& data = static_cast(*object->d); return Private::power_at(static_cast(*data.current), object->template current_buffer(), frequency); }, [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast(root); static_cast(*object->d).add_marker(object, frequency); }, [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast(root); static_cast(*object->d).remove_marker(object, frequency); }, [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).remove_selected_marker(object); }, [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).clear_markers(object); }, - [](const Root* root) { return static_cast(root)->template read_state().custom_markers.size(); }, - [](const Root* root) { return static_cast(root)->template read_state().selected_marker; }, + [](const Root* root) { return static_cast(root)->template read_prop().custom_markers.size(); }, + [](const Root* root) { return static_cast(root)->template read_prop().selected_marker; }, [](Root* root, Spectrum_Marker_Index index) { auto* object = static_cast(root); static_cast(*object->d).set_selected_marker(object, index); }, [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).select_next_marker(object); }, [](Root* root) { auto* object = static_cast(root); static_cast(*object->d).select_previous_marker(object); }, - [](const Root* root, Spectrum_Marker_Index index) -> std::expected { const auto& markers = static_cast(root)->template read_state().custom_markers; if (index < 0 || static_cast(index) >= markers.size()) return std::unexpected(Marker_Frequency_Result::index_out_of_range); return markers[static_cast(index)]; }, + [](const Root* root, Spectrum_Marker_Index index) -> std::expected { const auto& markers = static_cast(root)->template read_prop().custom_markers; if (index < 0 || static_cast(index) >= markers.size()) return std::unexpected(Marker_Frequency_Result::index_out_of_range); return markers[static_cast(index)]; }, [](Root* root, Spectrum_Marker_Index index, Spectrum_Frequency frequency) { auto* object = static_cast(root); return static_cast(*object->d).set_marker_frequency(object, index, frequency); }, [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast(root); return static_cast(*object->d).set_current_marker_frequency(object, frequency); } }; diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp index a7c5a7b..36a8284 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp @@ -1,5 +1,6 @@ #include "Sweep_Spectrum.hpp" namespace aethera::render_2d { +bool Sweep_Spectrum::Prop::operator==(const Prop&) const = default; bool Sweep_Spectrum::State::operator==(const State&) const = default; void Sweep_Spectrum::append_block(std::span values) { static_cast(*d).dispatch->append(this, values); } void Sweep_Spectrum::append_block(std::pmr::vector&& values) { append_block(std::span(values.data(), values.size())); } diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp index b89b4f1..96725ea 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp @@ -9,13 +9,11 @@ #include #include namespace aethera::render_2d { -struct Sweep_Spectrum_State_Tag {}; -struct Sweep_Spectrum : Def, Tagged_Buffer> { +struct Sweep_Spectrum : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct Prop : Prev_Prop { Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */ std::size_t bins_per_block{}; /* 每个扫描块期望的功率点数;零值接受首块尺寸。 */ std::size_t block_count{1}; /* 最多保留的扫描块数;零值按 1 处理。 */ @@ -26,6 +24,12 @@ struct Sweep_Spectrum : Def> blocks{}; /* 已提交扫描块的唯一权威集合。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { + std::size_t stored_block_count{}; /* 当前发布的扫描块数。 */ + std::size_t stored_point_count{}; /* 当前发布的扫描点总数。 */ + std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的曲线点数。 */ bool operator==(const State&) const; }; struct Private; diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp index 7a10a58..086c918 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp @@ -29,52 +29,55 @@ struct Sweep_Spectrum::Private : Prev_Private { void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); template [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:按扫描点规模构建分块 Prepare 子图。 */ - template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 覆盖:构建消费曲线分块的 Paint 子图。 */ - template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ - template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); template void prepare_frame(Object* object); template void prepare_partition(Object* object, Plot_Partition_Count index); template void paint_frame(Object* object); - template void after_state_set(Object* object, Member Owner::* member, State_Access states); + template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); + template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; template Sweep_Spectrum::Builder::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} template std::expected, Dependency_Graph_Error> Sweep_Spectrum::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto sweep = std::move(result).value(); static_cast(*sweep->d).bind_sources(scene, frequency_axis, power_axis); - auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(sweep.get()); prepare.template add_state_dependency(sweep.get(), scene); prepare.template add_state_dependency(sweep.get(), frequency_axis); prepare.template add_state_dependency(sweep.get(), frequency_axis); prepare.template add_state_dependency(sweep.get(), power_axis); prepare.template add_state_dependency(sweep.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(sweep.get()); }); + auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(sweep.get()); prepare.template add_prop_dependency(sweep.get(), scene); prepare.template add_prop_dependency(sweep.get(), frequency_axis); prepare.template add_prop_dependency(sweep.get(), frequency_axis); prepare.template add_prop_dependency(sweep.get(), power_axis); prepare.template add_prop_dependency(sweep.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(sweep.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(sweep); } template void Sweep_Spectrum::append_block(const Values& values) { append_block(std::span(std::data(values), std::size(values))); } template -bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const State& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, points); } +bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, points); } template -tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const State& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, points); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; } +tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, points); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; } template -tf::Taskflow Sweep_Spectrum::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("sweep_spectrum.paint.frame"); return graph; } +tf::Taskflow Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("sweep_spectrum.paint.frame"); return graph; } template void Sweep_Spectrum::Private::prepare_frame(Object* object) { - const auto& state = object->template read_state(); const auto& frequency_layout = frequency_axis->template read_state(); const auto& power_layout = power_axis->template read_state(); const auto& power_state = power_axis->template read_state(); prepared = {}; prepared.partitions.resize(graph_partition_count); prepared.canvas = scene->template read_state().viewport; + const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const auto& power_state = power_axis->template read_prop(); prepared = {}; prepared.partitions.resize(graph_partition_count); prepared.canvas = scene->template read_prop().viewport; if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation) return; for (const auto& block : state.blocks) prepared.values.insert(prepared.values.end(), block.begin(), block.end()); if (prepared.values.empty()) return; prepared.marker_first = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation); prepared.marker_second = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.target, frequency_layout.orientation); prepared.valid = true; } template void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_Count index) { - if (!prepared.valid) return; const auto& state = object->template read_state(); const auto& frequency_layout = frequency_axis->template read_state(); const auto& power_layout = power_axis->template read_state(); const auto& frequency_state = frequency_axis->template read_state(); const auto& power_state = power_axis->template read_state(); const auto range = detail::curve_partition_range(prepared.values.size(), index, prepared.partitions.size(), state.frequency_range); + if (!prepared.valid) return; const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& power_layout = power_axis->template read_prop(); const auto& frequency_state = frequency_axis->template read_prop(); const auto& power_state = power_axis->template read_prop(); const auto range = detail::curve_partition_range(prepared.values.size(), index, prepared.partitions.size(), state.frequency_range); prepared.partitions[index] = detail::prepare_curve(std::span(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); } template -void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_state(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); } -template -void Sweep_Spectrum::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); } +template +void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template +void Sweep_Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.stored_block_count = prop.blocks.size(); state.stored_point_count = 0; for (const auto& block : prop.blocks) state.stored_point_count += block.size(); state.rendered_point_count = 0; for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.points.size(); } template const Sweep_Spectrum::Private::Dispatch& Sweep_Spectrum::Private::dispatch_for() { static const Dispatch value{ - [](Root* root, std::span values) { auto* object = static_cast(root); object->template update_state<&State::blocks>([values](State_Access states) { auto& state = states.template get(); state.blocks.emplace_back(values.begin(), values.end()); const std::size_t limit = std::max(1, state.block_count); while (state.blocks.size() > limit) state.blocks.erase(state.blocks.begin()); }); }, - [](const Root* root) { return static_cast(root)->template read_state().blocks.size(); }, - [](const Root* root) { const auto& blocks = static_cast(root)->template read_state().blocks; std::size_t count{}; for (const auto& block : blocks) count += block.size(); return count; }, + [](Root* root, std::span values) { auto* object = static_cast(root); object->template update_prop<&Prop::blocks>([values](Prop_Access props) { auto& state = props.template get(); state.blocks.emplace_back(values.begin(), values.end()); const std::size_t limit = std::max(1, state.block_count); while (state.blocks.size() > limit) state.blocks.erase(state.blocks.begin()); }); }, + [](const Root* root) { return static_cast(root)->template read_prop().blocks.size(); }, + [](const Root* root) { const auto& blocks = static_cast(root)->template read_prop().blocks; std::size_t count{}; for (const auto& block : blocks) count += block.size(); return count; }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); std::size_t count{}; for (const auto& curve : data.prepared.partitions) count += curve.points.size(); return count; } }; return value; } diff --git a/render_2D/render_2D/plottable/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp index 2a7c828..8805282 100644 --- a/render_2D/render_2D/plottable/Waterfall.cpp +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -1,2 +1,3 @@ #include "Waterfall.hpp" -namespace aethera::render_2d { bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { static_cast(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span values) { static_cast(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { append_row(time, std::span(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } +namespace aethera::render_2d { bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default; +bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { static_cast(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span values) { static_cast(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { append_row(time, std::span(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } diff --git a/render_2D/render_2D/plottable/Waterfall.hpp b/render_2D/render_2D/plottable/Waterfall.hpp index db13e95..1c2526a 100644 --- a/render_2D/render_2D/plottable/Waterfall.hpp +++ b/render_2D/render_2D/plottable/Waterfall.hpp @@ -10,12 +10,10 @@ #include #include namespace aethera::render_2d { -struct Waterfall_State_Tag {}; struct Waterfall_Row { Plot_Time_Tick tick{}; std::vector values{}; bool operator==(const Waterfall_Row&) const; }; -struct Waterfall : Def, Tagged_Buffer> { +struct Waterfall : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Time_Object = Impl; - struct Prop : Prev_Prop {}; - struct State : Prev_State, Hover_Tooltip_Properties { + struct Prop : Prev_Prop, Hover_Tooltip_Properties { Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ @@ -25,6 +23,12 @@ struct Waterfall : Def, T Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ Color_Map color_map{}; /* 功率到颜色的映射。 */ std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { + std::size_t row_count{}; /* 当前发布的瀑布行数。 */ + std::size_t stored_point_count{}; /* 当前发布的功率点总数。 */ + std::size_t rendered_cell_count{}; /* 最近一次 Prepare 生成的色块数。 */ bool operator==(const State&) const; }; struct Private; diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp index 79adb51..f0dca8e 100644 --- a/render_2D/render_2D/plottable/Waterfall.ipp +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -36,34 +36,36 @@ struct Waterfall::Private : Prev_Private { void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value); template [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:按当前色块工作量构建分块 Prepare 子图。 */ - template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state); /* CRTP 覆盖:构建消费色块矩阵和提示信息的 Paint 子图。 */ - template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state); /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ - template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state); template void prepare_frame(Object* object); template void prepare_partition(Object* object, Plot_Partition_Count index); template void paint_frame(Object* object); /* CRTP 覆盖:更新 hover 位置并请求重绘。 */ template void handle_event(Object* object, const Event& event); /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ - template void after_state_set(Object* object, Member Owner::* member, State_Access states); + template void after_prop_set(Object* object, Member Owner::* member, Prop_Access states); + template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; template Waterfall::Builder::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {} template -std::expected, Dependency_Graph_Error> Waterfall::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast(*plot->d).bind_sources(scene, frequency_axis, time_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(time_axis); prepare.add(plot.get()); prepare.template add_state_dependency(plot.get(), scene); prepare.template add_state_dependency(plot.get(), frequency_axis); prepare.template add_state_dependency(plot.get(), frequency_axis); prepare.template add_state_dependency(plot.get(), time_axis); prepare.template add_state_dependency(plot.get(), time_axis); paint.add(frequency_axis); paint.add(time_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } +std::expected, Dependency_Graph_Error> Waterfall::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast(*plot->d).bind_sources(scene, frequency_axis, time_axis); auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(time_axis); prepare.add(plot.get()); prepare.template add_prop_dependency(plot.get(), scene); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), time_axis); prepare.template add_prop_dependency(plot.get(), time_axis); paint.add(frequency_axis); paint.add(time_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } template void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { append_row(tick, std::span(std::data(values), std::size(values))); } template void Waterfall::append_row(Time_Of_Day time, const Values& values) { append_row(time, std::span(std::data(values), std::size(values))); } -template bool Waterfall::Private::should_rebuild_prepare_graph(Object*, const State& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); } -template tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const State& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("waterfall.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("waterfall.prepare.partition"); begin.precede(task); } return graph; } -template tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("waterfall.paint.frame"); return graph; } +template bool Waterfall::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); } +template tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("waterfall.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("waterfall.prepare.partition"); begin.precede(task); } return graph; } +template tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("waterfall.paint.frame"); return graph; } template -void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_state(); const auto& frequency_layout = frequency_axis->template read_state(); const auto& time_layout = time_axis->template read_state(); prepared = {}; prepared.canvas = scene->template read_state().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int rows = static_cast(state.rows.size()); const Axis_Range time_range = rows == 1 ? time_axis->coordinate_range() : Axis_Range{static_cast(state.rows.front().tick), static_cast(state.rows.back().tick)}; prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, rows, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; } +void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& time_layout = time_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int rows = static_cast(state.rows.size()); const Axis_Range time_range = rows == 1 ? time_axis->coordinate_range() : Axis_Range{static_cast(state.rows.front().tick), static_cast(state.rows.back().tick)}; prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, rows, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; } template -void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_state(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast(columns) * state.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const std::size_t row_index = cell / static_cast(columns); const int column = static_cast(cell % static_cast(columns)); const auto& values = state.rows[row_index].values; const std::size_t source = static_cast(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, static_cast(row_index))] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } } -template void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_state(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } } +void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast(columns) * state.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const std::size_t row_index = cell / static_cast(columns); const int column = static_cast(cell % static_cast(columns)); const auto& values = state.rows[row_index].values; const std::size_t source = static_cast(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, static_cast(row_index))] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } } +template void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } } template void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty(); } -template void Waterfall::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +template void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.row_count = prop.rows.size(); state.stored_point_count = 0; for (const auto& row : prop.rows) state.stored_point_count += row.values.size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; } template -const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const std::size_t row_limit = static_cast(std::max(2, data.time_axis->template read_state().visible_count)); object->template update_state<&State::rows>([=](State_Access states) { auto& state = states.template get(); state.rows.push_back({tick, {values.begin(), values.end()}}); while (state.rows.size() > row_limit) state.rows.erase(state.rows.begin()); }); }, [](Root* root, Time_Of_Day time, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); data.dispatch->append(root, data.time_axis->append_time(time), values); }, [](const Root* root) { return static_cast(root)->template read_state().rows.size(); }, [](const Root* root) { const auto& rows = static_cast(root)->template read_state().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } +const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const std::size_t row_limit = static_cast(std::max(2, data.time_axis->template read_prop().visible_count)); object->template update_prop<&Prop::rows>([=](Prop_Access props) { auto& state = props.template get(); state.rows.push_back({tick, {values.begin(), values.end()}}); while (state.rows.size() > row_limit) state.rows.erase(state.rows.begin()); }); }, [](Root* root, Time_Of_Day time, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); data.dispatch->append(root, data.time_axis->append_time(time), values); }, [](const Root* root) { return static_cast(root)->template read_prop().rows.size(); }, [](const Root* root) { const auto& rows = static_cast(root)->template read_prop().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } template void Waterfall::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } inline void Waterfall::Private::bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) { scene = scene_value; frequency_axis = frequency_axis_value; time_axis = time_axis_value; } } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 326ad77..c686127 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -1,6 +1,7 @@ #include "Render_Scene_2D.hpp" namespace aethera::render_2d { bool Render_Scene_2D::State::operator==(const State&) const = default; +bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; Render_Frame_Status Render_Scene_2D::render_frame() { return static_cast(*d).dispatch->render_frame(this); diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index a4392fb..eed7868 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -3,7 +3,6 @@ #include "../render/Blend2D_Cache.hpp" #include namespace aethera::render_2d { -struct Render_Scene_2D_State_Tag {}; struct Scene_Color_Cache_Tag {}; enum class Render_Frame_Status : std::uint8_t { rendered, @@ -11,13 +10,15 @@ enum class Render_Frame_Status : std::uint8_t { empty_viewport }; /* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */ -struct Render_Scene_2D : Def, +struct Render_Scene_2D : Def> { - struct Prop : Prev_Prop {}; - struct State : Prev_State { + struct Prop : Prev_Prop { Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */ Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */ bool view_active{}; /* 视图是否接受 render_frame 请求。 */ + bool operator==(const Prop&) const; + }; + struct State : Prev_State { bool operator==(const State&) const; }; /* 完整声明、合成顺序和 CRTP 分派见 Render_Scene_2D.ipp。 */ diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index 0870b6e..ab8da57 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -30,7 +30,7 @@ struct Render_Scene_2D::Private : Prev_Private { template void Render_Scene_2D::Private::process(Object* object, Callback&& callback) requires std::invocable { - const auto& state = static_cast(*static_cast(*this).state.current); + const auto& state = static_cast(*static_cast(*this).current); if (!state.view_active) { const Render_Frame_Status status = Render_Frame_Status::view_inactive; std::invoke(std::forward(callback), status); @@ -92,7 +92,7 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for static_cast(*object->d).dispatch_event(object, event); }, [](Root* root, bool active) { - static_cast(root)->template update_state<&State::view_active>(active); + static_cast(root)->template set<&Prop::view_active>(active); }, [](const Root* root) { const auto* object = static_cast(root); diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index e9e21ec..c49ab8b 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -15,9 +15,9 @@ std::unique_ptr build_axis() { 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->set<&Abs_Axis::Prop::position>(Point_F{10.0, 0.0}); + axis->set<&Abs_Axis::Prop::pixel_length>(200.0); + axis->set<&Numeric_Axis::Prop::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); @@ -30,10 +30,10 @@ TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) { 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->set<&Abs_Axis::Prop::position>(Point_F{0.0, 20.0}); + axis->set<&Abs_Axis::Prop::pixel_length>(100.0); + axis->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical); + axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0}); axis->advance(); EXPECT_DOUBLE_EQ(axis->point_to_coordinate({90.0, 70.0}), 5.0); } @@ -60,9 +60,9 @@ TEST(axis_render, scene_prepares_and_paints_axis_cache) { 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); + axis->set<&Abs_Axis::Prop::position>(Point_F{8.0, 8.0}); + axis->set<&Abs_Axis::Prop::canvas_size>(Size{160, 64}); + axis->set<&Abs_Axis::Prop::pixel_length>(120.0); ASSERT_TRUE((scene->edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(axis.get()); paint.add(axis.get()); @@ -80,9 +80,9 @@ TEST(axis_render, scene_prepares_and_paints_axis_cache) { 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->set<&Abs_Axis::Prop::position>(Point_F{0.0, 0.0}); + axis->set<&Abs_Axis::Prop::pixel_length>(100.0); + axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0}); axis->advance(); Wheel_Event wheel; wheel.position = {50.0, 0.0}; @@ -105,7 +105,7 @@ TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) { 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); + EXPECT_THROW((axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{1.0, 1.0})), std::invalid_argument); axis->advance(); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0})); } diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index 0c27be8..1c9fb54 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -17,10 +17,10 @@ std::unique_ptr build_object(Args&&... args) { template void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) { - axis->template update_state<&Abs_Axis::State::orientation>(orientation); - axis->template update_state<&Abs_Axis::State::position>(position); - axis->template update_state<&Abs_Axis::State::pixel_length>(length); - axis->template update_state<&Abs_Axis::State::canvas_size>(canvas); + axis->template set<&Abs_Axis::Prop::orientation>(orientation); + axis->template set<&Abs_Axis::Prop::position>(position); + axis->template set<&Abs_Axis::Prop::pixel_length>(length); + axis->template set<&Abs_Axis::Prop::canvas_size>(canvas); } } @@ -42,22 +42,22 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) { configure_axis(power.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); configure_axis(time.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas); configure_axis(value.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); - frequency->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); - power->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-100.0, 0.0}); - value->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); + frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); + power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0}); + value->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); auto trace = build_object(scene.get(), time.get(), value.get()); auto sweep = build_object(scene.get(), frequency.get(), power.get()); - trace->update_state<&Frequency_Trace::State::partition_mode>(Plot_Partition_Mode::fixed); - trace->update_state<&Frequency_Trace::State::partition_count>(2u); + trace->set<&Frequency_Trace::Prop::partition_mode>(Plot_Partition_Mode::fixed); + trace->set<&Frequency_Trace::Prop::partition_count>(2u); trace->append_sample(0, 10.0); trace->append_sample(1, 50.0); trace->append_sample(2, 90.0); - sweep->update_state<&Sweep_Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0}); - sweep->update_state<&Sweep_Spectrum::State::partition_mode>(Plot_Partition_Mode::fixed); - sweep->update_state<&Sweep_Spectrum::State::partition_count>(2u); + sweep->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); + sweep->set<&Sweep_Spectrum::Prop::partition_mode>(Plot_Partition_Mode::fixed); + sweep->set<&Sweep_Spectrum::Prop::partition_count>(2u); const std::array block{-90.0, -60.0, -30.0, -10.0}; sweep->append_block(block); - scene->update_state<&Render_Scene_2D::State::viewport>(canvas); + scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(trace->sample_count(), 3u); @@ -82,20 +82,20 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) { configure_axis(frequency.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas); configure_axis(power.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); configure_axis(time.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); - frequency->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); - power->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-100.0, 0.0}); + frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); + power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0}); auto glow = build_object(scene.get(), frequency.get(), power.get()); auto waterfall = build_object(scene.get(), frequency.get(), time.get()); - glow->update_state<&Afterglow::State::frequency_range>(Axis_Range{0.0, 100.0}); - glow->update_state<&Afterglow::State::power_range>(Axis_Range{-100.0, 0.0}); - glow->update_state<&Afterglow::State::power_point_size>(16u); - waterfall->update_state<&Waterfall::State::frequency_range>(Axis_Range{0.0, 100.0}); - waterfall->update_state<&Waterfall::State::power_range>(Axis_Range{-100.0, 0.0}); + glow->set<&Afterglow::Prop::frequency_range>(Axis_Range{0.0, 100.0}); + glow->set<&Afterglow::Prop::power_range>(Axis_Range{-100.0, 0.0}); + glow->set<&Afterglow::Prop::power_point_size>(16u); + waterfall->set<&Waterfall::Prop::frequency_range>(Axis_Range{0.0, 100.0}); + waterfall->set<&Waterfall::Prop::power_range>(Axis_Range{-100.0, 0.0}); const std::array row{-90.0, -60.0, -30.0, -10.0}; glow->append_spectrum(row); waterfall->append_row(0, row); waterfall->append_row(1, row); - scene->update_state<&Render_Scene_2D::State::viewport>(canvas); + scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(glow->history_count(), 1u); @@ -116,17 +116,17 @@ TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) { auto vertical = build_object(); configure_axis(horizontal.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas); configure_axis(vertical.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); - horizontal->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-1.0, 1.0}); - vertical->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-1.0, 1.0}); + horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.0, 1.0}); + vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.0, 1.0}); auto diagram = build_object(scene.get(), horizontal.get(), vertical.get()); auto overlay = build_object(scene.get(), horizontal.get(), vertical.get()); - diagram->update_state<&Constellation_Diagram::State::i_range>(Axis_Range{-1.0, 1.0}); - diagram->update_state<&Constellation_Diagram::State::q_range>(Axis_Range{-1.0, 1.0}); + diagram->set<&Constellation_Diagram::Prop::i_range>(Axis_Range{-1.0, 1.0}); + diagram->set<&Constellation_Diagram::Prop::q_range>(Axis_Range{-1.0, 1.0}); diagram->append_point({0.25, -0.25}); - scene->update_state<&Render_Scene_2D::State::viewport>(canvas); + scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(diagram->point_count(), 1u); - EXPECT_EQ(overlay->read_state().prepare_task_count, 0u); - EXPECT_EQ(overlay->read_state().paint_task_count, 1u); + EXPECT_EQ(overlay->read_state().prepare_task_count, 0u); + EXPECT_EQ(overlay->read_state().paint_task_count, 1u); } diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index dfddd38..be1982e 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -32,7 +32,7 @@ TEST(spectrum_data, publishes_samples_and_interpolates_power) { const auto missing = spectrum->power_at(25.0); ASSERT_FALSE(missing.has_value()); EXPECT_EQ(missing.error(), Spectrum::Power_At_Result::no_samples); - spectrum->update_state<&Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0}); + spectrum->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); const double samples[]{-100.0, -50.0, 0.0}; spectrum->update_samples(samples); spectrum->advance(); @@ -84,24 +84,24 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { auto power = build_object(); auto spectrum = build_object(scene.get(), frequency.get(), power.get()); const Size canvas{160, 120}; - frequency->update_state<&Abs_Axis::State::position>(Point_F{20.0, 100.0}); - frequency->update_state<&Abs_Axis::State::canvas_size>(canvas); - frequency->update_state<&Abs_Axis::State::pixel_length>(120.0); - frequency->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); - power->update_state<&Abs_Axis::State::position>(Point_F{20.0, 100.0}); - power->update_state<&Abs_Axis::State::canvas_size>(canvas); - power->update_state<&Abs_Axis::State::pixel_length>(-80.0); - power->update_state<&Abs_Axis::State::orientation>(Axis_Orientation::vertical); - power->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-100.0, 0.0}); - spectrum->update_state<&Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0}); - spectrum->update_state<&Spectrum::State::partition_mode>(Spectrum_Partition_Mode::fixed); - spectrum->update_state<&Spectrum::State::partition_count>(3u); - spectrum->update_state<&Spectrum::State::sweep_region_visible>(true); - spectrum->update_state<&Spectrum::State::max_hold_visible>(true); + frequency->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0}); + frequency->set<&Abs_Axis::Prop::canvas_size>(canvas); + frequency->set<&Abs_Axis::Prop::pixel_length>(120.0); + frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); + power->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0}); + power->set<&Abs_Axis::Prop::canvas_size>(canvas); + power->set<&Abs_Axis::Prop::pixel_length>(-80.0); + power->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical); + power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0}); + spectrum->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); + spectrum->set<&Spectrum::Prop::partition_mode>(Spectrum_Partition_Mode::fixed); + spectrum->set<&Spectrum::Prop::partition_count>(3u); + spectrum->set<&Spectrum::Prop::sweep_region_visible>(true); + spectrum->set<&Spectrum::Prop::max_hold_visible>(true); const double samples[]{-90.0, -65.0, -20.0, -45.0, -75.0}; spectrum->update_samples(samples); - scene->update_state<&Render_Scene_2D::State::viewport>(canvas); - scene->update_state<&Render_Scene_2D::State::background>(Color::transparent()); + scene->set<&Render_Scene_2D::Prop::viewport>(canvas); + scene->set<&Render_Scene_2D::Prop::background>(Color::transparent()); scene->activate_view(); const auto prepare_graph = scene->pending_dependency_graph(); EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), scene.get())); @@ -109,23 +109,23 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), power.get())); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_GT(spectrum->rendered_point_count(), 0u); - const auto& render_state = spectrum->read_state(); + const auto& render_state = spectrum->read_state(); EXPECT_EQ(render_state.prepare_task_count, 4u); EXPECT_EQ(render_state.paint_task_count, 1u); const Image_View frame = scene->frame_view(); ASSERT_FALSE(frame.empty()); EXPECT_TRUE(contains_color(frame)); - spectrum->update_state<&Spectrum::State::partition_count>(2u); + spectrum->set<&Spectrum::Prop::partition_count>(2u); spectrum->update_samples(samples); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); - const auto& rebuilt_state = spectrum->read_state(); + const auto& rebuilt_state = spectrum->read_state(); EXPECT_TRUE(rebuilt_state.prepare_graph_rebuilt); EXPECT_EQ(rebuilt_state.prepare_task_count, 3u); EXPECT_TRUE(contains_color(scene->frame_view())); const Size resized_canvas{200, 140}; - scene->update_state<&Render_Scene_2D::State::viewport>(resized_canvas); - frequency->update_state<&Abs_Axis::State::canvas_size>(resized_canvas); - power->update_state<&Abs_Axis::State::canvas_size>(resized_canvas); + scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas); + frequency->set<&Abs_Axis::Prop::canvas_size>(resized_canvas); + power->set<&Abs_Axis::Prop::canvas_size>(resized_canvas); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); const Image_View resized_frame = scene->frame_view(); EXPECT_EQ(resized_frame.width, resized_canvas.width);