diff --git a/.gitignore b/.gitignore index 987cdc0..1d77899 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ /render_3D/old/ /webapp_gallery/node_modules/ /webapp_gallery/dist/ +/third_party/Adminive/CMakeFiles/ +/third_party/datoviz/ +/third_party/ diff --git a/Project_detail_specification.md b/Project_detail_specification.md new file mode 100644 index 0000000..adbfc88 --- /dev/null +++ b/Project_detail_specification.md @@ -0,0 +1,22 @@ +# 项目细节规范 + +## 代码组织 + +* `.hpp` 只放业务契约和必要前置声明,不允许出现函数体;构造函数、薄壳函数及模板函数也只能声明。模板定义统一放对应 `.ipp`,非模板定义放 `.cpp` 或仅供头文件实例化的 `.ipp`。 +* 嵌套 `Private` 在 `.hpp` 只写 `struct Private;`,完整定义、内部字段和 CRTP 定制点放在对应 `.ipp`。 +* 派生 `Private` 必须继承 `Prev_Private`。可覆盖或必需的 CRTP 能力必须在 `Private` 定义处写清用途、参数、返回值、默认行为、调用时机和编译期选择优先级。 +* 状态处理、算法和内部协作都实现在 `Private`;只有外部消费者需要的业务能力才在原始类声明薄壳,并通过 `d` 调用最终 `Private`。 +* 替换实现时删除旧实现,不保留重复定义、兼容别名或转发层。 + +## 状态与接口 + +* 每个状态只能有一个权威来源。双缓冲交换后的当前结构就是稳定读面,跨对象直接读取该结构;禁止为无锁访问再复制一份快照、View、镜像字段或同步缓存。 +* `Root` 只保存一个最终 `Private` 指针;`Builder::build()` 校验成功后创建并挂接完整 Private,`Root` 通过公共 Private 基类的虚析构统一释放。禁止直接公开该指针。 +* 能从权威结构查询或计算的数据即时获取,不保存为成员。类只保存自身职责需要且无法推导的状态,并检查每个新增成员的读写者和生命周期。 +* 公共接口只表达业务语义,不暴露 `Private`、内部指针、线程状态或缓冲区角色;接口保持正交,不增加空配置、未完成接口、无消费者统计或只做转发的 getter/setter。 + +## 注释与修改 + +* 普通字段使用对齐的同行 `/* ... */` 注释,写清含义、单位、有效条件和生命周期;函数、类型及 CRTP 契约使用声明前注释。 +* 审计和重命名应一次完成结构、引用、测试及文档闭环。任何 fallback 必须先写明触发条件、影响范围和验证方式,再实施。 +* 修改后完整编译相关目标;需要运行程序时按 `AGENTS.md` 通过 CDB 执行。 diff --git a/Project_naming_conventions.md b/Project_naming_conventions.md index 242a639..0b52daa 100644 --- a/Project_naming_conventions.md +++ b/Project_naming_conventions.md @@ -71,6 +71,7 @@ std::uint64_t execution_time_ns{}; /* 本次执行耗时,单位为纳秒 ## Private 实现放置 +* `.hpp` 中所有函数只能声明,禁止编写函数体;该限制同样适用于构造函数、薄壳函数和模板函数。模板定义放对应 `.ipp`,非模板定义放 `.cpp` 或仅供头文件实例化的 `.ipp`。 * 对外 `.hpp` 中只前置声明嵌套的 `struct Private;`,不得展开其字段、内部派发表、线程状态、缓冲角色或实现函数。 * `Owner::Private` 的完整声明放在对应 `.ipp` 中;模板实现继续放在 `.ipp`,非模板实现可放在 `.cpp`。 * 派生方可覆盖的 CRTP 函数和能力契约,必须在 `.ipp` 的 `Private` 完整声明处逐项注释;派生 `Private` 必须继承 `Prev_Private`。 diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp index cffbc53..1130935 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp @@ -168,6 +168,18 @@ public: bind_node_data ); } + /* 添加状态层级依赖;该 Tag 层任一字段通过 update_state 修改都会使目标阶段 dirty。 */ + template Target, detail::State_Layer_Dependency_Source Source> + Node* add_state_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::Buffer_Dependency_Source Source> Node* add_dependency(Target* target, Source* source) { return edit_dependency_graph->template add_dependency_runtime( @@ -194,6 +206,16 @@ public: bool remove_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) { + return edit_dependency_graph->remove_edge( + target, + source, + detail::dependency_id>(), + target_tag + ); + } template Target, detail::Buffer_Dependency_Source Source> bool remove_dependency(Target* target, Source* source) { return edit_dependency_graph->remove_edge(target, source, detail::dependency_id>(), target_tag); diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp index d09885e..4a10d75 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp @@ -294,6 +294,15 @@ struct Root::Builder { } ); } + /* 在构造期依赖图中添加指定 State Tag 的状态层级依赖。 */ + template Dependency_Graph_Tag, typename State_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::State_Layer_Dependency_Source Source> + Builder& add_state_dependency(Target* target, Source* source) { + return edit_dependency_graph( + [&](auto& editor) { + editor.template add_state_dependency(target, source); + } + ); + } template Dependency_Graph_Tag, typename Buffer_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Buffer_Dependency_Source Source> Builder& add_dependency(Target* target, Source* source) { return edit_dependency_graph( @@ -318,6 +327,15 @@ struct Root::Builder { } ); } + /* 从构造期依赖图中移除指定 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) { + return edit_dependency_graph( + [&](auto& editor) { + editor.template remove_state_dependency(target, source); + } + ); + } template Dependency_Graph_Tag, typename Buffer_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Buffer_Dependency_Source Source> Builder& remove_dependency(Target* target, Source* source) { return edit_dependency_graph( @@ -334,10 +352,8 @@ struct Root::Builder { buffer_storage.commit(private_data->buffer_storage); dependency_graph_storage.commit(private_data->dependency_graph_storage); object->d = private_data.release(); - // d 已指向最终 Private 后,定义层可按最终 Object 类型安装公开薄壳所需的 CRTP 分派。 - if constexpr (requires { object->bind_private_crtp(object.get()); }) { - object->bind_private_crtp(object.get()); - } + // d 已指向最终 Private 后,由 Private 继承链按最终 Object 类型安装公开薄壳所需的 CRTP 分派。 + static_cast(*object->d).bind_private_crtp(object.get()); return std::move(object); } std::expected validate() const { diff --git a/kernel/src/kernel/double_buffer/mechanism.hpp b/kernel/src/kernel/double_buffer/mechanism.hpp index 6aa673d..c7762d3 100644 --- a/kernel/src/kernel/double_buffer/mechanism.hpp +++ b/kernel/src/kernel/double_buffer/mechanism.hpp @@ -248,6 +248,8 @@ concept State_Member = requires { typename Member_Pointer_Traits::Owner_Type >; }; +template +using State_Member_Tag = typename Member_Pointer_Traits::Owner_Type::Tag_Type; template concept State_Member_Settable = requires(State& state, Value&& value) { requires State_Member; @@ -524,6 +526,8 @@ namespace detail { template struct State_Dependency_Key {}; template +struct State_Layer_Dependency_Key {}; +template struct Buffer_Dependency_Key {}; template struct Dirty_Dependency_Key {}; @@ -564,6 +568,9 @@ struct Root { /* 所有实现层 Private 的公共析构基类;Root 通过该类型唯一拥有最终 Private。 */ struct Private { virtual ~Private() = default; + /* CRTP 默认:Builder 挂接最终 Private 后按基类到派生类绑定最终对象类型;Root 层不处理。 */ + template + void bind_private_crtp(Object*) {} }; using Buffers = std::tuple<>; using Dependency_Graph_Types = std::tuple<>; @@ -678,6 +685,11 @@ concept State_Dependency_Source = requires { requires State_Member; }; template +concept State_Layer_Dependency_Source = requires { + requires Dependency_Object; + requires State_Tag_In; +}; +template concept Buffer_Dependency_Source = requires { requires Dependency_Object; requires Buffer_Tag_In; diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 06a01be..16ab355 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -77,6 +77,11 @@ struct Def : Base { using Dependency_Graph_Types = detail::Impl_Dependency_Graph_Types; using States = detail::Impl_States; struct Private : Base_Private { + /* CRTP 默认:继续调用上一 Private 层的最终对象绑定;派生 Private 覆盖时必须先调用此实现。 */ + template + void bind_private_crtp(Object* object) { + Base_Private::bind_private_crtp(object); + } /* CRTP 可覆盖:写入 State 成员前按基类到派生类顺序调用;pending_states.get() 返回对应可写状态层。 */ template void before_state_set(Self* object, Member Owner::* member, State_Access pending_states) {} @@ -167,6 +172,13 @@ private: }; walk_private(callback); } + /* 同时发布字段级键和字段所属 State Tag 的状态层级键。 */ + template + void emit_state_dependencies() { + this->emit_dependency_source(detail::dependency_id>()); + using State_Tag = detail::State_Member_Tag; + this->emit_dependency_source(detail::dependency_id>()); + } void before_advance() { State_Access pending_states{*data().state.pending}; State_Access current_states{std::as_const(*data().state.current)}; @@ -277,18 +289,24 @@ public: using Layer = detail::State_Value; std::invoke(std::forward(callback), static_cast(*data().state.current)); } + // 按 Tag 直接读取双缓冲已提交状态层;调用方不得保存引用跨越任一相关对象的下一次 advance/process。 + template Tag> + [[nodiscard]] const auto& read_state() const noexcept { + using Layer = detail::State_Value; + return static_cast(*data().state.current); + } template Tag> void notify_state() { data().state_callbacks.template notify(*data().state.current); } - // State 写入 pending,随后向依赖图发出对应成员的 dirty 信号;真正进入 current 发生在 advance/process 边界。 + // State 写入 pending,随后同时发出成员级与状态层级依赖信号;真正进入 current 发生在 advance/process 边界。 template Value> void update_state(Value&& value) { std::lock_guard guard(lock); before_state_set(Member); data().state.pending->*Member = std::forward(value); after_state_set(Member); - this->emit_dependency_source(detail::dependency_id>()); + emit_state_dependencies(); } // 在同一锁作用域内按 Tag 原子修改一组相关 State 成员,并逐项触发生命周期钩子和依赖通知。 template requires @@ -300,7 +318,7 @@ public: (before_state_set(Members), ...); std::invoke(std::forward(callback), State_Access{*data().state.pending}); (after_state_set(Members), ...); - (this->emit_dependency_source(detail::dependency_id>()), ...); + (emit_state_dependencies(), ...); } template Callback> void process(Callback&& callback) { diff --git a/kernel/src/kernel/render_common.hpp b/kernel/src/kernel/render_common.hpp index e23718a..0a039d4 100644 --- a/kernel/src/kernel/render_common.hpp +++ b/kernel/src/kernel/render_common.hpp @@ -22,6 +22,7 @@ using double_buffer::Tagged_Buffer; using double_buffer::Attached; using double_buffer::Dependency_Graph_Type; using double_buffer::Dependency_Graph; +using double_buffer::Dependency_Graph_Error; /* Prepare 数据阶段在 Dependency_Graph 图中的标签。 */ struct Prepare_Data_Tag {}; /* Paint 阶段在 Dependency_Graph 图中的标签。 */ diff --git a/kernel/src/kernel/renderable.hpp b/kernel/src/kernel/renderable.hpp index 687015c..80dc5fc 100644 --- a/kernel/src/kernel/renderable.hpp +++ b/kernel/src/kernel/renderable.hpp @@ -31,6 +31,9 @@ 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; }; +/* 最终 Private 声明 No_Prepare 时,该 Renderable 不参与 Prepare 阶段。 */ +template +concept No_Prepare_Renderable = Attached && requires { typename T::Private::No_Prepare; }; /* * Paint 数据模式定制点。 * 最终对象的 Private 提供 void paint(T* object) 即满足;若同时存在 build_paint_graph(...),子图模式优先。 @@ -58,7 +61,7 @@ concept Renderable_Object = Attached && std::derived_from && r { 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; -} && (Prepare_Data_Renderable || Prepare_Graph_Renderable) && (Paint_Data_Renderable || Paint_Graph_Renderable); +} && (No_Prepare_Renderable || Prepare_Data_Renderable || Prepare_Graph_Renderable) && (Paint_Data_Renderable || Paint_Graph_Renderable); /* * Renderable 定义 Scene 可调度对象的公共机制。 * 用户继续派生该定义,在派生类型的 Private 中提供 Prepare/Paint 定制点,并最终使用 Impl 创建可运行实例。 @@ -85,10 +88,6 @@ struct Renderable : Def> { struct Private; /* 将事件交给最终 Private 的可选 handle_event(...) 能力。 */ void dispatch_event(const Event& event); -protected: - /* Builder 挂接最终 Private 后,为事件公开薄壳绑定最终对象类型。 */ - template - void bind_private_crtp(Object* object); private: /* 数据模式的内部调度入口:执行最终对象 prepare_data(...),再触发各 CRTP 层 after_prepare_data(...)。 */ template diff --git a/kernel/src/kernel/renderable.ipp b/kernel/src/kernel/renderable.ipp index fb48f46..bc5a949 100644 --- a/kernel/src/kernel/renderable.ipp +++ b/kernel/src/kernel/renderable.ipp @@ -49,6 +49,9 @@ struct Renderable::Private : Prev_Private { bool should_paint(Attached auto* object, const State& state, bool dirty); /* CRTP 可覆盖:prepare_data(...) 完成后按派生类到基类顺序调用;object 为最终对象;默认不处理。 */ void after_prepare_data(Attached auto* object); + /* CRTP 覆盖:Builder 挂接最终 Private 后绑定事件、颜色缓存和阶段分派;派生 Private 必须先调用此实现。 */ + template + void bind_private_crtp(Object* object); /* Def::Private 的四个 State 生命周期 hook 同样可在派生 Private 中覆盖,并通过 State_Access::get() 访问状态层。 */ }; template @@ -74,8 +77,9 @@ inline bool Renderable::Private::should_paint(Attached auto*, const State&, bool } inline void Renderable::Private::after_prepare_data(Attached auto*) {} template -void Renderable::bind_private_crtp(Object* object) { - auto& data = static_cast(*object->d); +void Renderable::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + auto& data = static_cast(*this); if constexpr (Event_Renderable) { data.event_run = [](Root* root, const Event& event) { auto* value = static_cast(root); @@ -107,6 +111,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { [](Root* root, bool dirty) { auto* value = static_cast(root); auto& private_data = static_cast(*value->d); + if constexpr (No_Prepare_Renderable) return false; return private_data.should_prepare(value, *private_data.state.current, dirty); }, [](Root* root) { @@ -115,7 +120,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { return private_data.should_rebuild_prepare_graph(value, *private_data.state.current); }, []() -> Private::Stage_Run { - if constexpr (Prepare_Data_Renderable) { + if constexpr (Prepare_Data_Renderable && !No_Prepare_Renderable) { return [](Root* root) { auto* value = static_cast(root); run_prepare_data(value); diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index f356e09..f08355b 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -108,7 +108,7 @@ void Scene::Private::after_advance(Object* object, state.prepare_task_count = data->prepare_graph->num_tasks(); } else { - state.prepare_task_count = 1; + state.prepare_task_count = dispatch->prepare.run ? 1 : 0; } bool dirty = root->template dirty(); state.prepare_dirty = dirty; @@ -122,7 +122,7 @@ void Scene::Private::after_advance(Object* object, } else { prepare_run = taskflow.emplace([dispatch, root] { - dispatch->prepare.run(root); + if (dispatch->prepare.run) dispatch->prepare.run(root); }).name("renderable.prepare.data"); } auto prepare_done = taskflow.emplace([dispatch, root] { diff --git a/kernel/src/test/Dependency_Graph_Test.cpp b/kernel/src/test/Dependency_Graph_Test.cpp index 9d28445..e358af1 100644 --- a/kernel/src/test/Dependency_Graph_Test.cpp +++ b/kernel/src/test/Dependency_Graph_Test.cpp @@ -7,7 +7,8 @@ struct Graph_Tag {}; struct Node_Object : double_buffer::Def> { struct Prop : Prev_Prop {}; struct State : Prev_State { - int value{}; + int value{}; /* 字段级依赖测试使用的状态值。 */ + int other{}; /* 状态层级依赖测试使用的同层其他值。 */ bool operator==(const State&) const = default; }; struct Private : Prev_Private {}; @@ -34,6 +35,7 @@ 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); } TEST(dependency_graph_storage, edited_graph_commits_and_stays_synchronized) { auto first = build_object(); @@ -96,6 +98,30 @@ TEST(dependency_graph, topological_order_and_state_dirty_propagation) { source->update_state<&Node_Object::State::value>(31); EXPECT_TRUE(target->dirty()); } +TEST(dependency_graph, state_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_dependency<&Node_Object::State::value>(member_target.get(), member_source.get()); + editor.template add_state_dependency(layer_target.get(), layer_source.get()); + } + ).has_value()); + graph->advance(); + member_source->update_state<&Node_Object::State::other>(1); + EXPECT_FALSE(member_target->dirty()); + member_source->update_state<&Node_Object::State::value>(2); + EXPECT_TRUE(member_target->dirty()); + layer_source->update_state<&Node_Object::State::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 ad6f7ff..2c79d24 100644 --- a/kernel/src/test/object_test.cpp +++ b/kernel/src/test/object_test.cpp @@ -113,6 +113,10 @@ TEST(state_tag, callback_publishes_only_requested_layer) { static_assert(requires(Object& object) { object.template access_state([](const auto&) {}); }); +static_assert(std::same_as< + decltype(std::declval().template read_state()), + const Test_Object::State& +>); struct Missing_State_Tag {}; static_assert(!double_buffer::detail::State_Tag_In); TEST(state_tag, inherited_tags_remain_independently_addressable) { @@ -128,6 +132,14 @@ TEST(state_tag, inherited_tags_remain_independently_addressable) { EXPECT_EQ(base_calls, 1); EXPECT_EQ(derived_calls, 1); } +TEST(state_tag, committed_layer_is_directly_readable_by_tag) { + auto object = build_object(); + 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); +} static_assert(double_buffer::detail::State_Tag_In); static_assert(double_buffer::detail::State_Tag_In); static_assert(double_buffer::detail::State_Tag_In); diff --git a/render_2D/render_2D/axis/Abs_Axis.cpp b/render_2D/render_2D/axis/Abs_Axis.cpp index a60e47b..1aa4202 100644 --- a/render_2D/render_2D/axis/Abs_Axis.cpp +++ b/render_2D/render_2D/axis/Abs_Axis.cpp @@ -4,6 +4,8 @@ #include #include namespace aethera::render_2d { +bool Abs_Axis::State::operator==(const State&) 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 97d557a..e11e5b7 100644 --- a/render_2D/render_2D/axis/Abs_Axis.hpp +++ b/render_2D/render_2D/axis/Abs_Axis.hpp @@ -13,12 +13,12 @@ concept Axis_Object = Renderable_Object && std::derived_from && const typename T::Private& private_data, const T* object, Axis_Range coordinate_range, - double tick + Axis_Coordinate tick ) { { private_data.coordinate_range(object) } -> std::same_as; - { private_data.tick_step(object, coordinate_range) } -> std::same_as; + { private_data.tick_step(object, coordinate_range) } -> std::same_as; { private_data.tick_label(object, tick) } -> std::same_as; - { private_data.sub_tick_count(object, tick) } -> std::same_as; + { private_data.sub_tick_count(object, tick) } -> std::same_as; }; /* 所有二维坐标轴共享的定义层;最终通过 Impl 创建运行时对象。 */ struct Abs_Axis : Def, @@ -29,40 +29,36 @@ struct Abs_Axis : Def, struct State : Prev_State { Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */ Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */ - double pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */ + Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */ Axis_Orientation orientation{Axis_Orientation::horizontal}; /* 从二维点取轴向分量时使用的方向。 */ - double tick_length{10.0}; /* 主刻度线长度,单位为像素。 */ - double sub_tick_length{5.0}; /* 次刻度线长度,单位为像素。 */ + Axis_Tick_Length tick_length{10.0}; /* 主刻度线长度,单位为像素。 */ + Axis_Tick_Length sub_tick_length{5.0}; /* 次刻度线长度,单位为像素。 */ Pen axis_pen{Color::white()}; /* 轴线、主刻度和次刻度的描边样式。 */ std::string unit_text{}; /* 坐标轴末端显示的单位文本;空值表示不绘制。 */ Font unit_text_font{}; /* 刻度标签和单位文本使用的字体。 */ Pen unit_text_pen{Color::white()}; /* 刻度标签和单位文本使用的前景样式。 */ Brush unit_text_background_brush{}; /* 单位文本背景填充;none 表示不填充。 */ - double label_rotation_degrees{}; /* 刻度标签顺时针旋转角度,单位为度。 */ - bool operator==(const State&) const = default; + Axis_Label_Rotation label_rotation_degrees{}; /* 刻度标签顺时针旋转角度,单位为度。 */ + bool operator==(const State&) const; }; /* 完整声明及派生轴 CRTP 能力契约见 Abs_Axis.ipp。 */ struct Private; /* 返回由最终轴 State 计算得到的当前有向坐标区间。 */ [[nodiscard]] Axis_Range coordinate_range() const; /* 将坐标值映射到当前轴向像素位置。 */ - [[nodiscard]] double coordinate_to_pixel(double coordinate) const; + [[nodiscard]] Axis_Pixel_Position coordinate_to_pixel(Axis_Coordinate coordinate) const; /* 将当前轴向像素位置反算为坐标值。 */ - [[nodiscard]] double pixel_to_coordinate(double pixel) const; + [[nodiscard]] Axis_Coordinate pixel_to_coordinate(Axis_Pixel_Position pixel) const; /* 按轴方向从二维像素点取值并反算为坐标。 */ - [[nodiscard]] double point_to_coordinate(Point_F point) const; + [[nodiscard]] Axis_Coordinate point_to_coordinate(Point_F point) const; /* 返回给定坐标区间覆盖的整数像素样本数量。 */ - [[nodiscard]] int pixel_sample_count(Axis_Range coordinate_range) const; + [[nodiscard]] Axis_Pixel_Sample_Count pixel_sample_count(Axis_Range coordinate_range) const; /* 返回给定坐标区间使用的主刻度步长。 */ - [[nodiscard]] double tick_step(Axis_Range coordinate_range) const; + [[nodiscard]] Axis_Coordinate tick_step(Axis_Range coordinate_range) const; /* 返回指定主刻度的显示文本。 */ - [[nodiscard]] std::string tick_label(double tick) const; + [[nodiscard]] std::string tick_label(Axis_Coordinate tick) const; /* 返回相邻主刻度之间的次刻度数量。 */ - [[nodiscard]] int sub_tick_count(double major_step) const; -protected: - /* Builder 挂接最终 Private 后,为公开薄壳绑定最终轴类型的无虚函数分派。 */ - template - void bind_private_crtp(Object* object); + [[nodiscard]] Axis_Tick_Count sub_tick_count(Axis_Coordinate major_step) const; }; } #include "Abs_Axis.ipp" diff --git a/render_2D/render_2D/axis/Abs_Axis.ipp b/render_2D/render_2D/axis/Abs_Axis.ipp index 871aeef..666bef9 100644 --- a/render_2D/render_2D/axis/Abs_Axis.ipp +++ b/render_2D/render_2D/axis/Abs_Axis.ipp @@ -66,6 +66,9 @@ struct Abs_Axis::Private : Prev_Private { void paint(Attached auto* object); /* CRTP 默认:每两个主刻度之间生成 4 个次刻度。 */ [[nodiscard]] int sub_tick_count(const Attached auto* object, double major_step) const; + /* CRTP 覆盖:绑定 Renderable 机制和最终轴公开薄壳分派;派生 Private 必须先调用此实现。 */ + template + void bind_private_crtp(Object* object); }; template const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { @@ -141,9 +144,9 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { return result; } template -void Abs_Axis::bind_private_crtp(Object* object) { - Renderable::bind_private_crtp(object); - static_cast(*object->d).dispatch = &Private::dispatch_for(); +void Abs_Axis::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + dispatch = &Private::dispatch_for(); } inline void Abs_Axis::Private::prepare_data(Attached auto* object) { using Object = std::remove_pointer_t; diff --git a/render_2D/render_2D/axis/Axis_Types.cpp b/render_2D/render_2D/axis/Axis_Types.cpp new file mode 100644 index 0000000..baecae4 --- /dev/null +++ b/render_2D/render_2D/axis/Axis_Types.cpp @@ -0,0 +1,22 @@ +#include "Axis_Types.hpp" + +namespace aethera::render_2d { +Axis_Coordinate Axis_Range::size() const noexcept { + return std::abs(length()); +} + +Axis_Coordinate Axis_Range::length() const noexcept { + return target - origin; +} + +Axis_Coordinate Axis_Range::center() const noexcept { + return origin + length() * 0.5; +} + +bool Axis_Range::contains(Axis_Coordinate coordinate) const noexcept { + const auto [low, high] = std::minmax(origin, target); + return coordinate >= low - 1e-9 && coordinate <= high + 1e-9; +} + +bool Axis_Range::operator==(const Axis_Range&) const = default; +} diff --git a/render_2D/render_2D/axis/Axis_Types.hpp b/render_2D/render_2D/axis/Axis_Types.hpp index 7212c3a..4fd3d4e 100644 --- a/render_2D/render_2D/axis/Axis_Types.hpp +++ b/render_2D/render_2D/axis/Axis_Types.hpp @@ -4,6 +4,16 @@ #include #include namespace aethera::render_2d { +using Axis_Coordinate = double; +using Axis_Pixel_Position = double; +using Axis_Pixel_Length = double; +using Axis_Tick_Length = double; +using Axis_Label_Rotation = double; +using Axis_Tick_Count = int; +using Axis_Pixel_Sample_Count = int; +using Axis_Label_Precision = int; +using Axis_Time_Tick = int; +using Axis_Visible_Count = int; /* 坐标轴在二维画布中的方向。 */ enum class Axis_Orientation : std::uint8_t { horizontal, @@ -11,25 +21,16 @@ enum class Axis_Orientation : std::uint8_t { }; /* 坐标轴上的有向数值区间;origin 到 target 的顺序决定坐标增长方向。 */ struct Axis_Range { - double origin{}; /* 区间起点坐标。 */ - double target{}; /* 区间终点坐标;可小于 origin 以表达反向坐标轴。 */ + Axis_Coordinate origin{}; /* 区间起点坐标。 */ + Axis_Coordinate target{}; /* 区间终点坐标;可小于 origin 以表达反向坐标轴。 */ /* 返回不带方向的区间跨度。 */ - [[nodiscard]] double size() const noexcept { - return std::abs(length()); - } + [[nodiscard]] Axis_Coordinate size() const noexcept; /* 返回保留方向的区间长度。 */ - [[nodiscard]] double length() const noexcept { - return target - origin; - } + [[nodiscard]] Axis_Coordinate length() const noexcept; /* 返回区间中点。 */ - [[nodiscard]] double center() const noexcept { - return origin + length() * 0.5; - } + [[nodiscard]] Axis_Coordinate center() const noexcept; /* 判断坐标是否落在区间内;正向和反向区间使用相同边界语义。 */ - [[nodiscard]] bool contains(double coordinate) const noexcept { - const auto [low, high] = std::minmax(origin, target); - return coordinate >= low - 1e-9 && coordinate <= high + 1e-9; - } - bool operator==(const Axis_Range&) const = default; + [[nodiscard]] bool contains(Axis_Coordinate coordinate) const noexcept; + bool operator==(const Axis_Range&) const; }; } diff --git a/render_2D/render_2D/axis/Frequency_Axis.cpp b/render_2D/render_2D/axis/Frequency_Axis.cpp new file mode 100644 index 0000000..802569f --- /dev/null +++ b/render_2D/render_2D/axis/Frequency_Axis.cpp @@ -0,0 +1,5 @@ +#include "Frequency_Axis.hpp" + +namespace aethera::render_2d { +bool Frequency_Axis::State::operator==(const State&) const = default; +} diff --git a/render_2D/render_2D/axis/Frequency_Axis.hpp b/render_2D/render_2D/axis/Frequency_Axis.hpp index 7f2fcd7..b3843d1 100644 --- a/render_2D/render_2D/axis/Frequency_Axis.hpp +++ b/render_2D/render_2D/axis/Frequency_Axis.hpp @@ -9,7 +9,7 @@ struct Frequency_Axis : Def { - bool operator==(const State&) const = default; + bool operator==(const State&) const; }; /* 完整声明及频率标签 CRTP 覆盖见 Frequency_Axis.ipp。 */ struct Private; diff --git a/render_2D/render_2D/axis/Numeric_Axis.cpp b/render_2D/render_2D/axis/Numeric_Axis.cpp new file mode 100644 index 0000000..2b82141 --- /dev/null +++ b/render_2D/render_2D/axis/Numeric_Axis.cpp @@ -0,0 +1,5 @@ +#include "Numeric_Axis.hpp" + +namespace aethera::render_2d { +bool Numeric_Axis::State::operator==(const State&) const = default; +} diff --git a/render_2D/render_2D/axis/Numeric_Axis.hpp b/render_2D/render_2D/axis/Numeric_Axis.hpp index 3346a81..b29e3ad 100644 --- a/render_2D/render_2D/axis/Numeric_Axis.hpp +++ b/render_2D/render_2D/axis/Numeric_Axis.hpp @@ -10,11 +10,11 @@ struct Numeric_Axis : Def { Axis_Range coordinate_range{0.0, 20.0}; /* 当前有向数值区间;必须有限且非零。 */ - int precision{2}; /* 标签最大小数位数;格式化时限制到 0..12。 */ + Axis_Label_Precision precision{2}; /* 标签最大小数位数;格式化时限制到 0..12。 */ Number_Locale locale{}; /* 数值标签的小数点规则。 */ bool wheel_enabled{true}; /* 是否允许滚轮以指针位置为锚点缩放坐标范围。 */ bool drag_enabled{true}; /* 是否允许按住鼠标左键拖动坐标范围。 */ - bool operator==(const State&) const = default; + bool operator==(const State&) const; }; /* 完整声明及数值轴 CRTP 能力见 Numeric_Axis.ipp。 */ struct Private; diff --git a/render_2D/render_2D/axis/Time_Axis.cpp b/render_2D/render_2D/axis/Time_Axis.cpp index de1696d..d53213d 100644 --- a/render_2D/render_2D/axis/Time_Axis.cpp +++ b/render_2D/render_2D/axis/Time_Axis.cpp @@ -2,6 +2,8 @@ #include #include namespace aethera::render_2d { +bool Time_Axis::State::operator==(const State&) 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 cf6cbff..46b328e 100644 --- a/render_2D/render_2D/axis/Time_Axis.hpp +++ b/render_2D/render_2D/axis/Time_Axis.hpp @@ -13,27 +13,23 @@ struct Time_Axis : Def> { struct Prop : Prev_Prop {}; /* 时间轴样本与显示参数的唯一权威状态。 */ struct State : Prev_State { - int visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */ - double tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */ - double estimated_label_width_px{48.0}; /* 当前字体下单个时间标签的估算像素宽度;小于 48 时按 48 计算。 */ + 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 时最新样本位于坐标区间起点。 */ - int next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */ - std::deque> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */ - bool operator==(const State&) const = default; + Axis_Time_Tick next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */ + std::deque> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */ + bool operator==(const State&) const; }; /* 完整声明、时间轴 CRTP 能力及公开薄壳分派见 Time_Axis.ipp。 */ struct Private; /* 返回当前已提交时间样本数量。 */ [[nodiscard]] std::size_t time_point_count() const; /* 向提交侧追加时间并返回分配的 tick;结果在下一次 advance 后进入当前读面。 */ - int append_time(Time_Of_Day time); + Axis_Time_Tick append_time(Time_Of_Day time); /* 在当前已提交样本中查找 tick;不存在时返回无效时间。 */ - [[nodiscard]] Time_Of_Day tick_to_time(int tick) const; -protected: - /* Builder 挂接最终 Private 后,同时绑定通用轴分派和时间轴公开接口分派。 */ - template - void bind_private_crtp(Object* object); + [[nodiscard]] Time_Of_Day tick_to_time(Axis_Time_Tick tick) const; }; } #include "Time_Axis.ipp" diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp index 8b1ddb3..8f309a7 100644 --- a/render_2D/render_2D/axis/Time_Axis.ipp +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -25,6 +25,9 @@ struct Time_Axis::Private : Prev_Private { [[nodiscard]] static const Time_Dispatch& time_dispatch_for(); /* 将一天内时间按支持的占位符格式化;未知字符原样保留。 */ [[nodiscard]] static std::string formatted_time(Time_Of_Day time, std::string_view format); + /* CRTP 覆盖:绑定通用轴机制和最终时间轴公开薄壳分派;派生 Private 必须先调用此实现。 */ + template + void bind_private_crtp(Object* object); }; inline Axis_Range Time_Axis::Private::coordinate_range(const Attached auto* object) const { using Object = std::remove_cv_t>; @@ -88,8 +91,8 @@ const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() return result; } template -void Time_Axis::bind_private_crtp(Object* object) { - Abs_Axis::bind_private_crtp(object); - static_cast(*object->d).time_dispatch = &Private::time_dispatch_for(); +void Time_Axis::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + time_dispatch = &Private::time_dispatch_for(); } } diff --git a/render_2D/render_2D/plottable/Afterglow.cpp b/render_2D/render_2D/plottable/Afterglow.cpp new file mode 100644 index 0000000..cb63d1f --- /dev/null +++ b/render_2D/render_2D/plottable/Afterglow.cpp @@ -0,0 +1,2 @@ +#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); } } diff --git a/render_2D/render_2D/plottable/Afterglow.hpp b/render_2D/render_2D/plottable/Afterglow.hpp new file mode 100644 index 0000000..fd0491a --- /dev/null +++ b/render_2D/render_2D/plottable/Afterglow.hpp @@ -0,0 +1,47 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../render/Blend2D_Cache.hpp" +#include "../scene/Render_Scene_2D.hpp" +#include "Plot_Types.hpp" +#include +#include +#include +#include +#include +namespace aethera::render_2d { +struct Afterglow_State_Tag {}; +struct Afterglow : Def, Tagged_Buffer> { + using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; + struct Prop : Prev_Prop {}; + struct State : Prev_State { + Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */ + Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ + std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ + std::size_t power_point_size{}; /* 栅格功率行数;零值使用 128。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式分块数。 */ + bool interpolate{true}; /* 是否对频谱列执行线性重采样。 */ + Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例,限制到 0..1。 */ + Color_Map color_map{}; /* 强度到颜色的映射。 */ + std::vector> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ + bool operator==(const State&) const; + }; + struct Private; + template struct Builder : Prev_Builder { + using Base = Prev_Builder; + Builder(Scene_Object* scene, Frequency_Object* frequency_axis, Power_Object* power_axis); + [[nodiscard]] std::expected, Dependency_Graph_Error> build(); + private: + Scene_Object* scene{}; /* 不拥有的所属 Scene;生命周期必须覆盖 Afterglow。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Afterglow。 */ + Power_Object* power_axis{}; /* 不拥有的功率轴;生命周期必须覆盖 Afterglow。 */ + }; + void append_spectrum(std::span values); + void append_spectrum(std::pmr::vector&& values); + template void append_spectrum(const Values& values); + [[nodiscard]] std::size_t history_count() const; + [[nodiscard]] std::size_t latest_spectrum_point_count() const; + [[nodiscard]] std::size_t rendered_cell_count() const; +}; +} +#include "Afterglow.ipp" diff --git a/render_2D/render_2D/plottable/Afterglow.ipp b/render_2D/render_2D/plottable/Afterglow.ipp new file mode 100644 index 0000000..f29f2da --- /dev/null +++ b/render_2D/render_2D/plottable/Afterglow.ipp @@ -0,0 +1,63 @@ +#pragma once +#include "common/Curve_Plot.hpp" +#include "common/Raster_Plot.hpp" +#include +namespace aethera::render_2d { +struct Afterglow::Private : Prev_Private { + struct Prepared { + detail::Raster_Layout layout{}; /* 两根轴决定的色块矩阵布局。 */ + std::vector intensity{}; /* 历史频谱衰减累加后的未归一化强度。 */ + std::vector pixels{}; /* 归一化强度经色图转换后的像素矩阵。 */ + Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ + Plot_Ratio maximum{1.0}; /* 本帧强度归一化分母,最小为 1。 */ + bool valid{}; /* 轴布局和输入数据是否足以生成色块。 */ + }; + using Append_Run = void (*)(Root*, std::span); using Count_Run = std::size_t (*)(const Root*); + struct Dispatch { + Append_Run append; /* 向最终对象提交一帧频谱。 */ + Count_Run history_count; /* 查询权威历史帧数。 */ + Count_Run latest_count; /* 查询最新一帧的点数。 */ + Count_Run rendered_count; /* 查询已准备的色块数。 */ + }; + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ + Power_Object* power_axis{}; /* 不拥有的功率轴。 */ + Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ + Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ + const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ + /* CRTP 覆盖:绑定 Renderable 能力和最终 Afterglow 分派表。 */ + template void bind_private_crtp(Object* object); + 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); + /* CRTP 覆盖:构建消费色块矩阵的 Paint 子图。 */ + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& 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 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); } +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 +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; } +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; } +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; } } } +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)); } } +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 +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; } +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 new file mode 100644 index 0000000..c66bbae --- /dev/null +++ b/render_2D/render_2D/plottable/Constellation_Diagram.cpp @@ -0,0 +1,2 @@ +#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); } } diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.hpp b/render_2D/render_2D/plottable/Constellation_Diagram.hpp new file mode 100644 index 0000000..667f50f --- /dev/null +++ b/render_2D/render_2D/plottable/Constellation_Diagram.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../render/Blend2D_Cache.hpp" +#include "../scene/Render_Scene_2D.hpp" +#include "Plot_Types.hpp" +#include +#include +#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> { + using Scene_Object = Impl; using Axis_Object = Impl; + struct Prop : Prev_Prop {}; + struct State : Prev_State { + Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */ + Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */ + Color point_color{Color::red_color()}; /* 接收点颜色。 */ + Color anchor_color{Color::yellow()}; /* 理想星座锚点颜色。 */ + Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */ + Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */ + Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ + std::vector points{}; /* 已提交且尚未过期的点。 */ + bool operator==(const State&) const; + }; + struct Private; + template struct Builder : Prev_Builder { + using Base = Prev_Builder; + Builder(Scene_Object* scene, Axis_Object* i_axis, Axis_Object* q_axis); + [[nodiscard]] std::expected, Dependency_Graph_Error> build(); + private: + Scene_Object* scene{}; /* 不拥有的所属 Scene;生命周期必须覆盖星座图。 */ + Axis_Object* i_axis{}; /* 不拥有的同相分量轴;生命周期必须覆盖星座图。 */ + Axis_Object* q_axis{}; /* 不拥有的正交分量轴;生命周期必须覆盖星座图。 */ + }; + void append_point(Point_F point); [[nodiscard]] std::size_t point_count() const; void fit_square_to_axes(); +}; +} +#include "Constellation_Diagram.ipp" diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.ipp b/render_2D/render_2D/plottable/Constellation_Diagram.ipp new file mode 100644 index 0000000..181198f --- /dev/null +++ b/render_2D/render_2D/plottable/Constellation_Diagram.ipp @@ -0,0 +1,49 @@ +#pragma once +#include "common/Curve_Plot.hpp" +#include +#include +#include +namespace aethera::render_2d { +struct Constellation_Diagram::Private : Prev_Private { + struct Prepared { + std::vector points{}; /* 尚未过期的接收点画布坐标。 */ + std::vector anchors{}; /* 当前调制类型的理想锚点画布坐标。 */ + Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ + bool valid{}; /* 两根轴是否正交且画布有效。 */ + }; + using Point_Run = void (*)(Root*, Point_F); using Count_Run = std::size_t (*)(const Root*); using Void_Run = void (*)(Root*); + struct Dispatch { + Point_Run append; /* 向最终对象提交接收点。 */ + Count_Run count; /* 查询权威接收点数。 */ + Void_Run fit; /* 将两根轴调整为等跨度。 */ + }; + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Axis_Object* i_axis{}; /* 不拥有的同相分量轴。 */ + Axis_Object* q_axis{}; /* 不拥有的正交分量轴。 */ + Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ + const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ + /* CRTP 覆盖:绑定 Renderable 能力和最终 Constellation 分派表。 */ + template void bind_private_crtp(Object* object); + void bind_sources(Scene_Object* scene_value, Axis_Object* i_axis_value, Axis_Object* q_axis_value); + template [[nodiscard]] static const Dispatch& dispatch_for(); + /* CRTP 覆盖:从当前点集和轴状态准备画布坐标。 */ + template void prepare_data(Object* object); + /* CRTP 覆盖:直接绘制已准备的星座点与锚点。 */ + template void paint(Object* object); + /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ + template void after_state_set(Object* object, Member Owner::* member, State_Access 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); } +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(); } +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; } +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 new file mode 100644 index 0000000..a1fa9d9 --- /dev/null +++ b/render_2D/render_2D/plottable/Frequency_Trace.cpp @@ -0,0 +1,9 @@ +#include "Frequency_Trace.hpp" +namespace aethera::render_2d { +bool Frequency_Trace_Sample::operator==(const Frequency_Trace_Sample&) 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); } +std::size_t Frequency_Trace::sample_count() const { return static_cast(*d).dispatch->sample_count(this); } +std::size_t Frequency_Trace::rendered_point_count() const { return static_cast(*d).dispatch->rendered_point_count(this); } +} diff --git a/render_2D/render_2D/plottable/Frequency_Trace.hpp b/render_2D/render_2D/plottable/Frequency_Trace.hpp new file mode 100644 index 0000000..8f18f1e --- /dev/null +++ b/render_2D/render_2D/plottable/Frequency_Trace.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../render/Blend2D_Cache.hpp" +#include "../scene/Render_Scene_2D.hpp" +#include "Plot_Types.hpp" +#include +#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> { + using Scene_Object = Impl; + using Time_Object = Impl; + using Value_Object = Impl; + struct Prop : Prev_Prop {}; + struct State : Prev_State { + 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 State&) const; + }; + struct Private; + template + struct Builder : Prev_Builder { + using Base = Prev_Builder; + Builder(Scene_Object* scene, Time_Object* time_axis, Value_Object* value_axis); + [[nodiscard]] std::expected, Dependency_Graph_Error> build(); + private: + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Time_Object* time_axis{}; /* 不拥有的时间轴。 */ + Value_Object* value_axis{}; /* 不拥有的数值轴。 */ + }; + void append_sample(Plot_Time_Tick tick, Plot_Value value); + void append_sample(Time_Of_Day time, Plot_Value value); + [[nodiscard]] std::size_t sample_count() const; + [[nodiscard]] std::size_t rendered_point_count() const; +}; +} +#include "Frequency_Trace.ipp" diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp new file mode 100644 index 0000000..9cb5cbe --- /dev/null +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -0,0 +1,101 @@ +#pragma once +#include "common/Curve_Plot.hpp" +namespace aethera::render_2d { +struct Frequency_Trace::Private : Prev_Private { + struct Prepared { + std::vector partitions{}; /* Prepare 子图各分块的曲线输出。 */ + std::vector values{}; /* 当前状态样本提取出的连续值。 */ + Axis_Range domain{}; /* 当前值集合对应的时间 tick 范围。 */ + Size canvas{}; /* 当前颜色层尺寸。 */ + bool valid{}; /* 两根轴正交且画布有效。 */ + }; + using Append_Run = void (*)(Root*, Plot_Time_Tick, Plot_Value); + using Time_Append_Run = void (*)(Root*, Time_Of_Day, Plot_Value); + using Count_Run = std::size_t (*)(const Root*); + struct Dispatch { + Append_Run append; /* 按 tick 向最终对象提交样本。 */ + Time_Append_Run append_time; /* 按时刻分配 tick 后提交样本。 */ + Count_Run sample_count; /* 查询权威样本数。 */ + Count_Run rendered_point_count; /* 查询已准备的曲线点数。 */ + }; + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Time_Object* time_axis{}; /* 不拥有的时间轴。 */ + Value_Object* value_axis{}; /* 不拥有的数值轴。 */ + Prepared prepared{}; /* 当前状态和轴状态推导出的 Paint 输入。 */ + Plot_Partition_Count graph_partition_count{}; /* 当前 Prepare 子图固化的分块数。 */ + const Dispatch* dispatch{}; /* 最终类型公开薄壳分派表。 */ + /* CRTP 覆盖:绑定 Renderable 能力和最终 Frequency_Trace 分派表。 */ + template void bind_private_crtp(Object* object); + 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); + /* CRTP 覆盖:构建消费已准备曲线的 Paint 子图。 */ + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& 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 +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) {} +template +std::expected, Dependency_Graph_Error> Frequency_Trace::Builder::build() { + auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto trace = std::move(result).value(); + 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); + 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()); } +template +tf::Taskflow Frequency_Trace::Private::build_prepare_graph(Object* object, const State& 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; } +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; + 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(); + 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); +} +template +void Frequency_Trace::Private::after_state_set(Object* object, Member Owner::*, State_Access) { if constexpr (std::same_as) object->template mark_dirty(); } +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(); }, + [](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; +} +template +void Frequency_Trace::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } +inline void Frequency_Trace::Private::bind_sources(Scene_Object* scene_value, Time_Object* time_axis_value, Value_Object* value_axis_value) { scene = scene_value; time_axis = time_axis_value; value_axis = value_axis_value; } +} diff --git a/render_2D/render_2D/plottable/Hover_Tooltip.cpp b/render_2D/render_2D/plottable/Hover_Tooltip.cpp new file mode 100644 index 0000000..f433461 --- /dev/null +++ b/render_2D/render_2D/plottable/Hover_Tooltip.cpp @@ -0,0 +1,11 @@ +#include "Hover_Tooltip.hpp" +namespace aethera::render_2d { +bool Hover_Tooltip_Properties::operator==(const Hover_Tooltip_Properties&) const = default; +namespace detail { +bool update_hover_tooltip(Hover_Tooltip_Runtime& tooltip, const Event& event) { + if (event.type == Event_Type::pointer_move) { const auto* pointer = dynamic_cast(&event); if (!pointer) return false; tooltip.active = true; tooltip.position = {pointer->position_x(), pointer->position_y()}; return true; } + if (event.type == Event_Type::leave) { tooltip.active = false; return true; } + return false; +} +} +} diff --git a/render_2D/render_2D/plottable/Hover_Tooltip.hpp b/render_2D/render_2D/plottable/Hover_Tooltip.hpp new file mode 100644 index 0000000..6ed4d93 --- /dev/null +++ b/render_2D/render_2D/plottable/Hover_Tooltip.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "../event/Event.hpp" +#include "../render/Blend2D_Cache.hpp" +namespace aethera::render_2d { +struct Hover_Tooltip_Properties { + bool tooltip_enabled{true}; /* 是否响应指针位置显示提示。 */ + Font tooltip_font{}; /* 提示文字字体。 */ + Pen tooltip_text_pen{Color::white()}; /* 提示文字样式。 */ + Brush tooltip_background_brush{Color{20, 20, 20, 220}, Brush_Style::solid}; /* 提示背景样式。 */ + bool operator==(const Hover_Tooltip_Properties&) const; +}; +namespace detail { +struct Hover_Tooltip_Runtime { + bool active{}; /* 指针当前是否位于控件内。 */ + Point_F position{}; /* 最近一次指针位置。 */ +}; +bool update_hover_tooltip(Hover_Tooltip_Runtime& tooltip, const Event& event); +} +} diff --git a/render_2D/render_2D/plottable/Plot_Types.cpp b/render_2D/render_2D/plottable/Plot_Types.cpp new file mode 100644 index 0000000..2083037 --- /dev/null +++ b/render_2D/render_2D/plottable/Plot_Types.cpp @@ -0,0 +1,17 @@ +#include "Plot_Types.hpp" +#include +#include +namespace aethera::render_2d { +Color_Map::Color_Map() : stops{Color{0, 0, 64, 255}, Color{0, 128, 255, 255}, Color::yellow(), Color::red_color()} {} +Color Color_Map::sample(Plot_Ratio ratio) const { + if (stops.empty()) return Color::transparent(); + if (stops.size() == 1) return stops.front(); + const Plot_Ratio position = std::clamp(std::isfinite(ratio) ? ratio : 0.0, 0.0, 1.0) * static_cast(stops.size() - 1); + const auto first = static_cast(std::floor(position)); + const auto second = std::min(first + 1, stops.size() - 1); + const Plot_Ratio fraction = position - static_cast(first); + const auto channel = [fraction](std::uint8_t left, std::uint8_t right) { return static_cast(std::lround(left + (static_cast(right) - left) * fraction)); }; + return {channel(stops[first].red, stops[second].red), channel(stops[first].green, stops[second].green), channel(stops[first].blue, stops[second].blue), channel(stops[first].alpha, stops[second].alpha)}; +} +bool Color_Map::operator==(const Color_Map&) const = default; +} diff --git a/render_2D/render_2D/plottable/Plot_Types.hpp b/render_2D/render_2D/plottable/Plot_Types.hpp new file mode 100644 index 0000000..c8ecf6c --- /dev/null +++ b/render_2D/render_2D/plottable/Plot_Types.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "../base/Types.hpp" +#include +#include +#include +namespace aethera::render_2d { +using Plot_Coordinate = double; +using Plot_Value = double; +using Plot_Ratio = double; +using Plot_Time_Tick = int; +using Plot_Duration_Milliseconds = std::uint64_t; +using Plot_Partition_Count = std::size_t; +using Plot_Index = std::ptrdiff_t; +enum class Plot_Partition_Mode : std::uint8_t { + automatic, + fixed +}; +struct Color_Map { + std::vector stops{}; /* 从低值到高值的离散颜色停靠点;空值使用透明色。 */ + Color_Map(); + [[nodiscard]] Color sample(Plot_Ratio ratio) const; + bool operator==(const Color_Map&) const; +}; +} diff --git a/render_2D/render_2D/plottable/Plottables.hpp b/render_2D/render_2D/plottable/Plottables.hpp index dc697c6..b672f7b 100644 --- a/render_2D/render_2D/plottable/Plottables.hpp +++ b/render_2D/render_2D/plottable/Plottables.hpp @@ -1,2 +1,8 @@ #pragma once +#include "Afterglow.hpp" +#include "Constellation_Diagram.hpp" +#include "Frequency_Trace.hpp" +#include "Selection_Rectangle_Overlay.hpp" #include "Spectrum.hpp" +#include "Sweep_Spectrum.hpp" +#include "Waterfall.hpp" diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp new file mode 100644 index 0000000..8d3be69 --- /dev/null +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -0,0 +1,6 @@ +#include "Selection_Rectangle_Overlay.hpp" +namespace aethera::render_2d { +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 new file mode 100644 index 0000000..491e1ad --- /dev/null +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../render/Blend2D_Cache.hpp" +#include "../scene/Render_Scene_2D.hpp" +#include "Plot_Types.hpp" +#include +#include +#include +namespace aethera::render_2d { +struct Selection_Rectangle_Overlay_State_Tag {}; +struct Selection_Rectangle_Overlay : Def, Tagged_Buffer> { + using Scene_Object = Impl; + using Axis_Object = Impl; + struct Prop : Prev_Prop {}; + struct State : Prev_State { + 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 State&) const; + }; + struct Private; + template + struct Builder : Prev_Builder { + using Base = Prev_Builder; + Builder(Scene_Object* scene, Axis_Object* horizontal_axis, Axis_Object* vertical_axis); + [[nodiscard]] std::expected, Dependency_Graph_Error> build(); + private: + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Axis_Object* horizontal_axis{}; /* 不拥有的水平坐标轴。 */ + Axis_Object* vertical_axis{}; /* 不拥有的垂直坐标轴。 */ + }; + [[nodiscard]] std::vector selected_regions() const; + void clear_selected_regions(); +}; +} +#include "Selection_Rectangle_Overlay.ipp" diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp new file mode 100644 index 0000000..9a7c621 --- /dev/null +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp @@ -0,0 +1,95 @@ +#pragma once +#include "common/Curve_Plot.hpp" +#include +namespace aethera::render_2d { +struct Selection_Rectangle_Overlay::Private : Prev_Private { + using No_Prepare = void; + using Regions_Get = std::vector (*)(const Root*); + using Clear_Run = void (*)(Root*); + struct Dispatch { + Regions_Get selected_regions; /* 查询最终对象已发布选择区域。 */ + Clear_Run clear_selected_regions; /* 清空最终对象选择区域。 */ + }; + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Axis_Object* horizontal_axis{}; /* 不拥有的水平坐标轴。 */ + Axis_Object* vertical_axis{}; /* 不拥有的垂直坐标轴。 */ + Point_F drag_origin{}; /* 当前拖动起点,单位为画布像素。 */ + Point_F drag_current{}; /* 当前拖动终点,单位为画布像素。 */ + bool dragging{}; /* 是否正在构造尚未提交的选择矩形。 */ + const Dispatch* dispatch{}; /* 最终类型公开薄壳分派表。 */ + /* CRTP 覆盖:绑定 Paint-only、事件能力和最终 Overlay 分派表。 */ + template void bind_private_crtp(Object* object); + void bind_sources(Scene_Object* scene_value, Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value); + template [[nodiscard]] static const Dispatch& dispatch_for(); + /* CRTP 覆盖:无需 Prepare 子图,直接绘制选择矩形。 */ + template void paint(Object* object); + /* 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 +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) {} +template +std::expected, Dependency_Graph_Error> Selection_Rectangle_Overlay::Builder::build() { + auto result = Base::build(); + if (!result) return std::unexpected(result.error()); + auto overlay = std::move(result).value(); + 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); + }); + if (!graph_result) return std::unexpected(graph_result.error()); + return std::move(overlay); +} +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; + auto& cache = object->template pending_buffer(); + cache.ensure_size(canvas); cache.clear(); + if (canvas.empty()) return; + detail::Painter painter(cache, canvas); + const auto paint_region = [&](Rect_F region) { + const auto rect = detail::map_plot_rect(horizontal_axis, {region.x, region.x + region.width}, vertical_axis, {region.y, region.y + region.height}, Axis_Orientation::horizontal); + painter.rect(rect, state.selection_border_pen, state.selection_brush); + }; + for (const auto& region : state.selected_regions) paint_region(region); + if (dragging) painter.rect(Rect_F{drag_origin.x, drag_origin.y, drag_current.x - drag_origin.x, drag_current.y - drag_origin.y}.normalized(), state.selection_border_pen, state.selection_brush); +} +template +void Selection_Rectangle_Overlay::Private::handle_event(Object* object, const Event& event) { + const auto* pointer = dynamic_cast(&event); + if (!pointer) return; + const Point_F point{pointer->position_x(), pointer->position_y()}; + if (event.type == Event_Type::pointer_press && pointer->pointer_button() == Mouse_Button::left) { dragging = true; drag_origin = point; drag_current = point; object->template mark_dirty(); event.accept(); return; } + if (event.type == Event_Type::pointer_move && dragging) { drag_current = point; object->template mark_dirty(); event.accept(); return; } + if (event.type != Event_Type::pointer_release || !dragging) return; + dragging = false; drag_current = point; + const Axis_Coordinate first_x = horizontal_axis->point_to_coordinate(drag_origin); + 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()); }); + 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 +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{}); } + }; + return value; +} +template +void Selection_Rectangle_Overlay::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } +inline void Selection_Rectangle_Overlay::Private::bind_sources(Scene_Object* scene_value, Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value) { scene = scene_value; horizontal_axis = horizontal_axis_value; vertical_axis = vertical_axis_value; } +} diff --git a/render_2D/render_2D/plottable/Spectrum.cpp b/render_2D/render_2D/plottable/Spectrum.cpp index ee7d712..de18519 100644 --- a/render_2D/render_2D/plottable/Spectrum.cpp +++ b/render_2D/render_2D/plottable/Spectrum.cpp @@ -1,15 +1,62 @@ #include "Spectrum.hpp" namespace aethera::render_2d { -void Spectrum::update_samples(std::span values) { +bool Spectrum_Frame::operator==(const Spectrum_Frame&) 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); } +void Spectrum::update_samples(std::pmr::vector&& values) { + update_samples(std::span(values.data(), values.size())); +} std::size_t Spectrum::sample_count() const { return static_cast(*d).dispatch->sample_count(this); } std::size_t Spectrum::rendered_point_count() const { return static_cast(*d).dispatch->rendered_point_count(this); } -bool Spectrum::power_at(double frequency, double& power) const { - return static_cast(*d).dispatch->power_at(this, frequency, power); +std::expected Spectrum::power_at(Spectrum_Frequency frequency) const { + return static_cast(*d).dispatch->power_at(this, frequency); +} +void Spectrum::add_custom_marker(Spectrum_Frequency frequency) { + static_cast(*d).dispatch->add_marker(this, frequency); +} +void Spectrum::add_custom_line_marker(Spectrum_Frequency frequency) { + static_cast(*d).dispatch->add_marker(this, frequency); +} +void Spectrum::remove_custom_marker(Spectrum_Frequency frequency) { + static_cast(*d).dispatch->remove_marker(this, frequency); +} +void Spectrum::remove_selected_marker() { + static_cast(*d).dispatch->remove_selected_marker(this); +} +void Spectrum::clear_custom_markers() { + static_cast(*d).dispatch->clear_markers(this); +} +std::size_t Spectrum::selectable_line_marker_count() const { + return static_cast(*d).dispatch->marker_count(this); +} +Spectrum_Marker_Index Spectrum::selected_marker_index() const { + return static_cast(*d).dispatch->selected_marker(this); +} +void Spectrum::set_selected_marker_index(Spectrum_Marker_Index index) { + static_cast(*d).dispatch->set_selected_marker(this, index); +} +void Spectrum::select_next_marker() { + static_cast(*d).dispatch->select_next_marker(this); +} +void Spectrum::select_previous_marker() { + static_cast(*d).dispatch->select_previous_marker(this); +} +void Spectrum::clear_marker_selection() { + static_cast(*d).dispatch->set_selected_marker(this, -1); +} +std::expected Spectrum::marker_frequency(Spectrum_Marker_Index index) const { + return static_cast(*d).dispatch->marker_frequency(this, index); +} +Spectrum::Set_Marker_Frequency_Result Spectrum::set_marker_frequency(Spectrum_Marker_Index index, Spectrum_Frequency frequency) { + return static_cast(*d).dispatch->set_marker_frequency(this, index, frequency); +} +Spectrum::Set_Current_Marker_Frequency_Result Spectrum::set_current_marker_frequency(Spectrum_Frequency frequency) { + return static_cast(*d).dispatch->set_current_marker_frequency(this, frequency); } } diff --git a/render_2D/render_2D/plottable/Spectrum.hpp b/render_2D/render_2D/plottable/Spectrum.hpp index 3f3b046..f53c5a9 100644 --- a/render_2D/render_2D/plottable/Spectrum.hpp +++ b/render_2D/render_2D/plottable/Spectrum.hpp @@ -1,82 +1,130 @@ #pragma once #include "../axis/Axis.hpp" #include "../render/Blend2D_Cache.hpp" -#include +#include "../scene/Render_Scene_2D.hpp" +#include "Plot_Types.hpp" +#include +#include +#include #include +#include #include namespace aethera::render_2d { +using Spectrum_Frequency = Plot_Coordinate; +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{}; /* 最近提交的当前频谱功率样本。 */ - std::vector maxima{}; /* 与 samples 同尺寸的逐点历史最大值。 */ - std::vector minima{}; /* 与 samples 同尺寸的逐点历史最小值。 */ - bool operator==(const Spectrum_Frame&) const = default; + std::vector samples{}; /* 最近提交的当前频谱功率样本。 */ + std::vector maxima{}; /* 与 samples 同尺寸的逐点历史最大值。 */ + std::vector minima{}; /* 与 samples 同尺寸的逐点历史最小值。 */ + bool operator==(const Spectrum_Frame&) const; }; -/* 使用频率轴和功率轴绘制当前值、最大保持、最小保持及扫频区域。 */ -struct Spectrum : Def, - Tagged_Buffer, - Tagged_Buffer> { +/* 使用频率轴和功率轴分块准备、绘制当前值、保持曲线及频率标记。 */ +struct Spectrum : Def, Tagged_Buffer, Tagged_Buffer> { + using Scene_Object = Impl; + using Frequency_Object = Impl; + using Power_Object = Impl; struct Prop : Prev_Prop {}; struct State : Prev_State { Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */ - double center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ + Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */ - + Spectrum_Partition_Mode partition_mode{Spectrum_Partition_Mode::automatic}; /* Prepare 子图的分块策略。 */ + std::size_t partition_count{1}; /* fixed 模式使用的分块数;零值按 1 处理。 */ bool max_hold_visible{}; /* 是否绘制逐点历史最大值曲线。 */ bool min_hold_visible{}; /* 是否绘制逐点历史最小值曲线。 */ bool max_marker_visible{}; /* 是否标记当前样本的最大值位置。 */ bool min_marker_visible{}; /* 是否标记当前样本的最小值位置。 */ bool sweep_region_visible{}; /* 是否绘制扫频范围背景。 */ bool visible_range_only{true}; /* 是否裁掉频率轴当前范围以外的线段。 */ - Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻样本间的插值规则。 */ - Brush max_brush{}; /* 最大保持曲线下方的填充样式。 */ Brush current_brush{}; /* 当前频谱曲线下方的填充样式。 */ Brush min_brush{}; /* 最小保持曲线下方的填充样式。 */ - Pen max_pen{Color::red_color()}; /* 最大保持曲线及最大值标记样式。 */ Pen current_pen{Color::green_color()}; /* 当前频谱曲线样式。 */ Pen min_pen{Color::white()}; /* 最小保持曲线及最小值标记样式。 */ + Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; /* 当前选中自定义标记的线条样式。 */ + Pen marker_pen{Color::red_color()}; /* 未选中自定义标记的线条样式。 */ Pen middle_frequency_pen{Color::red_color()}; /* 中心频率垂线样式。 */ - Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */ - bool operator==(const State&) const = default; + std::vector custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */ + Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */ + bool operator==(const State&) const; }; - /* 完整声明、曲线准备缓存和 CRTP 能力见 Spectrum.ipp。 */ + /* 完整声明、分块子图、内部状态操作和 CRTP 分派见 Spectrum.ipp。 */ struct Private; template struct Builder : Prev_Builder { using Base = Prev_Builder; - template - Builder(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis) - : Base(), configure([frequency_axis, power_axis](Object* object) { - object->bind_axes(frequency_axis, power_axis); - }) {} - [[nodiscard]] auto build() { - auto result = Base::build(); - if (result) configure(result->get()); - return result; - } + Builder(Scene_Object* scene, Frequency_Object* frequency_axis, Power_Object* power_axis); + [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: - std::function configure; /* build 后把轴引用写入最终 Spectrum::Private。 */ + Scene_Object* scene{}; /* 不拥有的所属二维 Scene;生命周期必须覆盖 Spectrum。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Spectrum。 */ + Power_Object* power_axis{}; /* 不拥有的功率轴;生命周期必须覆盖 Spectrum。 */ }; - /* 提交新一帧样本并更新逐点最大/最小保持。 */ - void update_samples(std::span values); + /* 提交新一帧样本并更新逐点最大、最小保持。 */ + void update_samples(std::span values); + /* 接收 PMR 连续样本容器;提交完成后调用方仍拥有容器。 */ + void update_samples(std::pmr::vector&& values); + /* 接收具有 data() 和 size() 的连续样本容器。 */ + template + void update_samples(const Values& values); /* 返回最近一次双缓冲交换后发布的样本数。 */ [[nodiscard]] std::size_t sample_count() const; - /* 返回最近一次 Prepare 得到的当前曲线点数。 */ + /* 返回最近一次 Prepare 子图得到的当前曲线点数。 */ [[nodiscard]] std::size_t rendered_point_count() const; - /* 在已发布帧的线性样本位置上查询功率;频率越界或没有样本时返回 false。 */ - [[nodiscard]] bool power_at(double frequency, double& power) const; -protected: - /* Builder 挂接最终 Private 后安装 Renderable 与 Spectrum 业务分派。 */ - template - void bind_private_crtp(Object* object); - /* Spectrum::Builder 在最终 Private 创建后绑定两根轴;轴必须比 Spectrum 生命周期更长。 */ - template - void bind_axes(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis); + enum class Power_At_Result { + no_samples, + invalid_frequency_range, + frequency_out_of_range + }; + /* 在已发布帧的线性样本位置上查询功率。 */ + [[nodiscard]] std::expected power_at(Spectrum_Frequency frequency) const; + /* 添加一个自定义频率标记。 */ + void add_custom_marker(Spectrum_Frequency frequency); + /* 添加一个自定义垂直线标记。 */ + void add_custom_line_marker(Spectrum_Frequency frequency); + /* 删除与给定频率距离最近的自定义标记。 */ + void remove_custom_marker(Spectrum_Frequency frequency); + /* 删除当前选中的自定义标记;未选择时不处理。 */ + void remove_selected_marker(); + /* 删除全部自定义标记并清除选择。 */ + void clear_custom_markers(); + /* 返回可选择的自定义线标记数量。 */ + [[nodiscard]] std::size_t selectable_line_marker_count() const; + /* 返回当前选中标记下标;-1 表示未选择。 */ + [[nodiscard]] Spectrum_Marker_Index selected_marker_index() const; + /* 选择指定下标;越界值清除选择。 */ + void set_selected_marker_index(Spectrum_Marker_Index index); + /* 循环选择下一个标记;无标记时清除选择。 */ + void select_next_marker(); + /* 循环选择上一个标记;无标记时清除选择。 */ + void select_previous_marker(); + /* 清除当前标记选择。 */ + void clear_marker_selection(); + enum class Marker_Frequency_Result { + index_out_of_range + }; + /* 查询指定标记的频率。 */ + [[nodiscard]] std::expected marker_frequency(Spectrum_Marker_Index index) const; + enum class Set_Marker_Frequency_Result { + updated, + index_out_of_range + }; + /* 修改指定标记频率。 */ + [[nodiscard]] Set_Marker_Frequency_Result set_marker_frequency(Spectrum_Marker_Index index, Spectrum_Frequency frequency); + enum class Set_Current_Marker_Frequency_Result { + updated, + no_selection + }; + /* 修改当前选中标记频率。 */ + [[nodiscard]] Set_Current_Marker_Frequency_Result set_current_marker_frequency(Spectrum_Frequency frequency); }; } #include "Spectrum.ipp" diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index 57a60c6..c442eb4 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -1,313 +1,217 @@ #pragma once #include #include -#include +#include +#include "common/Curve_Plot.hpp" namespace aethera::render_2d { struct Spectrum::Private : Prev_Private { - struct Axis_Binding { - Root* object{}; /* 不拥有的轴对象;Builder 要求其生命周期覆盖 Spectrum。 */ - Axis_Range (*coordinate_range)(const Root*){}; /* 读取轴当前有向坐标范围。 */ - double (*coordinate_to_pixel)(const Root*, double){}; /* 将坐标映射到轴向画布像素。 */ - Axis_Orientation (*orientation)(const Root*){}; /* 查询轴当前方向,不保存方向镜像。 */ - Size (*canvas_size)(const Root*){}; /* 查询轴当前画布尺寸,不保存尺寸镜像。 */ - template - [[nodiscard]] static Axis_Binding make(Axis* axis) { - return { - axis, - [](const Root* root) { return static_cast(root)->coordinate_range(); }, - [](const Root* root, double coordinate) { - return static_cast(root)->coordinate_to_pixel(coordinate); - }, - [](const Root* root) { - Axis_Orientation result{}; - static_cast(root)->template access_state( - [&](const Abs_Axis::State& state) { result = state.orientation; }); - return result; - }, - [](const Root* root) { - Size result{}; - static_cast(root)->template access_state( - [&](const Abs_Axis::State& state) { result = state.canvas_size; }); - return result; - } - }; - } - [[nodiscard]] Axis_Range range() const { return coordinate_range(object); } - [[nodiscard]] double pixel(double coordinate) const { return coordinate_to_pixel(object, coordinate); } - [[nodiscard]] explicit operator bool() const noexcept { - return object && coordinate_range && coordinate_to_pixel && orientation && canvas_size; - } + using Prepared_Curve = detail::Curve_Prepared; + struct Prepared_Partition { + Prepared_Curve current{}; /* 当前频谱在本分块内的曲线。 */ + Prepared_Curve maximum{}; /* 最大保持在本分块内的曲线。 */ + Prepared_Curve minimum{}; /* 最小保持在本分块内的曲线。 */ + Rect_F clip{}; /* 本分块对应的画布裁剪矩形。 */ }; - struct Sample { - double coordinate{}; /* 插值样本对应的频率,单位为 Hz。 */ - double value{}; /* 插值后的功率值。 */ + enum class Marker_Style : std::uint8_t { middle, normal, selected }; + struct Prepared_Marker { + Point_F first{}; /* 标记线在功率轴起点侧的端点。 */ + Point_F second{}; /* 标记线在功率轴终点侧的端点。 */ + Marker_Style style{Marker_Style::normal}; /* Paint 阶段选择描边样式的业务类型。 */ }; - struct Prepared_Curve { - std::vector points{}; /* 映射到画布后的折线点。 */ - std::vector fill{}; /* 含功率轴基线闭合点的填充多边形。 */ + struct Prepared_Extreme { + Point_F point{}; /* 当前帧极值映射到画布的位置。 */ + bool maximum{}; /* true 使用最大值样式,false 使用最小值样式。 */ }; struct Prepared { - Prepared_Curve current{}; /* 当前频谱曲线。 */ - Prepared_Curve maximum{}; /* 最大保持曲线。 */ - Prepared_Curve minimum{}; /* 最小保持曲线。 */ - Rect_F sweep_region{}; /* 扫频背景在画布中的矩形。 */ - Point_F center_first{}; /* 中心频率线的功率轴起点。 */ - Point_F center_second{}; /* 中心频率线的功率轴终点。 */ - Point_F maximum_point{}; /* 当前样本最大值标记位置。 */ - Point_F minimum_point{}; /* 当前样本最小值标记位置。 */ - Size canvas_size{}; /* 两根轴当前状态共同确定的颜色层尺寸。 */ - - bool maximum_valid{}; /* maximum_point 是否有效。 */ - bool minimum_valid{}; /* minimum_point 是否有效。 */ - bool valid{}; /* 两根轴与画布是否足以生成绘制数据。 */ + std::vector partitions{}; /* Prepare 子图各分块的独立输出。 */ + std::vector markers{}; /* 中心频率及自定义频率标记。 */ + std::vector extremes{}; /* 当前帧启用的最大值和最小值标记。 */ + Rect_F sweep_region{}; /* 扫频背景在画布中的矩形。 */ + Size canvas_size{}; /* 所属 Scene viewport 决定的颜色层尺寸。 */ + bool valid{}; /* 两根轴与画布是否足以生成绘制数据。 */ }; - using Update_Run = void (*)(Root*, std::span); + using Update_Run = void (*)(Root*, std::span); using Count_Run = std::size_t (*)(const Root*); - using Power_Run = bool (*)(const Root*, double, double&); + using Power_Run = std::expected (*)(const Root*, Spectrum_Frequency); + using Frequency_Run = void (*)(Root*, Spectrum_Frequency); + using Void_Run = void (*)(Root*); + using Index_Get_Run = Spectrum_Marker_Index (*)(const Root*); + using Index_Set_Run = void (*)(Root*, Spectrum_Marker_Index); + using Marker_Frequency_Run = std::expected (*)(const Root*, Spectrum_Marker_Index); + using Set_Marker_Frequency_Run = Set_Marker_Frequency_Result (*)(Root*, Spectrum_Marker_Index, Spectrum_Frequency); + using Set_Current_Marker_Frequency_Run = Set_Current_Marker_Frequency_Result (*)(Root*, Spectrum_Frequency); struct Dispatch { Update_Run update_samples; /* 向最终对象写入 Spectrum_Frame_Tag。 */ - Count_Run sample_count; /* 查询最终对象最近发布的样本数。 */ - Count_Run rendered_point_count; /* 查询本轮准备出的当前曲线点数。 */ - Power_Run power_at; /* 查询最终对象最近发布帧的插值功率。 */ + Count_Run sample_count; /* 查询最终对象已发布样本数量。 */ + Count_Run rendered_point_count; /* 查询 Prepare 子图生成的曲线点数。 */ + Power_Run power_at; /* 查询最终对象已发布帧的插值功率。 */ + Frequency_Run add_marker; /* 添加自定义标记。 */ + Frequency_Run remove_marker; /* 删除最接近指定频率的标记。 */ + Void_Run remove_selected_marker; /* 删除当前选中标记。 */ + Void_Run clear_markers; /* 清空全部标记。 */ + Count_Run marker_count; /* 查询自定义标记数量。 */ + Index_Get_Run selected_marker; /* 查询当前选中下标。 */ + Index_Set_Run set_selected_marker; /* 修改当前选中下标。 */ + Void_Run select_next_marker; /* 循环选择下一个标记。 */ + Void_Run select_previous_marker; /* 循环选择上一个标记。 */ + Marker_Frequency_Run marker_frequency; /* 查询指定标记频率。 */ + Set_Marker_Frequency_Run set_marker_frequency; /* 修改指定标记频率。 */ + Set_Current_Marker_Frequency_Run set_current_marker_frequency; /* 修改当前选中标记频率。 */ }; - Axis_Binding frequency_axis{}; /* 频率到横向像素的唯一映射来源。 */ - Axis_Binding power_axis{}; /* 功率到纵向像素的唯一映射来源。 */ - Prepared prepared{}; /* 由当前 State、Frame 和轴状态推导的 Paint 输入。 */ - const Dispatch* dispatch{}; /* Builder 绑定最终 Spectrum 类型后的静态分派表。 */ - template - [[nodiscard]] static const Dispatch& dispatch_for(); - template - void update_samples(Object* object, std::span values); - template - void prepare_data(Object* object); - template - void paint(Object* object); - [[nodiscard]] static bool power_at(const State& state, - const Spectrum_Frame& frame, - double frequency, - double& power); - [[nodiscard]] static double power_domain_lerp(double first, double second, double ratio); - [[nodiscard]] static double cubic_value(double previous, double first, double second, - double next, double ratio) noexcept; - [[nodiscard]] static std::vector interpolate(std::span values, - Axis_Range domain, - Line_Interpolation_Mode mode); - [[nodiscard]] std::vector visible_samples(std::vector samples) const; - [[nodiscard]] Prepared_Curve prepare_curve(std::span values, - Axis_Range domain, - Line_Interpolation_Mode mode, - bool visible_only) const; - [[nodiscard]] Point_F map_point(double frequency, double power) const; - static void paint_curve(detail::Painter& painter, - const Prepared_Curve& curve, - const Pen& pen, - const Brush& brush); + Scene_Object* scene{}; /* 不拥有的所属二维 Scene;Builder 已登记状态层依赖。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;Prepare 直接读取其当前状态。 */ + Power_Object* power_axis{}; /* 不拥有的功率轴;Prepare 直接读取其当前状态。 */ + Prepared prepared{}; /* 当前权威 State、Frame 和轴状态推导出的 Paint 输入。 */ + std::size_t prepare_graph_partition_count{}; /* 当前 Prepare 子图实际固化的任务分块数。 */ + const Dispatch* dispatch{}; /* Builder 绑定最终 Spectrum 类型后的静态分派表。 */ + /* CRTP 覆盖:绑定 Renderable 机制和 Spectrum 公开薄壳分派;派生 Private 必须先调用此实现。 */ + template void bind_private_crtp(Object* object); + /* Builder 内部绑定 Scene 与两根轴;三个来源都必须比 Spectrum 生命周期更长。 */ + void bind_render_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); + template [[nodiscard]] static const Dispatch& dispatch_for(); + template void update_samples(Object* object, std::span values); + template void add_marker(Object* object, Spectrum_Frequency frequency); + template void remove_marker(Object* object, Spectrum_Frequency frequency); + template void remove_selected_marker(Object* object); + template void clear_markers(Object* object); + template void set_selected_marker(Object* object, Spectrum_Marker_Index index); + template void select_next_marker(Object* object); + template void select_previous_marker(Object* object); + 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); + /* CRTP 子图能力:按当前样本数和 State 分块策略构建并行 Prepare 图。 */ + template [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); + /* CRTP 子图能力:构建背景、分块曲线和覆盖标记的 Paint 图。 */ + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& 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 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); }; -inline bool Spectrum::Private::power_at(const State& state, const Spectrum_Frame& frame, - double frequency, double& power) { - if (frame.samples.empty() || !state.frequency_range.contains(frequency) || - state.frequency_range.length() == 0.0) return false; - const double normalized = (frequency - state.frequency_range.origin) / state.frequency_range.length(); - const double position = std::clamp(normalized, 0.0, 1.0) * (frame.samples.size() - 1); +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) {} +template +std::expected, Dependency_Graph_Error> Spectrum::Builder::build() { + auto result = Base::build(); + if (!result) return std::unexpected(result.error()); + auto spectrum = std::move(result).value(); + static_cast(*spectrum->d).bind_render_sources(scene, frequency_axis, power_axis); + auto dependency_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { + 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); + paint.add(frequency_axis); + paint.add(power_axis); + paint.add(spectrum.get()); + }); + if (!dependency_result) return std::unexpected(dependency_result.error()); + return std::move(spectrum); +} +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) { + 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); + const Spectrum_Interpolation_Ratio normalized = (frequency - state.frequency_range.origin) / state.frequency_range.length(); + const Spectrum_Interpolation_Ratio position = std::clamp(normalized, 0.0, 1.0) * static_cast(frame.samples.size() - 1); const auto lower = static_cast(std::floor(position)); const auto upper = std::min(lower + 1, frame.samples.size() - 1); - const double fraction = position - lower; - power = frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction; - return true; -} -inline double Spectrum::Private::power_domain_lerp(double first, double second, double ratio) { - const double first_power = std::pow(10.0, std::clamp(first, -3'000.0, 3'000.0) / 10.0); - const double second_power = std::pow(10.0, std::clamp(second, -3'000.0, 3'000.0) / 10.0); - return 10.0 * std::log10(std::max(first_power + (second_power - first_power) * ratio, 1e-300)); -} -inline double Spectrum::Private::cubic_value(double previous, double first, double second, - double next, double ratio) noexcept { - const double ratio2 = ratio * ratio; - const double ratio3 = ratio2 * ratio; - return 0.5 * ((2.0 * first) + (-previous + second) * ratio + - (2.0 * previous - 5.0 * first + 4.0 * second - next) * ratio2 + - (-previous + 3.0 * first - 3.0 * second + next) * ratio3); -} -inline std::vector Spectrum::Private::interpolate( - std::span values, Axis_Range domain, Line_Interpolation_Mode mode) { - std::vector result; - if (values.empty()) return result; - if (values.size() == 1) return {{domain.origin, values.front()}}; - constexpr int subdivisions = 4; - const double denominator = static_cast(values.size() - 1); - const auto coordinate = [domain, denominator](std::size_t index) { - return domain.origin + domain.length() * index / denominator; - }; - result.push_back({coordinate(0), values.front()}); - for (std::size_t index = 0; index + 1 < values.size(); ++index) { - const double first_coordinate = coordinate(index); - const double second_coordinate = coordinate(index + 1); - const double first = values[index]; - const double second = values[index + 1]; - switch (mode) { - case Line_Interpolation_Mode::nearest_sample: { - const double middle = (first_coordinate + second_coordinate) * 0.5; - result.push_back({middle, first}); - result.push_back({middle, second}); - result.push_back({second_coordinate, second}); - break; - } - case Line_Interpolation_Mode::linear_value: - result.push_back({second_coordinate, second}); - break; - case Line_Interpolation_Mode::linear_power_domain: - for (int part = 1; part <= subdivisions; ++part) { - const double ratio = static_cast(part) / subdivisions; - result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, - power_domain_lerp(first, second, ratio)}); - } - break; - case Line_Interpolation_Mode::step_left: - result.push_back({second_coordinate, first}); - result.push_back({second_coordinate, second}); - break; - case Line_Interpolation_Mode::step_right: - result.push_back({first_coordinate, second}); - result.push_back({second_coordinate, second}); - break; - case Line_Interpolation_Mode::cubic_value: { - const double previous = values[index == 0 ? 0 : index - 1]; - const double next = values[std::min(index + 2, values.size() - 1)]; - for (int part = 1; part <= subdivisions; ++part) { - const double ratio = static_cast(part) / subdivisions; - result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, - cubic_value(previous, first, second, next, ratio)}); - } - break; - } - } - } - return result; -} -inline std::vector Spectrum::Private::visible_samples( - std::vector samples) const { - const auto [low, high] = std::minmax(frequency_axis.range().origin, frequency_axis.range().target); - if (samples.size() < 2) - return !samples.empty() && samples.front().coordinate >= low && samples.front().coordinate <= high - ? std::move(samples) : std::vector{}; - std::size_t first = samples.size(); - std::size_t last{}; - for (std::size_t index = 0; index + 1 < samples.size(); ++index) { - const auto [segment_low, segment_high] = - std::minmax(samples[index].coordinate, samples[index + 1].coordinate); - if (segment_high < low || segment_low > high) continue; - first = std::min(first, index); - last = std::max(last, index + 1); - } - if (first == samples.size()) return {}; - return {samples.begin() + static_cast(first), - samples.begin() + static_cast(last + 1)}; -} -inline Spectrum::Private::Prepared_Curve Spectrum::Private::prepare_curve( - std::span values, Axis_Range domain, Line_Interpolation_Mode mode, - bool visible_only) const { - Prepared_Curve result; - auto samples = interpolate(values, domain, mode); - if (visible_only) samples = visible_samples(std::move(samples)); - for (const auto& sample : samples) { - if (std::isfinite(sample.coordinate) && std::isfinite(sample.value)) - result.points.push_back(map_point(sample.coordinate, sample.value)); - } - if (result.points.size() < 2) return result; - result.fill.reserve(result.points.size() + 2); - const double baseline = power_axis.pixel(power_axis.range().target); - Point_F first_baseline = result.points.front(); - Point_F last_baseline = result.points.back(); - if (power_axis.orientation(power_axis.object) == Axis_Orientation::horizontal) { - first_baseline.x = baseline; - last_baseline.x = baseline; - } else { - first_baseline.y = baseline; - last_baseline.y = baseline; - } - result.fill.push_back(first_baseline); - result.fill.insert(result.fill.end(), result.points.begin(), result.points.end()); - result.fill.push_back(last_baseline); - return result; -} -inline Point_F Spectrum::Private::map_point(double frequency, double power) const { - const double frequency_pixel = frequency_axis.pixel(frequency); - const double power_pixel = power_axis.pixel(power); - return frequency_axis.orientation(frequency_axis.object) == Axis_Orientation::horizontal - ? Point_F{frequency_pixel, power_pixel} : Point_F{power_pixel, frequency_pixel}; -} -inline void Spectrum::Private::paint_curve(detail::Painter& painter, const Prepared_Curve& curve, - const Pen& pen, const Brush& brush) { - if (curve.points.size() < 2) return; - if (brush.enabled()) painter.polygon(curve.fill, Pen{.style = Line_Style::none}, brush); - painter.polyline(curve.points, pen); + const Spectrum_Interpolation_Ratio fraction = position - static_cast(lower); + return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction; } template -void Spectrum::Private::update_samples(Object* object, std::span values) { - const Spectrum_Frame& previous = object->template current_buffer(); - Spectrum_Frame next; - next.samples.assign(values.begin(), values.end()); - next.maxima.resize(values.size()); - next.minima.resize(values.size()); - for (std::size_t index = 0; index < values.size(); ++index) { - next.maxima[index] = previous.maxima.size() == values.size() - ? std::max(previous.maxima[index], values[index]) : values[index]; - next.minima[index] = previous.minima.size() == values.size() - ? std::min(previous.minima[index], values[index]) : values[index]; - } - object->template pending_buffer() = std::move(next); - object->template mark_dirty(); +std::size_t Spectrum::Private::desired_partition_count(const Object* object, const State& state) const { + return detail::curve_partition_count(state.partition_mode, state.partition_count, object->template current_buffer().samples.size()); } template -void Spectrum::Private::prepare_data(Object* object) { +bool Spectrum::Private::should_rebuild_prepare_graph(Object* object, const State& state) { + return prepare_graph_partition_count != desired_partition_count(object, state); +} +template +tf::Taskflow Spectrum::Private::build_prepare_graph(Object* object, const State& state) { + const std::size_t partition_count = desired_partition_count(object, state); + prepare_graph_partition_count = partition_count; + tf::Taskflow graph; + auto begin = graph.emplace([this, object, partition_count] { prepare_frame(object, partition_count); }).name("spectrum.prepare.frame"); + for (std::size_t index = 0; index < partition_count; ++index) { + auto partition = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("spectrum.prepare.partition"); + begin.precede(partition); + } + return graph; +} +template +tf::Taskflow Spectrum::Private::build_paint_graph(Object* object, const State&) { + tf::Taskflow graph; + graph.emplace([this, object] { paint_frame(object); }).name("spectrum.paint.frame"); + return graph; +} +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& 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(); prepared = {}; - if (!frequency_axis || !power_axis) return; - const Axis_Orientation frequency_orientation = frequency_axis.orientation(frequency_axis.object); - const Axis_Orientation power_orientation = power_axis.orientation(power_axis.object); - if (frequency_orientation == power_orientation) return; - const Size frequency_canvas = frequency_axis.canvas_size(frequency_axis.object); - const Size power_canvas = power_axis.canvas_size(power_axis.object); - if (frequency_canvas.empty() || frequency_canvas != power_canvas) return; - prepared.canvas_size = frequency_canvas; - prepared.current = prepare_curve(frame.samples, state.frequency_range, - state.interpolation_mode, state.visible_range_only); - if (state.max_hold_visible) - prepared.maximum = prepare_curve(frame.maxima, state.frequency_range, - state.interpolation_mode, state.visible_range_only); - if (state.min_hold_visible) - prepared.minimum = prepare_curve(frame.minima, state.frequency_range, - state.interpolation_mode, state.visible_range_only); - const Axis_Range power_range = power_axis.range(); - prepared.center_first = map_point(state.center_frequency, power_range.origin); - prepared.center_second = map_point(state.center_frequency, power_range.target); - if (state.sweep_region_visible) { - const Point_F first = map_point(state.sweep_frequency_range.origin, power_range.origin); - const Point_F second = map_point(state.sweep_frequency_range.target, power_range.target); - prepared.sweep_region = Rect_F{first.x, first.y, second.x - first.x, second.y - first.y}.normalized(); + prepared.partitions.resize(partition_count); + if (frequency_layout.orientation == power_layout.orientation) return; + if (scene_state.viewport.empty() || frequency_layout.canvas_size != scene_state.viewport || power_layout.canvas_size != scene_state.viewport) return; + prepared.canvas_size = scene_state.viewport; + if (state.sweep_region_visible) prepared.sweep_region = detail::map_plot_rect(frequency_axis, state.sweep_frequency_range, power_axis, power_state.coordinate_range, frequency_layout.orientation); + prepared.markers.push_back({detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), Marker_Style::middle}); + for (std::size_t index = 0; index < state.custom_markers.size(); ++index) { + const Spectrum_Frequency frequency = state.custom_markers[index]; + const Marker_Style style = static_cast(index) == state.selected_marker ? Marker_Style::selected : Marker_Style::normal; + prepared.markers.push_back({detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), style}); } - if (!frame.samples.empty()) { - const double denominator = frame.samples.size() > 1 ? frame.samples.size() - 1.0 : 1.0; - const auto point_at = [&](auto iterator) { - const std::size_t index = static_cast(std::distance(frame.samples.begin(), iterator)); - const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator; - return map_point(frequency, *iterator); + if (!frame.samples.empty() && (state.max_marker_visible || state.min_marker_visible)) { + const Spectrum_Interpolation_Ratio denominator = frame.samples.size() > 1 ? static_cast(frame.samples.size() - 1) : 1.0; + const auto prepare_extreme = [&](bool maximum) { + const auto iterator = maximum ? std::max_element(frame.samples.begin(), frame.samples.end()) : std::min_element(frame.samples.begin(), frame.samples.end()); + const auto index = static_cast(std::distance(frame.samples.begin(), iterator)); + const Spectrum_Frequency frequency = state.frequency_range.origin + state.frequency_range.length() * static_cast(index) / denominator; + prepared.extremes.push_back({detail::map_plot_point(frequency_axis, frequency, power_axis, *iterator, frequency_layout.orientation), maximum}); }; - if (state.max_marker_visible) { - prepared.maximum_point = point_at(std::max_element(frame.samples.begin(), frame.samples.end())); - prepared.maximum_valid = true; - } - if (state.min_marker_visible) { - prepared.minimum_point = point_at(std::min_element(frame.samples.begin(), frame.samples.end())); - prepared.minimum_valid = true; - } + if (state.max_marker_visible) prepare_extreme(true); + if (state.min_marker_visible) prepare_extreme(false); } prepared.valid = true; } template -void Spectrum::Private::paint(Object* object) { +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& 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 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); + partition.current = detail::prepare_curve(std::span(frame.samples).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); + if (state.max_hold_visible && frame.maxima.size() == frame.samples.size()) partition.maximum = detail::prepare_curve(std::span(frame.maxima).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); + if (state.min_hold_visible && frame.minima.size() == frame.samples.size()) partition.minimum = detail::prepare_curve(std::span(frame.minima).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 Spectrum::Private::paint_frame(Object* object) { auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.state.current); auto& cache = object->template pending_buffer(); @@ -315,51 +219,132 @@ void Spectrum::Private::paint(Object* object) { cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas_size); - if (state.sweep_region_visible) - painter.rect(prepared.sweep_region, Pen{.style = Line_Style::none}, state.sweep_region_brush); - paint_curve(painter, prepared.maximum, state.max_pen, state.max_brush); - paint_curve(painter, prepared.minimum, state.min_pen, state.min_brush); - paint_curve(painter, prepared.current, state.current_pen, state.current_brush); - painter.line(prepared.center_first, prepared.center_second, state.middle_frequency_pen); - if (prepared.maximum_valid) - painter.circle(prepared.maximum_point, 3.0, state.max_pen, Brush{state.max_pen.color, Brush_Style::solid}); - if (prepared.minimum_valid) - painter.circle(prepared.minimum_point, 3.0, state.min_pen, Brush{state.min_pen.color, Brush_Style::solid}); + if (state.sweep_region_visible && !prepared.sweep_region.empty()) painter.rect(prepared.sweep_region, Pen{.style = Line_Style::none}, state.sweep_region_brush); + for (const auto& partition : prepared.partitions) { + if (partition.clip.empty()) continue; + const auto clip = painter.scoped_clip(partition.clip); + detail::paint_curve(painter, partition.maximum, state.max_pen, state.max_brush); + detail::paint_curve(painter, partition.minimum, state.min_pen, state.min_brush); + detail::paint_curve(painter, partition.current, state.current_pen, state.current_brush); + } + for (const auto& marker : prepared.markers) { + const Pen& pen = marker.style == Marker_Style::middle ? state.middle_frequency_pen : marker.style == Marker_Style::selected ? state.selected_marker_pen : state.marker_pen; + if (pen.enabled()) painter.line(marker.first, marker.second, pen); + } + for (const auto& extreme : prepared.extremes) { + const Pen& pen = extreme.maximum ? state.max_pen : state.min_pen; + painter.circle(extreme.point, 3.0, pen, Brush{pen.color, Brush_Style::solid}); + } +} +template +void Spectrum::Private::update_samples(Object* object, std::span values) { + const Spectrum_Frame& previous = object->template current_buffer(); + Spectrum_Frame next; + next.samples.assign(values.begin(), values.end()); + next.maxima.resize(values.size()); + next.minima.resize(values.size()); + for (std::size_t index = 0; index < values.size(); ++index) { + next.maxima[index] = previous.maxima.size() == values.size() ? std::max(previous.maxima[index], values[index]) : values[index]; + next.minima[index] = previous.minima.size() == values.size() ? std::min(previous.minima[index], values[index]) : values[index]; + } + object->template pending_buffer() = std::move(next); + object->template mark_dirty(); +} +template +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 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(); + 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); + state.custom_markers.erase(closest); + if (state.selected_marker == removed) state.selected_marker = -1; + else if (state.selected_marker > removed) --state.selected_marker; + }); + object->template mark_dirty(); +} +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(); + 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; + }); + object->template mark_dirty(); +} +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 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 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 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 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; }); + 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; + 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 const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() { static const Dispatch value{ - [](Root* root, std::span values) { - auto* object = static_cast(root); - static_cast(*object->d).update_samples(object, values); - }, - [](const Root* root) { - const auto* object = static_cast(root); - const auto& data = static_cast(*object->d); - return object->template current_buffer().samples.size(); - }, - [](const Root* root) { - const auto& data = static_cast(*static_cast(root)->d); - return data.prepared.current.points.size(); - }, - [](const Root* root, double frequency, double& power) { - const auto* object = static_cast(root); - const auto& data = static_cast(*object->d); - const auto& state = static_cast(*data.state.current); - return Private::power_at(state, object->template current_buffer(), frequency, power); - } + [](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); }, + [](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; }, + [](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)]; }, + [](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); } }; return value; } template -void Spectrum::bind_private_crtp(Object* object) { - Renderable::bind_private_crtp(object); - static_cast(*object->d).dispatch = &Private::dispatch_for(); +void Spectrum::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + dispatch = &Private::dispatch_for(); } -template -void Spectrum::bind_axes(Frequency_Axis_Object* frequency_axis, Power_Axis_Object* power_axis) { - auto& private_data = static_cast(*d); - private_data.frequency_axis = Private::Axis_Binding::make(frequency_axis); - private_data.power_axis = Private::Axis_Binding::make(power_axis); +inline void Spectrum::Private::bind_render_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/Sweep_Spectrum.cpp b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp new file mode 100644 index 0000000..a7c5a7b --- /dev/null +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp @@ -0,0 +1,9 @@ +#include "Sweep_Spectrum.hpp" +namespace aethera::render_2d { +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())); } +std::size_t Sweep_Spectrum::stored_block_count() const { return static_cast(*d).dispatch->block_count(this); } +std::size_t Sweep_Spectrum::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } +std::size_t Sweep_Spectrum::rendered_point_count() const { return static_cast(*d).dispatch->rendered_point_count(this); } +} diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp new file mode 100644 index 0000000..b89b4f1 --- /dev/null +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp @@ -0,0 +1,50 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../render/Blend2D_Cache.hpp" +#include "../scene/Render_Scene_2D.hpp" +#include "Plot_Types.hpp" +#include +#include +#include +#include +#include +namespace aethera::render_2d { +struct Sweep_Spectrum_State_Tag {}; +struct Sweep_Spectrum : Def, Tagged_Buffer> { + using Scene_Object = Impl; + using Frequency_Object = Impl; + using Power_Object = Impl; + struct Prop : Prev_Prop {}; + struct State : Prev_State { + Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */ + std::size_t bins_per_block{}; /* 每个扫描块期望的功率点数;零值接受首块尺寸。 */ + std::size_t block_count{1}; /* 最多保留的扫描块数;零值按 1 处理。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式分块数。 */ + Pen pen{Color::yellow()}; /* 扫频折线样式。 */ + Pen current_frequency_pen{Color::red_color(), 2.0}; /* 当前扫频位置垂线样式。 */ + bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */ + Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */ + std::vector> blocks{}; /* 已提交扫描块的唯一权威集合。 */ + bool operator==(const State&) const; + }; + struct Private; + template + struct Builder : Prev_Builder { + using Base = Prev_Builder; + Builder(Scene_Object* scene, Frequency_Object* frequency_axis, Power_Object* power_axis); + [[nodiscard]] std::expected, Dependency_Graph_Error> build(); + private: + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ + Power_Object* power_axis{}; /* 不拥有的功率轴。 */ + }; + void append_block(std::span values); + void append_block(std::pmr::vector&& values); + template void append_block(const Values& values); + [[nodiscard]] std::size_t stored_block_count() const; + [[nodiscard]] std::size_t stored_point_count() const; + [[nodiscard]] std::size_t rendered_point_count() const; +}; +} +#include "Sweep_Spectrum.ipp" diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp new file mode 100644 index 0000000..7a10a58 --- /dev/null +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp @@ -0,0 +1,83 @@ +#pragma once +#include "common/Curve_Plot.hpp" +#include +namespace aethera::render_2d { +struct Sweep_Spectrum::Private : Prev_Private { + struct Prepared { + std::vector partitions{}; /* Prepare 子图各分块的曲线输出。 */ + std::vector values{}; /* 已提交扫描块拼接后的连续功率值。 */ + Point_F marker_first{}; /* 当前扫描位置线的首端点。 */ + Point_F marker_second{}; /* 当前扫描位置线的末端点。 */ + Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ + bool valid{}; /* 两根轴是否正交且画布有效。 */ + }; + using Append_Run = void (*)(Root*, std::span); using Count_Run = std::size_t (*)(const Root*); + struct Dispatch { + Append_Run append; /* 向最终对象提交一个扫描块。 */ + Count_Run block_count; /* 查询权威扫描块数。 */ + Count_Run point_count; /* 查询权威功率点总数。 */ + Count_Run rendered_point_count; /* 查询已准备的曲线点数。 */ + }; + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ + Power_Object* power_axis{}; /* 不拥有的功率轴。 */ + Prepared prepared{}; /* 当前块集合推导出的绘制输入。 */ + Plot_Partition_Count graph_partition_count{}; /* 当前 Prepare 子图分块数。 */ + const Dispatch* dispatch{}; /* 最终类型公开薄壳分派表。 */ + /* CRTP 覆盖:绑定 Renderable 能力和最终 Sweep_Spectrum 分派表。 */ + template void bind_private_crtp(Object* object); + 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); + /* CRTP 覆盖:构建消费曲线分块的 Paint 子图。 */ + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& 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 +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()); }); + 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); } +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; } +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; } +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; + 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); + 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(); } +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; }, + [](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; +} +template void Sweep_Spectrum::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } +inline void Sweep_Spectrum::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/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp new file mode 100644 index 0000000..2a7c828 --- /dev/null +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -0,0 +1,2 @@ +#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); } } diff --git a/render_2D/render_2D/plottable/Waterfall.hpp b/render_2D/render_2D/plottable/Waterfall.hpp new file mode 100644 index 0000000..db13e95 --- /dev/null +++ b/render_2D/render_2D/plottable/Waterfall.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../render/Blend2D_Cache.hpp" +#include "../scene/Render_Scene_2D.hpp" +#include "Hover_Tooltip.hpp" +#include "Plot_Types.hpp" +#include +#include +#include +#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> { + using Scene_Object = Impl; using Frequency_Object = Impl; using Time_Object = Impl; + struct Prop : Prev_Prop {}; + struct State : Prev_State, 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{}; /* 目标频率列数;零值使用最新行尺寸。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式分块数。 */ + bool visible_range_only{true}; /* 是否按频率轴范围裁剪。 */ + Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ + Color_Map color_map{}; /* 功率到颜色的映射。 */ + std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ + bool operator==(const State&) const; + }; + struct Private; + template struct Builder : Prev_Builder { + using Base = Prev_Builder; + Builder(Scene_Object* scene, Frequency_Object* frequency_axis, Time_Object* time_axis); + [[nodiscard]] std::expected, Dependency_Graph_Error> build(); + private: + Scene_Object* scene{}; /* 不拥有的所属 Scene;生命周期必须覆盖 Waterfall。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Waterfall。 */ + Time_Object* time_axis{}; /* 不拥有的时间轴;生命周期必须覆盖 Waterfall。 */ + }; + void append_row(Plot_Time_Tick tick, std::span values); void append_row(Plot_Time_Tick tick, std::pmr::vector&& values); void append_row(Time_Of_Day time, std::span values); void append_row(Time_Of_Day time, std::pmr::vector&& values); + template void append_row(Plot_Time_Tick tick, const Values& values); template void append_row(Time_Of_Day time, const Values& values); + [[nodiscard]] std::size_t row_count() const; [[nodiscard]] std::size_t stored_point_count() const; [[nodiscard]] std::size_t rendered_cell_count() const; +}; +} +#include "Waterfall.ipp" diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp new file mode 100644 index 0000000..79adb51 --- /dev/null +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -0,0 +1,69 @@ +#pragma once +#include "common/Curve_Plot.hpp" +#include "common/Raster_Plot.hpp" +#include +#include +#include +#include +namespace aethera::render_2d { +struct Waterfall::Private : Prev_Private { + struct Prepared { + detail::Raster_Layout layout{}; /* 可视频段与时间行组成的色块布局。 */ + std::vector pixels{}; /* 当前行集合转换后的像素矩阵。 */ + Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */ + std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */ + Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ + int source_first{}; /* 可视频段在源频谱行中的首列。 */ + bool valid{}; /* 轴布局和行数据是否足以生成色块。 */ + }; + using Append_Run = void (*)(Root*, Plot_Time_Tick, std::span); using Time_Append_Run = void (*)(Root*, Time_Of_Day, std::span); using Count_Run = std::size_t (*)(const Root*); + struct Dispatch { + Append_Run append; /* 按 tick 向最终对象提交一行。 */ + Time_Append_Run append_time; /* 按时刻分配 tick 后提交一行。 */ + Count_Run row_count; /* 查询权威行数。 */ + Count_Run point_count; /* 查询权威样本总数。 */ + Count_Run rendered_count; /* 查询已准备的色块数。 */ + }; + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴。 */ + Time_Object* time_axis{}; /* 不拥有的时间轴,同时权威决定保留行数。 */ + Prepared prepared{}; /* 当前权威状态推导出的 Paint 输入。 */ + detail::Hover_Tooltip_Runtime tooltip{}; /* 事件侧当前 hover 位置。 */ + Plot_Partition_Count graph_partition_count{}; /* Prepare 子图当前固化的分块数。 */ + const Dispatch* dispatch{}; /* 最终类型的公开薄壳分派表。 */ + /* CRTP 覆盖:绑定 Renderable、事件能力和最终 Waterfall 分派表。 */ + template void bind_private_crtp(Object* object); + 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); + /* CRTP 覆盖:构建消费色块矩阵和提示信息的 Paint 子图。 */ + template [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); + /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ + template [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& 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 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); } +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 +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; } +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); } } +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 +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; } +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/plottable/common/Curve_Plot.cpp b/render_2D/render_2D/plottable/common/Curve_Plot.cpp new file mode 100644 index 0000000..68270e7 --- /dev/null +++ b/render_2D/render_2D/plottable/common/Curve_Plot.cpp @@ -0,0 +1,92 @@ +#include "Curve_Plot.hpp" +#include +#include +#include +namespace aethera::render_2d::detail { +namespace { +Plot_Value power_domain_lerp(Plot_Value first, Plot_Value second, Plot_Ratio ratio) { + const Plot_Value first_power = std::pow(10.0, std::clamp(first, -3'000.0, 3'000.0) / 10.0); + const Plot_Value second_power = std::pow(10.0, std::clamp(second, -3'000.0, 3'000.0) / 10.0); + return 10.0 * std::log10(std::max(first_power + (second_power - first_power) * ratio, 1e-300)); +} +Plot_Value cubic_value(Plot_Value previous, Plot_Value first, Plot_Value second, Plot_Value next, Plot_Ratio ratio) { + const Plot_Ratio ratio2 = ratio * ratio; + const Plot_Ratio ratio3 = ratio2 * ratio; + return 0.5 * ((2.0 * first) + (-previous + second) * ratio + (2.0 * previous - 5.0 * first + 4.0 * second - next) * ratio2 + (-previous + 3.0 * first - 3.0 * second + next) * ratio3); +} +} +Plot_Partition_Count curve_partition_count(Plot_Partition_Mode mode, Plot_Partition_Count requested_count, std::size_t sample_count) { + const Plot_Partition_Count segment_count = sample_count > 1 ? sample_count - 1 : 1; + if (mode == Plot_Partition_Mode::fixed) return std::clamp(requested_count, Plot_Partition_Count{1}, segment_count); + const Plot_Partition_Count worker_count = std::max(1, std::thread::hardware_concurrency()); + return std::min({segment_count, worker_count, std::max(1, (segment_count + 127) / 128)}); +} +Curve_Partition_Range curve_partition_range(std::size_t sample_count, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count, Axis_Range domain) { + if (sample_count == 0 || partition_count == 0) return {}; + const std::size_t segment_count = sample_count > 1 ? sample_count - 1 : 1; + const std::size_t first_sample = std::min(segment_count * partition_index / partition_count, sample_count - 1); + const std::size_t last_sample = std::min(segment_count * (partition_index + 1) / partition_count, sample_count - 1); + const Plot_Ratio denominator = sample_count > 1 ? static_cast(sample_count - 1) : 1.0; + return {first_sample, last_sample - first_sample + 1, {domain.origin + domain.length() * static_cast(first_sample) / denominator, domain.origin + domain.length() * static_cast(last_sample) / denominator}}; +} +std::vector interpolate_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode) { + std::vector result; + if (values.empty()) return result; + if (values.size() == 1) return {{domain.origin, values.front()}}; + constexpr int subdivisions = 4; + const Plot_Ratio denominator = static_cast(values.size() - 1); + const auto coordinate = [domain, denominator](std::size_t index) { return domain.origin + domain.length() * static_cast(index) / denominator; }; + result.push_back({coordinate(0), values.front()}); + for (std::size_t index = 0; index + 1 < values.size(); ++index) { + const Plot_Coordinate first_coordinate = coordinate(index); + const Plot_Coordinate second_coordinate = coordinate(index + 1); + const Plot_Value first = values[index]; + const Plot_Value second = values[index + 1]; + switch (mode) { + case Line_Interpolation_Mode::nearest_sample: { const Plot_Coordinate middle = (first_coordinate + second_coordinate) * 0.5; result.push_back({middle, first}); result.push_back({middle, second}); result.push_back({second_coordinate, second}); break; } + case Line_Interpolation_Mode::linear_value: result.push_back({second_coordinate, second}); break; + case Line_Interpolation_Mode::linear_power_domain: for (int part = 1; part <= subdivisions; ++part) { const Plot_Ratio ratio = static_cast(part) / subdivisions; result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, power_domain_lerp(first, second, ratio)}); } break; + case Line_Interpolation_Mode::step_left: result.push_back({second_coordinate, first}); result.push_back({second_coordinate, second}); break; + case Line_Interpolation_Mode::step_right: result.push_back({first_coordinate, second}); result.push_back({second_coordinate, second}); break; + case Line_Interpolation_Mode::cubic_value: { const Plot_Value previous = values[index == 0 ? 0 : index - 1]; const Plot_Value next = values[std::min(index + 2, values.size() - 1)]; for (int part = 1; part <= subdivisions; ++part) { const Plot_Ratio ratio = static_cast(part) / subdivisions; result.push_back({first_coordinate + (second_coordinate - first_coordinate) * ratio, cubic_value(previous, first, second, next, ratio)}); } break; } + } + } + return result; +} +std::vector visible_curve_samples(std::vector samples, Axis_Range visible_range) { + const auto [low, high] = std::minmax(visible_range.origin, visible_range.target); + if (samples.size() < 2) return !samples.empty() && samples.front().coordinate >= low && samples.front().coordinate <= high ? std::move(samples) : std::vector{}; + std::size_t first = samples.size(); + std::size_t last{}; + for (std::size_t index = 0; index + 1 < samples.size(); ++index) { const auto [segment_low, segment_high] = std::minmax(samples[index].coordinate, samples[index + 1].coordinate); if (segment_high < low || segment_low > high) continue; first = std::min(first, index); last = std::max(last, index + 1); } + if (first == samples.size()) return {}; + return {samples.begin() + static_cast(first), samples.begin() + static_cast(last + 1)}; +} +Point_F map_plot_point(const Abs_Axis* coordinate_axis, Plot_Coordinate coordinate, const Abs_Axis* value_axis, Plot_Value value, Axis_Orientation coordinate_orientation) { + const Axis_Pixel_Position coordinate_pixel = coordinate_axis->coordinate_to_pixel(coordinate); + const Axis_Pixel_Position value_pixel = value_axis->coordinate_to_pixel(value); + return coordinate_orientation == Axis_Orientation::horizontal ? Point_F{coordinate_pixel, value_pixel} : Point_F{value_pixel, coordinate_pixel}; +} +Rect_F map_plot_rect(const Abs_Axis* coordinate_axis, Axis_Range coordinate_range, const Abs_Axis* value_axis, Axis_Range value_range, Axis_Orientation coordinate_orientation) { + const Point_F first = map_plot_point(coordinate_axis, coordinate_range.origin, value_axis, value_range.origin, coordinate_orientation); + const Point_F second = map_plot_point(coordinate_axis, coordinate_range.target, value_axis, value_range.target, coordinate_orientation); + return Rect_F{first.x, first.y, second.x - first.x, second.y - first.y}.normalized(); +} +Curve_Prepared prepare_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation) { + Curve_Prepared result; + auto samples = interpolate_curve(values, domain, mode); + if (visible_only) samples = visible_curve_samples(std::move(samples), visible_coordinate_range); + for (const auto& sample : samples) if (std::isfinite(sample.coordinate) && std::isfinite(sample.value)) result.points.push_back(map_plot_point(coordinate_axis, sample.coordinate, value_axis, sample.value, coordinate_orientation)); + if (result.points.size() < 2) return result; + const Axis_Pixel_Position baseline = value_axis->coordinate_to_pixel(value_range.target); + Point_F first = result.points.front(); Point_F last = result.points.back(); + if (value_orientation == Axis_Orientation::horizontal) { first.x = baseline; last.x = baseline; } else { first.y = baseline; last.y = baseline; } + result.fill.reserve(result.points.size() + 2); result.fill.push_back(first); result.fill.insert(result.fill.end(), result.points.begin(), result.points.end()); result.fill.push_back(last); + return result; +} +void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush) { + if (curve.points.size() < 2) return; + if (brush.enabled()) painter.polygon(curve.fill, Pen{.style = Line_Style::none}, brush); + painter.polyline(curve.points, pen); +} +} diff --git a/render_2D/render_2D/plottable/common/Curve_Plot.hpp b/render_2D/render_2D/plottable/common/Curve_Plot.hpp new file mode 100644 index 0000000..1dcecaa --- /dev/null +++ b/render_2D/render_2D/plottable/common/Curve_Plot.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "../../axis/Axis.hpp" +#include "../../render/Blend2D_Cache.hpp" +#include "../Plot_Types.hpp" +#include +#include +namespace aethera::render_2d::detail { +struct Curve_Sample { + Plot_Coordinate coordinate{}; /* 曲线样本的横向业务坐标。 */ + Plot_Value value{}; /* 曲线样本的纵向业务值。 */ +}; +struct Curve_Prepared { + std::vector points{}; /* 映射到画布后的折线点。 */ + std::vector fill{}; /* 含值轴基线闭合点的填充多边形。 */ +}; +struct Curve_Partition_Range { + std::size_t first_sample{}; /* 分块包含的首个样本下标。 */ + std::size_t sample_count{}; /* 分块包含的样本数;相邻块共享边界样本。 */ + Axis_Range domain{}; /* 分块样本对应的业务坐标范围。 */ +}; +[[nodiscard]] Plot_Partition_Count curve_partition_count(Plot_Partition_Mode mode, Plot_Partition_Count requested_count, std::size_t sample_count); +[[nodiscard]] Curve_Partition_Range curve_partition_range(std::size_t sample_count, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count, Axis_Range domain); +[[nodiscard]] std::vector interpolate_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode); +[[nodiscard]] std::vector visible_curve_samples(std::vector samples, Axis_Range visible_range); +[[nodiscard]] Point_F map_plot_point(const Abs_Axis* coordinate_axis, Plot_Coordinate coordinate, const Abs_Axis* value_axis, Plot_Value value, Axis_Orientation coordinate_orientation); +[[nodiscard]] Rect_F map_plot_rect(const Abs_Axis* coordinate_axis, Axis_Range coordinate_range, const Abs_Axis* value_axis, Axis_Range value_range, Axis_Orientation coordinate_orientation); +[[nodiscard]] Curve_Prepared prepare_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation); +void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush = {}); +} diff --git a/render_2D/render_2D/plottable/common/Raster_Plot.cpp b/render_2D/render_2D/plottable/common/Raster_Plot.cpp new file mode 100644 index 0000000..fece53f --- /dev/null +++ b/render_2D/render_2D/plottable/common/Raster_Plot.cpp @@ -0,0 +1,45 @@ +#include "Raster_Plot.hpp" +#include "Curve_Plot.hpp" +#include +namespace aethera::render_2d::detail { +bool Raster_Layout::valid() const noexcept { return width > 0 && height > 0 && !target.empty(); } +int Raster_Axis_Selection::count() const noexcept { return last - first + 1; } +std::size_t Raster_Layout::index(int first, int second) const noexcept { + if (first_reversed) first = (first_horizontal ? width : height) - 1 - first; + if (second_reversed) second = (first_horizontal ? height : width) - 1 - second; + const int x = first_horizontal ? first : second; + const int y = first_horizontal ? second : first; + return static_cast(y) * static_cast(width) + static_cast(x); +} +Plot_Ratio normalized_plot_value(Plot_Value value, Axis_Range range) { + return range.length() == 0.0 ? 0.0 : std::clamp((value - range.origin) / range.length(), 0.0, 1.0); +} +std::optional raster_axis_selection(Axis_Range data_range, Axis_Range visible_range, int source_count, bool visible_only) { + if (source_count <= 0) return std::nullopt; + if (!visible_only || source_count == 1 || data_range.length() == 0.0) return Raster_Axis_Selection{0, source_count - 1, data_range}; + const auto [data_low, data_high] = std::minmax(data_range.origin, data_range.target); + const auto [visible_low, visible_high] = std::minmax(visible_range.origin, visible_range.target); + const Axis_Coordinate clipped_low = std::max(data_low, visible_low); + const Axis_Coordinate clipped_high = std::min(data_high, visible_high); + if (clipped_low > clipped_high) return std::nullopt; + const auto position = [data_range, source_count](Axis_Coordinate coordinate) { return (coordinate - data_range.origin) / data_range.length() * static_cast(source_count - 1); }; + const auto [position_low, position_high] = std::minmax(position(clipped_low), position(clipped_high)); + int first = std::clamp(static_cast(std::floor(position_low)), 0, source_count - 1); + int last = std::clamp(static_cast(std::ceil(position_high)), first, source_count - 1); + if (first == last) { if (last + 1 < source_count) ++last; else if (first > 0) --first; } + const auto coordinate = [data_range, source_count](int index) { return data_range.origin + data_range.length() * static_cast(index) / static_cast(source_count - 1); }; + return Raster_Axis_Selection{first, last, {coordinate(first), coordinate(last)}}; +} +Raster_Layout raster_layout(const Abs_Axis* first_axis, Axis_Range first_range, int first_count, const Abs_Axis* second_axis, Axis_Range second_range, int second_count, Axis_Orientation first_orientation, Axis_Orientation second_orientation) { + if (first_orientation == second_orientation || first_count <= 0 || second_count <= 0) return {}; + const bool horizontal = first_orientation == Axis_Orientation::horizontal; + return {horizontal ? first_count : second_count, horizontal ? second_count : first_count, map_plot_rect(first_axis, first_range, second_axis, second_range, first_orientation), first_axis->coordinate_to_pixel(first_range.origin) > first_axis->coordinate_to_pixel(first_range.target), second_axis->coordinate_to_pixel(second_range.origin) > second_axis->coordinate_to_pixel(second_range.target), horizontal}; +} +std::pair raster_partition_range(std::size_t work_size, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count) { + if (partition_count == 0) return {}; + return {work_size * partition_index / partition_count, work_size * (partition_index + 1) / partition_count}; +} +void paint_raster(Painter& painter, const Raster_Layout& layout, std::span pixels, Image_Interpolation_Mode interpolation) { + if (layout.valid() && pixels.size() == static_cast(layout.width) * static_cast(layout.height)) painter.heatmap(layout.target, layout.width, layout.height, pixels, interpolation); +} +} diff --git a/render_2D/render_2D/plottable/common/Raster_Plot.hpp b/render_2D/render_2D/plottable/common/Raster_Plot.hpp new file mode 100644 index 0000000..328e17d --- /dev/null +++ b/render_2D/render_2D/plottable/common/Raster_Plot.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "../../axis/Axis.hpp" +#include "../../render/Blend2D_Cache.hpp" +#include "../Plot_Types.hpp" +#include +#include +#include +#include +namespace aethera::render_2d::detail { +struct Raster_Layout { + int width{}; /* 像素矩阵宽度。 */ + int height{}; /* 像素矩阵高度。 */ + Rect_F target{}; /* 栅格映射到画布的目标矩形。 */ + bool first_reversed{}; /* 第一坐标轴是否按像素反向。 */ + bool second_reversed{}; /* 第二坐标轴是否按像素反向。 */ + bool first_horizontal{}; /* 第一坐标轴是否映射到矩阵水平方向。 */ + [[nodiscard]] bool valid() const noexcept; + [[nodiscard]] std::size_t index(int first, int second) const noexcept; +}; +struct Raster_Axis_Selection { + int first{}; /* 源数据中首个被选中的格点下标。 */ + int last{}; /* 源数据中最后一个被选中的格点下标。 */ + Axis_Range range{}; /* 选中格点覆盖的业务坐标范围。 */ + [[nodiscard]] int count() const noexcept; +}; +[[nodiscard]] Plot_Ratio normalized_plot_value(Plot_Value value, Axis_Range range); +[[nodiscard]] std::optional raster_axis_selection(Axis_Range data_range, Axis_Range visible_range, int source_count, bool visible_only); +[[nodiscard]] Raster_Layout raster_layout(const Abs_Axis* first_axis, Axis_Range first_range, int first_count, const Abs_Axis* second_axis, Axis_Range second_range, int second_count, Axis_Orientation first_orientation, Axis_Orientation second_orientation); +[[nodiscard]] std::pair raster_partition_range(std::size_t work_size, Plot_Partition_Count partition_index, Plot_Partition_Count partition_count); +void paint_raster(Painter& painter, const Raster_Layout& layout, std::span pixels, Image_Interpolation_Mode interpolation); +} diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 686907c..326ad77 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -1,5 +1,7 @@ #include "Render_Scene_2D.hpp" namespace aethera::render_2d { +bool Render_Scene_2D::State::operator==(const State&) 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 25c3b41..a4392fb 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -18,7 +18,7 @@ struct Render_Scene_2D : Def - void bind_private_crtp(Object* object); }; } #include "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 bea4d9a..0870b6e 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -23,6 +23,9 @@ struct Render_Scene_2D::Private : Prev_Private { void dispatch_event(Object* object, const Event& event); template [[nodiscard]] static const Dispatch& dispatch_for(); + /* CRTP 覆盖:Builder 挂接最终 Private 后安装二维 Scene 的无虚函数业务分派。 */ + template + void bind_private_crtp(Object* object); }; template void Render_Scene_2D::Private::process(Object* object, Callback&& callback) @@ -100,7 +103,8 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for return value; } template -void Render_Scene_2D::bind_private_crtp(Object* object) { - static_cast(*object->d).dispatch = &Private::dispatch_for(); +void Render_Scene_2D::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + dispatch = &Private::dispatch_for(); } } diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp new file mode 100644 index 0000000..0c27be8 --- /dev/null +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -0,0 +1,132 @@ +#include +#include +#include +#include + +namespace { +using namespace aethera; +using namespace aethera::render_2d; + +template +std::unique_ptr build_object(Args&&... args) { + typename Object::Builder builder(std::forward(args)...); + auto result = builder.build(); + if (!result) std::terminate(); + return std::move(result).value(); +} + +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); +} +} + +TEST(plottable_migration, curve_plots_share_partitioned_rendering) { + using Scene = Impl; + using Frequency = Impl; + using Numeric = Impl; + using Time = Impl; + using Trace = Impl; + using Sweep = Impl; + initialize_runtime(2); + const Size canvas{160, 120}; + auto scene = build_object(); + auto frequency = build_object(); + auto power = build_object(); + auto time = build_object