diff --git a/Project_detail_specification.md b/Project_detail_specification.md index cb06e64..2c91c9a 100644 --- a/Project_detail_specification.md +++ b/Project_detail_specification.md @@ -14,6 +14,8 @@ * `Prop` 保存外部可读写的业务属性,`State` 保存实现向外发布的运行结果,`Private` 保存实现细节;三者不得互相复制并手工同步。 * 每个 `Def` 定义层自动以自身类型生成 `Base_Tag`;Prop、State、Private 分别在隔离的标签空间中复用该标签,禁止再声明 `XXX_Prop_Tag`、`XXX_State_Tag` 或 `XXX_Private_Tag`。 * 依赖可以选择 Prop/State 的单字段或整个 `Base_Tag` 层;字段写入必须同时发出字段级和所属层级变更,使用方按实际重建粒度选择一种依赖。 +* 整体对象依赖只表达依赖图中的拓扑顺序,不传播 dirty;准备顺序、绘图顺序等业务含义由各自 Tag 解释。具体字段依赖才用于对应 Tag 的 dirty 传播,例如绘图缓存失效。 +* Kernel `Scene` 只负责 2D/3D 共有的 Prepare 数据阶段;Paint、缓存失效、像素合成和异步后端提交由对应渲染模块自己的 Scene、Tag 与 Taskflow 负责。 * `Root` 只保存一个最终 `Private` 指针;`Builder::build()` 校验成功后创建并挂接完整 Private,`Root` 通过公共 Private 基类的虚析构统一释放。禁止直接公开该指针。 * 能从权威结构查询或计算的数据即时获取,不保存为成员。类只保存自身职责需要且无法推导的状态,并检查每个新增成员的读写者和生命周期。 * 公共接口只表达业务语义,不暴露 `Private`、内部指针、线程状态或缓冲区角色;接口保持正交,不增加空配置、未完成接口、无消费者统计或只做转发的 getter/setter。 diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp index 8329242..1c042a0 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph.hpp @@ -1,7 +1,7 @@ #pragma once #include "mechanism.hpp" namespace double_buffer { -// Dependency_Graph 是有向无环依赖图。Node 保存对象和直接前驱,Edge 额外记录触发 dirty 的来源键,用于状态/Buffer/阶段脏标记传播。 +// Dependency_Graph 是有向无环依赖图。Node 保存对象和直接前驱;Edge 可选携带 dirty 来源键,整体对象边只参与拓扑排序。 struct Dependency_Graph { using Error = Dependency_Graph_Error; struct Node { @@ -157,6 +157,11 @@ public: void disconnect(Root* object) { edit_dependency_graph->disconnect_impl(object); } + /* 添加整个对象依赖;只形成拓扑边,不注册 dirty 传播。 */ + template Target, detail::Dependency_Object Source> + Node* add_dependency(Target* target, Source* source) { + return edit_dependency_graph->template add_dependency_runtime(target, source, nullptr, nullptr, nullptr, bind_node_data); + } /* 添加 Prop 字段级依赖;source 通过 set 修改该字段时目标阶段变脏。 */ template Target, detail::Prop_Dependency_Source Source> Node* add_prop_dependency(Target* target, Source* source) { @@ -295,6 +300,7 @@ private: void rebuild_dirty_edges() { dirty_edges.clear(); for (const auto& edge : edges) { + if (!edge.source_key || !edge.target_tag || !edge.target_dirty_key) continue; Dirty_Source source{edge.source, edge.source_key}; auto [first, last] = dirty_edges.equal_range(source); auto current = std::find_if( diff --git a/kernel/src/kernel/render_common.hpp b/kernel/src/kernel/render_common.hpp index 3805790..48d28d8 100644 --- a/kernel/src/kernel/render_common.hpp +++ b/kernel/src/kernel/render_common.hpp @@ -26,8 +26,6 @@ using double_buffer::Dependency_Graph; using double_buffer::Dependency_Graph_Error; /* Prepare 数据阶段在 Dependency_Graph 图中的标签。 */ struct Prepare_Data_Tag {}; -/* Paint 阶段在 Dependency_Graph 图中的标签。 */ -struct Paint_Tag {}; /* 全局 Taskflow 运行时状态标签,用于注册状态回调。 */ struct Task_Runtime_State_Tag {}; /* 单一 Taskflow 任务类型的累计统计。 */ diff --git a/kernel/src/kernel/renderable.hpp b/kernel/src/kernel/renderable.hpp index 7c3bcc3..ae108a9 100644 --- a/kernel/src/kernel/renderable.hpp +++ b/kernel/src/kernel/renderable.hpp @@ -71,7 +71,7 @@ struct Renderable : Def { /* Renderable 每次 Scene 执行后发布的阶段状态与统计。 */ struct State : Prev_State { bool prepare_dirty{}; /* Prepare 条件判断时观察到的 Prepare_Data_Tag dirty 状态。 */ - bool paint_dirty{}; /* Paint 条件判断时观察到的 Paint_Tag dirty 状态。 */ + bool paint_dirty{}; /* 专用渲染阶段条件判断时观察到的 dirty 状态。 */ bool prepare_executed{}; /* 本次 Scene 执行是否运行了 Prepare 数据函数或子图。 */ bool paint_executed{}; /* 本次 Scene 执行是否运行了 Paint 数据函数或子图。 */ bool prepare_graph_rebuilt{}; /* 本次 Prepare 条件判断是否重建了 Prepare 子图。 */ diff --git a/kernel/src/kernel/scene.cpp b/kernel/src/kernel/scene.cpp index 64bdc7b..8236aae 100644 --- a/kernel/src/kernel/scene.cpp +++ b/kernel/src/kernel/scene.cpp @@ -1,4 +1,4 @@ -#include "scene.hpp" +#include "scene.hpp" /* 后端共有 Prepare Scene 实现。 */ namespace aethera { Scene::Private::Private() : runtime(std::make_unique()) {} Scene::Private::~Private() { diff --git a/kernel/src/kernel/scene.hpp b/kernel/src/kernel/scene.hpp index ca31d40..c5acd5e 100644 --- a/kernel/src/kernel/scene.hpp +++ b/kernel/src/kernel/scene.hpp @@ -3,21 +3,21 @@ namespace aethera { /* Scene 状态标签,用于访问和订阅 Scene::State。 */ /* - * Scene 汇总 Prepare/Paint 两张 Dependency_Graph 图并构建总 Taskflow。 + * Scene 只汇总跨渲染后端共有的 Prepare 数据依赖图并构建 Taskflow。 * 用户最终通过 Impl 创建可使用实例;编辑 Dependency_Graph 后调用 advance() 提交结构变化,再调用 process(...) 执行当前场景。 */ -struct Scene : Def, Dependency_Graph_Type> { +struct Scene : Def> { /* Scene 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */ struct Prop : Prev_Prop {}; /* Scene 每次 process(...) 后发布的总图结构与执行统计。 */ struct State : Prev_State { - bool taskflow_rebuilt{}; /* 本次 process(...) 前的 advance 是否重新构建了总 Taskflow。 */ - std::size_t renderable_count{}; /* Prepare/Paint 两张依赖图中去重后的 Renderable 数量。 */ - std::size_t taskflow_task_count{}; /* 当前总 Taskflow 中的任务节点数量。 */ - std::size_t taskflow_dependency_count{}; /* 当前总 Taskflow 中的直接依赖边数量。 */ - std::size_t taskflow_max_predecessors{}; /* 当前总 Taskflow 中单个任务的最大直接前驱数量。 */ - std::size_t taskflow_max_successors{}; /* 当前总 Taskflow 中单个任务的最大直接后继数量。 */ - std::uint64_t taskflow_execution_time_ns{}; /* 本次 process(...) 执行总 Taskflow 的耗时,单位为纳秒;没有任务时为 0。 */ + bool taskflow_rebuilt{}; /* 本次 process(...) 前的 advance 是否重新构建了总 Taskflow。 */ + std::size_t renderable_count{}; /* Prepare 数据依赖图中的 Renderable 数量。 */ + std::size_t taskflow_task_count{}; /* 当前 Prepare Taskflow 中的任务节点数量。 */ + std::size_t taskflow_dependency_count{}; /* 当前 Prepare Taskflow 中的直接依赖边数量。 */ + std::size_t taskflow_max_predecessors{}; /* 当前 Prepare Taskflow 中单个任务的最大直接前驱数量。 */ + std::size_t taskflow_max_successors{}; /* 当前 Prepare Taskflow 中单个任务的最大直接后继数量。 */ + std::uint64_t taskflow_execution_time_ns{}; /* 本次 process(...) 执行 Prepare Taskflow 的耗时,单位为纳秒;没有任务时为 0。 */ /* 支持测试、快照比较和变更检测的逐字段相等比较。 */ bool operator==(const State&) const = default; }; diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index 82da1f9..bce790c 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -5,9 +5,9 @@ #include namespace aethera { struct Scene::Private : Prev_Private { - struct Result {}; /* process(...) 完成回调的结果类型;当前仅表示完成。 */ - struct Runtime; /* Scene 的 Taskflow 构建产物;完整定义位于本文件下方。 */ - std::unique_ptr runtime; /* Scene 唯一运行时构建产物的所有权。 */ + struct Result {}; /* process(...) 完成回调的结果类型;当前仅表示完成。 */ + struct Runtime; /* Scene 的 Taskflow 构建产物;完整定义位于本文件下方。 */ + std::unique_ptr runtime; /* Scene 唯一运行时构建产物的所有权。 */ Private(); ~Private(); /* Def CRTP hook:所有缓冲推进后重建必要的总 Taskflow,并更新 Scene_State_Tag 状态层。 */ @@ -23,7 +23,7 @@ struct Scene::Private : Prev_Private { /* 派生 Scene 的 Private 还可覆盖 Def::Private 的四个 State 生命周期 hook,并通过 State_Access::get() 访问状态层。 */ }; struct Scene::Private::Runtime { - std::unique_ptr taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */ + std::unique_ptr taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */ }; template void Scene::Private::process(Object* object, Callback&& callback) requires std::invocable { @@ -56,41 +56,29 @@ void Scene::Private::after_advance(Object* object, } ); bool taskflow_dirty = !runtime->taskflow; - object->template access_pending_dependency_graph( - [&](auto& prepare_state, auto& paint_state) { - taskflow_dirty = taskflow_dirty || prepare_state.dirty() || paint_state.dirty(); - } - ); + object->template access_pending_dependency_graph( + [&](auto& prepare_state) { taskflow_dirty = taskflow_dirty || prepare_state.dirty(); }); if (!taskflow_dirty) return; if (!runtime->taskflow) runtime->taskflow = std::make_unique(); auto& taskflow = *runtime->taskflow; auto prepare_dependencies = object->template current_dependency_graph(); - auto paint_dependencies = object->template current_dependency_graph(); std::pmr::unordered_set renderables{resource}; prepare_dependencies.for_each_bound( [&](Renderable* renderable, Renderable::Private&) { renderables.insert(renderable); } ); - paint_dependencies.for_each_bound( - [&](Renderable* renderable, Renderable::Private&) { - renderables.insert(renderable); - } - ); scene_state.renderable_count = renderables.size(); detail::clear_stage_observers(&taskflow); taskflow.clear(); struct Stage_Tasks { - tf::Task prepare_entry; /* Prepare 条件任务,作为该阶段依赖入口。 */ - tf::Task prepare_exit; /* Prepare 完成任务,作为该阶段依赖出口。 */ - tf::Task paint_entry; /* Paint 条件任务,作为该阶段依赖入口。 */ - tf::Task paint_exit; /* Paint 完成任务,作为该阶段依赖出口。 */ + tf::Task prepare_entry; /* Prepare 条件任务,作为该阶段依赖入口。 */ + tf::Task prepare_exit; /* Prepare 完成任务,作为该阶段依赖出口。 */ }; std::pmr::unordered_map stage_tasks{resource}; for (auto* renderable : renderables) { Root* root = renderable; auto* data = prepare_dependencies.private_data(root); - if (!data) data = paint_dependencies.private_data(root); if (!data) continue; auto* dispatch = data->dispatch; auto prepare_if = taskflow.emplace([data, dispatch, root] { @@ -127,59 +115,16 @@ void Scene::Private::after_advance(Object* object, } auto prepare_done = taskflow.emplace([dispatch, root] { auto& state = *dispatch->state.get(root); - if (!state.prepare_executed) return; - root->template take_dirty(); - root->template mark_dirty(); + if (state.prepare_executed) root->template take_dirty(); + dispatch->state.notify(root); }).name("renderable.prepare.complete"); prepare_if.precede(prepare_run, prepare_done); prepare_run.precede(prepare_done); - auto paint_if = taskflow.emplace([data, dispatch, root] { - auto& state = *dispatch->state.get(root); - state.paint_graph_rebuilt = false; - state.paint_execution_time_ns = 0; - if (dispatch->paint.builder) { - bool rebuild = dispatch->paint.rebuild_predicate(root); - if (!data->paint_graph_built || rebuild) { - *data->paint_graph = dispatch->paint.builder(root); - data->paint_graph_built = true; - state.paint_graph_rebuilt = true; - root->template mark_dirty(); - } - state.paint_task_count = data->paint_graph->num_tasks(); - } - else { - state.paint_task_count = 1; - } - bool dirty = root->template dirty(); - state.paint_dirty = dirty; - state.paint_executed = dispatch->paint.predicate(root, dirty); - return state.paint_executed ? 0 : 1; - }).name("renderable.paint.condition"); - tf::Task paint_run; - if (dispatch->paint.builder) { - if (!data->paint_graph) data->paint_graph = std::make_unique(); - paint_run = taskflow.composed_of(*data->paint_graph).name("renderable.paint.graph"); - } - else { - paint_run = taskflow.emplace([dispatch, root] { - dispatch->paint.run(root); - }).name("renderable.paint.data"); - } - auto paint_done = taskflow.emplace([dispatch, root] { - auto& state = *dispatch->state.get(root); - if (state.paint_executed) root->template take_dirty(); - dispatch->state.notify(root); - }).name("renderable.paint.complete"); - paint_if.precede(paint_run, paint_done); - paint_run.precede(paint_done); - prepare_done.precede(paint_if); detail::bind_stage_observer(&taskflow, prepare_if.hash_value(), root, data, detail::Renderable_Stage::Prepare, detail::Stage_Observer_Point::Begin); detail::bind_stage_observer(&taskflow, prepare_done.hash_value(), root, data, detail::Renderable_Stage::Prepare, detail::Stage_Observer_Point::End); - detail::bind_stage_observer(&taskflow, paint_if.hash_value(), root, data, detail::Renderable_Stage::Paint, detail::Stage_Observer_Point::Begin); - detail::bind_stage_observer(&taskflow, paint_done.hash_value(), root, data, detail::Renderable_Stage::Paint, detail::Stage_Observer_Point::End); - stage_tasks.emplace(root, Stage_Tasks{prepare_if, prepare_done, paint_if, paint_done}); + stage_tasks.emplace(root, Stage_Tasks{prepare_if, prepare_done}); } - auto connect_dependencies = [&](const auto& dependency_graph, bool prepare) { + auto connect_dependencies = [&](const auto& dependency_graph) { dependency_graph.for_each( [&](const Dependency_Graph::Node& dependency_node) { if (!dependency_graph.private_data(dependency_node)) return; @@ -195,9 +140,7 @@ void Scene::Private::after_advance(Object* object, if (dependency_graph.private_data(*dependency)) { auto source = stage_tasks.find(dependency->object); if (source != stage_tasks.end()) { - auto source_task = prepare ? source->second.prepare_exit : source->second.paint_exit; - auto target_task = prepare ? target->second.prepare_entry : target->second.paint_entry; - source_task.precede(target_task); + source->second.prepare_exit.precede(target->second.prepare_entry); } continue; } @@ -206,8 +149,7 @@ void Scene::Private::after_advance(Object* object, } ); }; - connect_dependencies(prepare_dependencies, true); - connect_dependencies(paint_dependencies, false); + connect_dependencies(prepare_dependencies); scene_state.taskflow_task_count = taskflow.num_tasks(); scene_state.taskflow_dependency_count = 0; scene_state.taskflow_max_predecessors = 0; @@ -219,12 +161,8 @@ void Scene::Private::after_advance(Object* object, scene_state.taskflow_max_successors = std::max(scene_state.taskflow_max_successors, task.num_successors()); } ); - object->template access_pending_dependency_graph( - [](auto& prepare_state, auto& paint_state) { - if (prepare_state.dirty()) prepare_state.take_dirty(); - if (paint_state.dirty()) paint_state.take_dirty(); - } - ); + object->template access_pending_dependency_graph( + [](auto& prepare_state) { if (prepare_state.dirty()) prepare_state.take_dirty(); }); scene_state.taskflow_rebuilt = true; } } diff --git a/kernel/src/test/Dependency_Graph_Test.cpp b/kernel/src/test/Dependency_Graph_Test.cpp index 2dce4ce..d2d50cd 100644 --- a/kernel/src/test/Dependency_Graph_Test.cpp +++ b/kernel/src/test/Dependency_Graph_Test.cpp @@ -97,6 +97,22 @@ TEST(dependency_graph, topological_order_and_state_dirty_propagation) { source->update_state<&Node_Object::State::value>(31); EXPECT_TRUE(target->dirty()); } +TEST(dependency_graph, whole_object_dependency_only_controls_topology) { + auto source = build_object(); + auto target = build_object(); + auto graph = build_object(); + ASSERT_TRUE(graph->edit_dependency_graph([&](auto& editor) { editor.add_dependency(target.get(), source.get()); }).has_value()); + graph->advance(); + std::vector order; + ASSERT_TRUE(graph->current_dependency_graph().for_each_topological_view([&](const auto&, const auto& node) { order.push_back(node.object); }).has_value()); + ASSERT_EQ(order.size(), 2u); + EXPECT_EQ(order[0], source.get()); + EXPECT_EQ(order[1], target.get()); + source->set<&Node_Object::Prop::value>(1); + source->update_state<&Node_Object::State::value>(1); + source->mark_dirty(); + EXPECT_FALSE(target->dirty()); +} TEST(dependency_graph, state_dependency_granularity_is_selectable) { auto member_source = build_object(); auto member_target = build_object(); diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index d5fe943..19f587e 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -85,12 +85,7 @@ std::unique_ptr build_object() { } template void add_renderable(Scene& scene, Renderable* renderable) { - ASSERT_TRUE((scene.edit_dependency_graph( - [&](auto& prepare, auto& paint) { - prepare.add(renderable); - paint.add(renderable); - } - ).has_value())); + ASSERT_TRUE(scene.edit_dependency_graph([&](auto& prepare) { prepare.add(renderable); }).has_value()); } } TEST(renderable_capability, direct_stages_do_not_allocate_subgraphs) { @@ -102,12 +97,12 @@ TEST(renderable_capability, direct_stages_do_not_allocate_subgraphs) { auto& data = renderable->data_for_test(); auto& base = static_cast(data); EXPECT_EQ(data.prepare_calls, 1); - EXPECT_EQ(data.paint_calls, 1); + EXPECT_EQ(data.paint_calls, 0); EXPECT_EQ(base.prepare_graph, nullptr); EXPECT_EQ(base.paint_graph, nullptr); scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_calls, 1); - EXPECT_EQ(data.paint_calls, 1); + EXPECT_EQ(data.paint_calls, 0); } TEST(renderable_capability, graph_stage_builds_lazily_and_rebuilds_inside_condition) { aethera::initialize_runtime(2); @@ -141,7 +136,7 @@ TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundarie renderable->set_state_callback([&](const auto& state) { ++renderable_updates; EXPECT_TRUE(state.prepare_executed); - EXPECT_TRUE(state.paint_executed); + EXPECT_FALSE(state.paint_executed); }); scene->set_state_callback([&](const auto& state) { ++scene_updates; @@ -190,13 +185,11 @@ TEST(scene_condition, upstream_change_makes_downstream_run_in_same_taskflow) { auto source = build_object(); auto target = build_object(); auto scene = build_object(); - ASSERT_TRUE((scene->edit_dependency_graph( - [&](auto& prepare, auto& paint) { + ASSERT_TRUE((scene->edit_dependency_graph( + [&](auto& prepare) { prepare.add(source.get()); prepare.add(target.get()); prepare.template add_dependency<&Dependency_Renderable::State::revision>(target.get(), source.get()); - paint.add(source.get()); - paint.add(target.get()); } ).has_value())); scene->process([](const auto&) {}); diff --git a/render_2D/render_2D/axis/Abs_Axis.cpp b/render_2D/render_2D/axis/Abs_Axis.cpp index 2246b99..c9f549c 100644 --- a/render_2D/render_2D/axis/Abs_Axis.cpp +++ b/render_2D/render_2D/axis/Abs_Axis.cpp @@ -1,4 +1,4 @@ -#include "Abs_Axis.hpp" +#include "Abs_Axis.hpp" /* 轴属性缓存失效实现。 */ #include #include #include diff --git a/render_2D/render_2D/axis/Abs_Axis.hpp b/render_2D/render_2D/axis/Abs_Axis.hpp index 9a5880b..965c929 100644 --- a/render_2D/render_2D/axis/Abs_Axis.hpp +++ b/render_2D/render_2D/axis/Abs_Axis.hpp @@ -1,6 +1,6 @@ #pragma once #include "Axis_Types.hpp" -#include "../render/Blend2D_Cache.hpp" +#include "../base/Renderable_2D.hpp" #include #include namespace aethera::render_2d { @@ -19,8 +19,7 @@ concept Axis_Object = Renderable_Object && std::derived_from && { private_data.sub_tick_count(object, tick) } -> std::same_as; }; /* 所有二维坐标轴共享的定义层;最终通过 Impl 创建运行时对象。 */ -struct Abs_Axis : Def> { +struct Abs_Axis : Def> { struct Prop : Prev_Prop { Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */ Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */ diff --git a/render_2D/render_2D/axis/Abs_Axis.ipp b/render_2D/render_2D/axis/Abs_Axis.ipp index edaeac5..9b7cb2f 100644 --- a/render_2D/render_2D/axis/Abs_Axis.ipp +++ b/render_2D/render_2D/axis/Abs_Axis.ipp @@ -20,32 +20,32 @@ struct Abs_Axis::Private : Prev_Private { using Count_Call = int (*)(const Root*, double); using Label_Call = std::string (*)(const Root*, double); struct Dispatch { - Coordinate_Range_Call coordinate_range; /* 读取最终轴权威坐标区间。 */ - Scalar_Call coordinate_to_pixel; /* 坐标到像素的最终类型分派。 */ - Scalar_Call pixel_to_coordinate; /* 像素到坐标的最终类型分派。 */ - Point_Call point_to_coordinate; /* 二维点到坐标的最终类型分派。 */ - Range_Count_Call pixel_sample_count; /* 像素样本数量的最终类型分派。 */ - Range_Scalar_Call tick_step; /* 主刻度步长的最终类型分派。 */ - Label_Call tick_label; /* 主刻度标签的最终类型分派。 */ - Count_Call sub_tick_count; /* 次刻度数量的最终类型分派。 */ + Coordinate_Range_Call coordinate_range; /* 读取最终轴权威坐标区间。 */ + Scalar_Call coordinate_to_pixel; /* 坐标到像素的最终类型分派。 */ + Scalar_Call pixel_to_coordinate; /* 像素到坐标的最终类型分派。 */ + Point_Call point_to_coordinate; /* 二维点到坐标的最终类型分派。 */ + Range_Count_Call pixel_sample_count; /* 像素样本数量的最终类型分派。 */ + Range_Scalar_Call tick_step; /* 主刻度步长的最终类型分派。 */ + Label_Call tick_label; /* 主刻度标签的最终类型分派。 */ + Count_Call sub_tick_count; /* 次刻度数量的最终类型分派。 */ }; struct Prepared_Line { - Point_F first{}; /* 线段起点,单位为画布像素。 */ - Point_F second{}; /* 线段终点,单位为画布像素。 */ + Point_F first{}; /* 线段起点,单位为画布像素。 */ + Point_F second{}; /* 线段终点,单位为画布像素。 */ }; struct Prepared_Label { - Point_F position{}; /* 标签左上角位置,单位为画布像素。 */ - std::string text{}; /* 已按最终轴规则格式化的标签文本。 */ + Point_F position{}; /* 标签左上角位置,单位为画布像素。 */ + std::string text{}; /* 已按最终轴规则格式化的标签文本。 */ }; struct Prepared_Axis { - std::vector lines{}; /* 本轮 Prepare 生成的轴线与刻度线。 */ - std::vector labels{}; /* 本轮 Prepare 生成的刻度标签。 */ - Point_F unit_position{}; /* 单位文本左上角位置,单位为画布像素。 */ - double unit_width{}; /* 单位文本背景的估算宽度,单位为像素。 */ - bool valid{}; /* 本轮 Prepare 是否生成了可绘制内容。 */ + std::vector lines{}; /* 本轮 Prepare 生成的轴线与刻度线。 */ + std::vector labels{}; /* 本轮 Prepare 生成的刻度标签。 */ + Point_F unit_position{}; /* 单位文本左上角位置,单位为画布像素。 */ + double unit_width{}; /* 单位文本背景的估算宽度,单位为像素。 */ + bool valid{}; /* 本轮 Prepare 是否生成了可绘制内容。 */ }; - const Dispatch* dispatch{}; /* Builder 绑定最终轴类型后指向静态分派表。 */ - Prepared_Axis prepared{}; /* 由当前 State 推导、仅供紧随其后的 Paint 消费。 */ + const Dispatch* dispatch{}; /* Builder 绑定最终轴类型后指向静态分派表。 */ + Prepared_Axis prepared{}; /* 由当前 State 推导、仅供紧随其后的 Paint 消费。 */ [[nodiscard]] Axis_Range coordinate_range(const Root* object) const; [[nodiscard]] double coordinate_to_pixel(const Root* object, double coordinate) const; [[nodiscard]] double pixel_to_coordinate(const Root* object, double pixel) const; @@ -64,6 +64,9 @@ struct Abs_Axis::Private : Prev_Private { void prepare_data(Attached auto* object); /* CRTP 实现:将本轮 Prepared_Axis 绘制到 Color_Cache 的写缓冲。 */ void paint(Attached auto* object); + /* CRTP Prop hook:任一轴属性变化都会使该轴的准备数据和颜色缓存失效。 */ + template + void after_prop_set(Object* object, Member Owner::* member, Prop_Access props); /* CRTP 默认:每两个主刻度之间生成 4 个次刻度。 */ [[nodiscard]] int sub_tick_count(const Attached auto* object, double major_step) const; /* CRTP 覆盖:绑定 Renderable 机制和最终轴公开薄壳分派;派生 Private 必须先调用此实现。 */ @@ -85,7 +88,8 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { const Axis_Range range = private_data.coordinate_range(object); const double coordinate_length = range.length(); const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal - ? axis_state.position.x : axis_state.position.y; + ? axis_state.position.x + : axis_state.position.y; if (coordinate_length == 0.0) return pixel_origin; return pixel_origin + (coordinate - range.origin) / coordinate_length * axis_state.pixel_length; }, @@ -96,7 +100,8 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { const Axis_Range range = private_data.coordinate_range(object); if (axis_state.pixel_length == 0.0) return range.origin; const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal - ? axis_state.position.x : axis_state.position.y; + ? axis_state.position.x + : axis_state.position.y; return range.origin + (pixel - pixel_origin) / axis_state.pixel_length * range.length(); }, [](const Root* root, Point_F point) { @@ -107,7 +112,8 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { const Axis_Range range = private_data.coordinate_range(object); if (axis_state.pixel_length == 0.0) return range.origin; const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal - ? axis_state.position.x : axis_state.position.y; + ? axis_state.position.x + : axis_state.position.y; return range.origin + (pixel - pixel_origin) / axis_state.pixel_length * range.length(); }, [](const Root* root, Axis_Range coordinate_range) { @@ -117,7 +123,8 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() { const Axis_Range range = private_data.coordinate_range(object); const auto map = [&](double coordinate) { const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal - ? axis_state.position.x : axis_state.position.y; + ? axis_state.position.x + : axis_state.position.y; if (range.length() == 0.0) return pixel_origin; return pixel_origin + (coordinate - range.origin) / range.length() * axis_state.pixel_length; }; @@ -176,7 +183,8 @@ inline void Abs_Axis::Private::prepare_data(Attached auto* object) { tick_start = {pixel, state.position.y}; tick_end = {pixel, state.position.y + state.tick_length}; label = {pixel + 2.0, state.position.y + state.tick_length + 2.0}; - } else { + } + else { tick_start = {state.position.x, pixel}; tick_end = {state.position.x + state.tick_length, pixel}; label = {state.position.x + state.tick_length + 2.0, pixel - 7.0}; @@ -189,11 +197,15 @@ inline void Abs_Axis::Private::prepare_data(Attached auto* object) { if (sub_tick >= high) break; const double sub_pixel = coordinate_to_pixel(object, sub_tick); if (state.orientation == Axis_Orientation::horizontal) - output.lines.push_back({{sub_pixel, state.position.y}, - {sub_pixel, state.position.y + state.sub_tick_length}}); + output.lines.push_back({ + {sub_pixel, state.position.y}, + {sub_pixel, state.position.y + state.sub_tick_length} + }); else - output.lines.push_back({{state.position.x, sub_pixel}, - {state.position.x + state.sub_tick_length, sub_pixel}}); + output.lines.push_back({ + {state.position.x, sub_pixel}, + {state.position.x + state.sub_tick_length, sub_pixel} + }); } } if (!state.unit_text.empty()) { @@ -207,9 +219,7 @@ inline void Abs_Axis::Private::paint(Attached auto* object) { using Object = std::remove_pointer_t; auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.current); - auto& cache = object->template pending_buffer(); - cache.ensure_size(state.canvas_size); - cache.clear(); + auto& cache = private_data.paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, state.canvas_size); for (const auto& line : prepared.lines) painter.line(line.first, line.second, state.axis_pen); @@ -217,12 +227,18 @@ inline void Abs_Axis::Private::paint(Attached auto* object) { painter.text(label.position, label.text, state.unit_text_font, state.unit_text_pen, state.label_rotation_degrees); if (!state.unit_text.empty()) { - painter.rect({prepared.unit_position.x - 2.0, prepared.unit_position.y - 2.0, - prepared.unit_width + 4.0, state.unit_text_font.size * 1.5 + 4.0}, + painter.rect({ + prepared.unit_position.x - 2.0, prepared.unit_position.y - 2.0, + prepared.unit_width + 4.0, state.unit_text_font.size * 1.5 + 4.0 + }, Pen{.style = Line_Style::none}, state.unit_text_background_brush); painter.text(prepared.unit_position, state.unit_text, state.unit_text_font, state.unit_text_pen); } } +template +void Abs_Axis::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { + object->template mark_dirty(); +} inline int Abs_Axis::Private::sub_tick_count(const Attached auto*, double) const { return 4; } diff --git a/render_2D/render_2D/axis/Frequency_Axis.cpp b/render_2D/render_2D/axis/Frequency_Axis.cpp index 802569f..94dfe51 100644 --- a/render_2D/render_2D/axis/Frequency_Axis.cpp +++ b/render_2D/render_2D/axis/Frequency_Axis.cpp @@ -1,4 +1,4 @@ -#include "Frequency_Axis.hpp" +#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/Numeric_Axis.cpp b/render_2D/render_2D/axis/Numeric_Axis.cpp index 1f39d15..9f9ef6e 100644 --- a/render_2D/render_2D/axis/Numeric_Axis.cpp +++ b/render_2D/render_2D/axis/Numeric_Axis.cpp @@ -1,4 +1,4 @@ -#include "Numeric_Axis.hpp" +#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/Time_Axis.cpp b/render_2D/render_2D/axis/Time_Axis.cpp index a522ef8..312421b 100644 --- a/render_2D/render_2D/axis/Time_Axis.cpp +++ b/render_2D/render_2D/axis/Time_Axis.cpp @@ -1,4 +1,4 @@ -#include "Time_Axis.hpp" +#include "Time_Axis.hpp" /* 继承轴属性缓存失效实现。 */ #include #include namespace aethera::render_2d { diff --git a/render_2D/render_2D/base/Renderable_2D.hpp b/render_2D/render_2D/base/Renderable_2D.hpp new file mode 100644 index 0000000..236ad57 --- /dev/null +++ b/render_2D/render_2D/base/Renderable_2D.hpp @@ -0,0 +1,37 @@ +#pragma once +#include "../render/Blend2D_Cache.hpp" +#include +namespace aethera::render_2d { +/* 二维 Renderable 是否拥有可跨帧复用的完整颜色缓存。 */ +enum class Renderable_2D_Cache { + disabled, + enabled +}; +/* + * 二维绘制能力层。业务类型在该模板位置选择是否拥有颜色缓存;默认不分配完整缓存。 + * Paint 实现只向 Scene 本轮指定的绘制目标输出,不直接选择或清理双缓冲角色。 + */ +template +struct Renderable_2D; +template <> +struct Renderable_2D : Def, Renderable> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { + bool operator==(const State&) const; + }; + struct Private; +}; +template <> +struct Renderable_2D + : Def, + Renderable_2D, + Tagged_Buffer> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { + bool operator==(const State&) const; + }; + struct Private; +}; +using Renderable_2D_Base = Renderable_2D; +} +#include "Renderable_2D.ipp" diff --git a/render_2D/render_2D/base/Renderable_2D.ipp b/render_2D/render_2D/base/Renderable_2D.ipp new file mode 100644 index 0000000..c014dc3 --- /dev/null +++ b/render_2D/render_2D/base/Renderable_2D.ipp @@ -0,0 +1,35 @@ +#pragma once +#include +namespace aethera::render_2d { +struct Renderable_2D::Private : Prev_Private { + using Cache_Access = Blend2D_Cache* (*)(Root*); + Cache_Access pending_cache{}; /* 非空时返回最终对象本轮可写的缓存物理对象。 */ + Blend2D_Cache* paint_target{}; /* 仅在 Scene Paint 阶段有效的非拥有绘制目标。 */ + Blend2D_Cache* valid_cache{}; /* 缓存根最近一次完整重绘产生的权威物理缓存。 */ + /* Paint 实现使用:返回 Scene 为本轮指定的目标;未进入 Paint 阶段属于契约错误。 */ + [[nodiscard]] Blend2D_Cache& paint_surface(); + /* CRTP 覆盖:识别最终类型是否选择了二维颜色缓存。 */ + template void bind_private_crtp(Object* object); +}; +struct Renderable_2D::Private : Prev_Private { + /* CRTP 覆盖:继续绑定二维绘制能力;缓存机制由本定义层静态加入。 */ + template void bind_private_crtp(Object* object); +}; +inline bool Renderable_2D::State::operator==(const State&) const = default; +inline bool Renderable_2D::State::operator==(const State&) const = default; +inline Blend2D_Cache& Renderable_2D::Private::paint_surface() { + if (!paint_target) throw std::logic_error("2D renderable painted without a scene paint target"); + return *paint_target; +} +template +void Renderable_2D::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); +} +template +void Renderable_2D::Private::bind_private_crtp(Object* object) { + Prev_Private::bind_private_crtp(object); + this->pending_cache = [](Root* root) { + return &static_cast(root)->template pending_buffer(); + }; +} +} diff --git a/render_2D/render_2D/base/Types.hpp b/render_2D/render_2D/base/Types.hpp index 7a8559f..e3be6b1 100644 --- a/render_2D/render_2D/base/Types.hpp +++ b/render_2D/render_2D/base/Types.hpp @@ -5,22 +5,26 @@ #include #include namespace aethera::render_2d { +/* Paint 阶段在 Dependency_Graph 图中的标签。 */ +struct Paint_Tag {}; +/* Paint 缓存依赖标签;具体字段变化通过该图使缓存拥有者失效。 */ +struct Paint_Cache_Tag {}; /* 整数二维坐标。 */ struct Point { - int x{}; /* 水平方向坐标。 */ - int y{}; /* 垂直方向坐标。 */ + int x{}; /* 水平方向坐标。 */ + int y{}; /* 垂直方向坐标。 */ bool operator==(const Point&) const = default; }; /* 浮点二维坐标。 */ struct Point_F { - double x{}; /* 水平方向坐标。 */ - double y{}; /* 垂直方向坐标。 */ + double x{}; /* 水平方向坐标。 */ + double y{}; /* 垂直方向坐标。 */ bool operator==(const Point_F&) const = default; }; /* 整数二维尺寸。 */ struct Size { - int width{}; /* 水平方向像素数。 */ - int height{}; /* 垂直方向像素数。 */ + int width{}; /* 水平方向像素数。 */ + int height{}; /* 垂直方向像素数。 */ [[nodiscard]] bool empty() const noexcept { return width <= 0 || height <= 0; } @@ -28,10 +32,10 @@ struct Size { }; /* 整数矩形。 */ struct Rect { - int x{}; /* 左上角水平坐标。 */ - int y{}; /* 左上角垂直坐标。 */ - int width{}; /* 矩形宽度。 */ - int height{}; /* 矩形高度。 */ + int x{}; /* 左上角水平坐标。 */ + int y{}; /* 左上角垂直坐标。 */ + int width{}; /* 矩形宽度。 */ + int height{}; /* 矩形高度。 */ [[nodiscard]] bool empty() const noexcept { return width <= 0 || height <= 0; } @@ -48,10 +52,10 @@ struct Rect { }; /* 浮点矩形。 */ struct Rect_F { - double x{}; /* 左上角水平坐标。 */ - double y{}; /* 左上角垂直坐标。 */ - double width{}; /* 矩形宽度;可为负数表达反向输入。 */ - double height{}; /* 矩形高度;可为负数表达反向输入。 */ + double x{}; /* 左上角水平坐标。 */ + double y{}; /* 左上角垂直坐标。 */ + double width{}; /* 矩形宽度;可为负数表达反向输入。 */ + double height{}; /* 矩形高度;可为负数表达反向输入。 */ [[nodiscard]] bool empty() const noexcept { return width <= 0.0 || height <= 0.0; } @@ -86,9 +90,9 @@ using Pixel = std::uint32_t; return (static_cast(color.alpha) << 24) | (red << 16) | (green << 8) | blue; } [[nodiscard]] constexpr Pixel pack_rgba(std::uint8_t red, - std::uint8_t green, - std::uint8_t blue, - std::uint8_t alpha = 255) noexcept { + std::uint8_t green, + std::uint8_t blue, + std::uint8_t alpha = 255) noexcept { return premultiply(Color{red, green, blue, alpha}); } enum class Line_Style : std::uint8_t { none, solid, dash, dot }; @@ -96,11 +100,11 @@ enum class Line_Cap : std::uint8_t { butt, square, round }; enum class Line_Join : std::uint8_t { miter, bevel, round }; /* 线条和文字前景样式。 */ struct Pen { - Color color{Color::white()}; /* 描边或文字颜色。 */ - double width{1.0}; /* 描边宽度,单位为像素。 */ - Line_Style style{Line_Style::solid}; /* 描边图案;none 表示禁用。 */ - Line_Cap cap{Line_Cap::butt}; /* 线段端点样式。 */ - Line_Join join{Line_Join::miter}; /* 折线连接样式。 */ + Color color{Color::white()}; /* 描边或文字颜色。 */ + double width{1.0}; /* 描边宽度,单位为像素。 */ + Line_Style style{Line_Style::solid}; /* 描边图案;none 表示禁用。 */ + Line_Cap cap{Line_Cap::butt}; /* 线段端点样式。 */ + Line_Join join{Line_Join::miter}; /* 折线连接样式。 */ [[nodiscard]] bool enabled() const noexcept { return style != Line_Style::none && width > 0.0 && color.alpha != 0; } @@ -109,8 +113,8 @@ struct Pen { enum class Brush_Style : std::uint8_t { none, solid }; /* 区域填充样式。 */ struct Brush { - Color color{Color::transparent()}; /* 填充颜色。 */ - Brush_Style style{Brush_Style::none}; /* 填充模式;none 表示禁用。 */ + Color color{Color::transparent()}; /* 填充颜色。 */ + Brush_Style style{Brush_Style::none}; /* 填充模式;none 表示禁用。 */ [[nodiscard]] bool enabled() const noexcept { return style != Brush_Style::none && color.alpha != 0; } @@ -118,14 +122,14 @@ struct Brush { }; /* 字体选择参数。 */ struct Font { - double size{12.0}; /* 字号,单位为像素。 */ - int weight{400}; /* 字重;600 及以上选择粗体。 */ - bool italic{}; /* 是否选择斜体字形。 */ + double size{12.0}; /* 字号,单位为像素。 */ + int weight{400}; /* 字重;600 及以上选择粗体。 */ + bool italic{}; /* 是否选择斜体字形。 */ bool operator==(const Font&) const = default; }; /* 数值标签使用的小数点规则。 */ struct Number_Locale { - char decimal_point{'.'}; /* 替换默认小数点的字符。 */ + char decimal_point{'.'}; /* 替换默认小数点的字符。 */ bool operator==(const Number_Locale&) const = default; }; enum class Image_Interpolation_Mode : std::uint8_t { nearest, bilinear, bicubic }; @@ -140,11 +144,11 @@ enum class Line_Interpolation_Mode : std::uint8_t { enum class Pixel_Format : std::uint8_t { premultiplied_32 }; /* 只读图像像素视图。 */ struct Image_View { - const std::byte* data{}; /* 首行像素地址;不拥有内存。 */ - int width{}; /* 图像宽度,单位为像素。 */ - int height{}; /* 图像高度,单位为像素。 */ - int stride{}; /* 相邻两行起点的字节距离。 */ - Pixel_Format format{Pixel_Format::premultiplied_32}; /* 像素存储格式。 */ + const std::byte* data{}; /* 首行像素地址;不拥有内存。 */ + int width{}; /* 图像宽度,单位为像素。 */ + int height{}; /* 图像高度,单位为像素。 */ + int stride{}; /* 相邻两行起点的字节距离。 */ + Pixel_Format format{Pixel_Format::premultiplied_32}; /* 像素存储格式。 */ [[nodiscard]] bool empty() const noexcept { return data == nullptr || width <= 0 || height <= 0; } diff --git a/render_2D/render_2D/plottable/Afterglow.cpp b/render_2D/render_2D/plottable/Afterglow.cpp index 68d5b64..e4fc3b0 100644 --- a/render_2D/render_2D/plottable/Afterglow.cpp +++ b/render_2D/render_2D/plottable/Afterglow.cpp @@ -1,5 +1,4 @@ -#include "Afterglow.hpp" +#include "Afterglow.hpp" /* Afterglow 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Afterglow_Editable_Prop_Data::operator==(const Afterglow_Editable_Prop_Data&) const = default; -bool Afterglow_Prop_Data::operator==(const Afterglow_Prop_Data&) const = default; bool Afterglow::Prop::operator==(const Prop&) const = default; +bool Afterglow::Prop::operator==(const Prop&) const = default; bool Afterglow::State::operator==(const State&) const = default; void Afterglow::append_spectrum(std::span values) { static_cast(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector&& values) { append_spectrum(std::span(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } diff --git a/render_2D/render_2D/plottable/Afterglow.hpp b/render_2D/render_2D/plottable/Afterglow.hpp index 5f53acf..cf22c76 100644 --- a/render_2D/render_2D/plottable/Afterglow.hpp +++ b/render_2D/render_2D/plottable/Afterglow.hpp @@ -9,28 +9,19 @@ #include #include namespace aethera::render_2d { -struct Afterglow; -struct Afterglow_Editable_Prop_Data { - using Prop_Tag = Afterglow; - std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ - std::size_t power_point_size{}; /* 栅格功率行数;零值使用默认尺寸。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ - bool interpolate{true}; /* 是否对频谱列执行线性重采样。 */ - Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例。 */ - bool operator==(const Afterglow_Editable_Prop_Data&) const; -}; -struct Afterglow_Prop_Data { - using Prop_Tag = Afterglow; - Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */ - Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - Color_Map color_map{}; /* 强度到颜色的映射。 */ - std::vector> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ - bool operator==(const Afterglow_Prop_Data&) const; -}; -struct Afterglow : Def> { +struct Afterglow : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop, Afterglow_Editable_Prop_Data, Afterglow_Prop_Data { + struct Prop : Prev_Prop { + std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ + std::size_t power_point_size{}; /* 栅格功率行数;零值使用默认尺寸。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool interpolate{true}; /* 是否对频谱列执行线性重采样。 */ + Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例。 */ + Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */ + Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Color_Map color_map{}; /* 强度到颜色的映射。 */ + std::vector> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Afterglow.ipp b/render_2D/render_2D/plottable/Afterglow.ipp index cd2ba92..8b65d9c 100644 --- a/render_2D/render_2D/plottable/Afterglow.ipp +++ b/render_2D/render_2D/plottable/Afterglow.ipp @@ -42,7 +42,7 @@ struct Afterglow::Private : Prev_Private { }; 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_prop_dependency(plot.get(), scene); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), power_axis); prepare.template add_prop_dependency(plot.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } +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, auto& cache) { prepare.add_dependency(plot.get(), scene); prepare.add_dependency(plot.get(), frequency_axis); prepare.add_dependency(plot.get(), power_axis); paint.add_dependency(frequency_axis, plot.get()); paint.add_dependency(power_axis, plot.get()); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(plot.get(), scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(plot.get(), power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(plot.get(), power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(plot.get(), power_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(plot.get(), power_axis); }); 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 Prop& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max(1, columns)); } template @@ -55,7 +55,7 @@ void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Cou 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_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast(columns), index, graph_partition_count); for (std::size_t column = first; column < last; ++column) for (int row = 0; row < rows; ++row) { const std::size_t cell = static_cast(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } } -template void Afterglow::Private::paint_frame(Object* object) { auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); } +template void Afterglow::Private::paint_frame(Object*) { auto& cache = this->paint_surface(); 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_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template void Afterglow::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.history_count = prop.spectra.size(); state.latest_spectrum_point_count = prop.spectra.empty() ? 0 : prop.spectra.back().size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; } template diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/render_2D/plottable/Constellation_Diagram.cpp index 7e72042..de27bca 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.cpp @@ -1,5 +1,4 @@ -#include "Constellation_Diagram.hpp" +#include "Constellation_Diagram.hpp" /* Constellation 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Constellation_Diagram_Editable_Prop_Data::operator==(const Constellation_Diagram_Editable_Prop_Data&) const = default; -bool Constellation_Diagram_Prop_Data::operator==(const Constellation_Diagram_Prop_Data&) const = default; bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::Prop::operator==(const Prop&) const = default; +bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::Prop::operator==(const Prop&) const = default; bool Constellation_Diagram::State::operator==(const State&) const = default; void Constellation_Diagram::append_point(Point_F point) { static_cast(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast(*d).dispatch->fit(this); } } diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.hpp b/render_2D/render_2D/plottable/Constellation_Diagram.hpp index 2bd8740..b159913 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.hpp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.hpp @@ -9,26 +9,17 @@ namespace aethera::render_2d { enum class Constellation_Diagram_Type : std::uint8_t { psk4 = 4, psk8 = 8, psk16 = 16 }; struct Constellation_Point { Point_F point{}; Plot_Duration_Milliseconds submitted_at_ms{}; bool operator==(const Constellation_Point&) const; }; -struct Constellation_Diagram; -struct Constellation_Diagram_Editable_Prop_Data { - using Prop_Tag = Constellation_Diagram; - Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */ - Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */ - Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ - bool operator==(const Constellation_Diagram_Editable_Prop_Data&) const; -}; -struct Constellation_Diagram_Prop_Data { - using Prop_Tag = Constellation_Diagram; - 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()}; /* 理想星座锚点颜色。 */ - std::vector points{}; /* 已提交且尚未过期的点。 */ - bool operator==(const Constellation_Diagram_Prop_Data&) const; -}; -struct Constellation_Diagram : Def> { +struct Constellation_Diagram : Def> { using Scene_Object = Impl; using Axis_Object = Impl; - struct Prop : Prev_Prop, Constellation_Diagram_Editable_Prop_Data, Constellation_Diagram_Prop_Data { + struct Prop : Prev_Prop { + Plot_Duration_Milliseconds point_lifetime_ms{1000}; /* 接收点保留时间,单位为毫秒。 */ + Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */ + Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ + 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()}; /* 理想星座锚点颜色。 */ + std::vector points{}; /* 已提交且尚未过期的点。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Constellation_Diagram.ipp b/render_2D/render_2D/plottable/Constellation_Diagram.ipp index f185fc0..718bc63 100644 --- a/render_2D/render_2D/plottable/Constellation_Diagram.ipp +++ b/render_2D/render_2D/plottable/Constellation_Diagram.ipp @@ -35,10 +35,10 @@ struct Constellation_Diagram::Private : Prev_Private { }; 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_prop_dependency(plot.get(), scene); prepare.template add_prop_dependency(plot.get(), i_axis); prepare.template add_prop_dependency(plot.get(), i_axis); prepare.template add_prop_dependency(plot.get(), q_axis); prepare.template add_prop_dependency(plot.get(), q_axis); paint.add(i_axis); paint.add(q_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } +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, auto& cache) { prepare.add_dependency(plot.get(), scene); prepare.add_dependency(plot.get(), i_axis); prepare.add_dependency(plot.get(), q_axis); paint.add_dependency(i_axis, plot.get()); paint.add_dependency(q_axis, plot.get()); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(plot.get(), scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(plot.get(), i_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(plot.get(), i_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(plot.get(), i_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(plot.get(), i_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(plot.get(), q_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(plot.get(), q_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(plot.get(), q_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(plot.get(), q_axis); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } template void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_prop(); const auto& i_layout = i_axis->template read_prop(); const auto& q_layout = q_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) return; const auto current = monotonic_milliseconds(); for (const auto& value : state.points) if (current - value.submitted_at_ms <= state.point_lifetime_ms) prepared.points.push_back(detail::map_plot_point(i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); const int count = static_cast(state.type); const Plot_Coordinate center_i = state.i_range.center(); const Plot_Coordinate center_q = state.q_range.center(); const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for (int index = 0; index < count; ++index) { const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); } prepared.valid = true; object->template mark_dirty(); } -template void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); } +template void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); } template void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template void Constellation_Diagram::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { pending_states.template get().point_count = static_cast(*current_prop).points.size(); } template diff --git a/render_2D/render_2D/plottable/Frequency_Trace.cpp b/render_2D/render_2D/plottable/Frequency_Trace.cpp index 24afd22..a810019 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.cpp @@ -1,7 +1,5 @@ -#include "Frequency_Trace.hpp" +#include "Frequency_Trace.hpp" /* Frequency Trace 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Frequency_Trace_Editable_Prop_Data::operator==(const Frequency_Trace_Editable_Prop_Data&) const = default; -bool Frequency_Trace_Prop_Data::operator==(const Frequency_Trace_Prop_Data&) const = default; bool Frequency_Trace_Sample::operator==(const Frequency_Trace_Sample&) const = default; bool Frequency_Trace::Prop::operator==(const Prop&) const = default; bool Frequency_Trace::State::operator==(const State&) const = default; diff --git a/render_2D/render_2D/plottable/Frequency_Trace.hpp b/render_2D/render_2D/plottable/Frequency_Trace.hpp index 0a8b42d..fd15ef5 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.hpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.hpp @@ -12,24 +12,15 @@ struct Frequency_Trace_Sample { Plot_Value value{}; /* 该时间点对应的频率值。 */ bool operator==(const Frequency_Trace_Sample&) const; }; -struct Frequency_Trace; -struct Frequency_Trace_Editable_Prop_Data { - using Prop_Tag = Frequency_Trace; - Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ - bool operator==(const Frequency_Trace_Editable_Prop_Data&) const; -}; -struct Frequency_Trace_Prop_Data { - using Prop_Tag = Frequency_Trace; - Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - std::vector samples{}; /* 已提交轨迹样本的唯一权威集合。 */ - bool operator==(const Frequency_Trace_Prop_Data&) const; -}; -struct Frequency_Trace : Def> { +struct Frequency_Trace : Def> { using Scene_Object = Impl; using Time_Object = Impl; using Value_Object = Impl; - struct Prop : Prev_Prop, Frequency_Trace_Editable_Prop_Data, Frequency_Trace_Prop_Data { + struct Prop : Prev_Prop { + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + std::vector samples{}; /* 已提交轨迹样本的唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp index 1ffeab7..3aafbc2 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -47,12 +47,13 @@ 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_prop_dependency(trace.get(), scene); - prepare.template add_prop_dependency(trace.get(), time_axis); prepare.template add_prop_dependency(trace.get(), time_axis); - prepare.template add_prop_dependency(trace.get(), value_axis); prepare.template add_prop_dependency(trace.get(), value_axis); - paint.add(time_axis); paint.add(value_axis); paint.add(trace.get()); + auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { + prepare.add_dependency(trace.get(), scene); prepare.add_dependency(trace.get(), time_axis); prepare.add_dependency(trace.get(), value_axis); + paint.add_dependency(time_axis, trace.get()); paint.add_dependency(value_axis, trace.get()); + cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(trace.get(), scene); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(trace.get(), time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(trace.get(), time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(trace.get(), time_axis); + cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(trace.get(), time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(trace.get(), time_axis); cache.template add_dependency<&Time_Axis::State::next_tick>(trace.get(), time_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(trace.get(), value_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(trace.get(), value_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(trace.get(), value_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(trace.get(), value_axis); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(trace); } @@ -83,7 +84,7 @@ void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_ } template void Frequency_Trace::Private::paint_frame(Object* object) { - const auto& state = object->template read_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); + const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); 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_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp index 3875914..cd9665d 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -1,6 +1,5 @@ -#include "Selection_Rectangle_Overlay.hpp" +#include "Selection_Rectangle_Overlay.hpp" /* Selection Overlay 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Selection_Rectangle_Overlay_Prop_Data::operator==(const Selection_Rectangle_Overlay_Prop_Data&) const = default; bool Selection_Rectangle_Overlay::Prop::operator==(const Prop&) const = default; bool Selection_Rectangle_Overlay::State::operator==(const State&) const = default; std::vector Selection_Rectangle_Overlay::selected_regions() const { return static_cast(*d).dispatch->selected_regions(this); } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp index 86815d7..f1122a2 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp @@ -7,24 +7,19 @@ #include #include namespace aethera::render_2d { -struct Selection_Rectangle_Overlay; -struct Selection_Rectangle_Overlay_Prop_Data { - using Prop_Tag = Selection_Rectangle_Overlay; - 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 Selection_Rectangle_Overlay_Prop_Data&) const; -}; -struct Selection_Rectangle_Overlay : Def> { +struct Selection_Rectangle_Overlay : Def> { using Scene_Object = Impl; using Axis_Object = Impl; - struct Prop : Prev_Prop, Selection_Rectangle_Overlay_Prop_Data { + struct Prop : Prev_Prop { + Font label_font{}; /* 选择范围标签使用的字体。 */ + Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */ + Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::solid}; /* 选择矩形内部填充。 */ + Pen selection_border_pen{Color::white(), 1.0, Line_Style::dash}; /* 选择矩形边框样式。 */ + std::vector selected_regions{}; /* 已完成选择的轴坐标矩形。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { - std::size_t selected_region_count{}; /* 当前已完成选择的矩形数量。 */ + std::size_t selected_region_count{}; /* 当前已完成选择的矩形数量。 */ bool operator==(const State&) const; }; struct Private; @@ -34,9 +29,9 @@ struct Selection_Rectangle_Overlay : Def, Dependency_Graph_Error> build(); private: - Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ - Axis_Object* horizontal_axis{}; /* 不拥有的水平坐标轴。 */ - Axis_Object* vertical_axis{}; /* 不拥有的垂直坐标轴。 */ + Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ + Axis_Object* horizontal_axis{}; /* 不拥有的水平坐标轴。 */ + Axis_Object* vertical_axis{}; /* 不拥有的垂直坐标轴。 */ }; [[nodiscard]] std::vector selected_regions() const; void clear_selected_regions(); diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp index 7a46fd8..cebce8c 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp @@ -37,13 +37,21 @@ std::expected, Dependency_Graph_Error> Selection_Rectang 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_prop_dependency(overlay.get(), scene); - paint.template add_prop_dependency(overlay.get(), horizontal_axis); - paint.template add_prop_dependency(overlay.get(), horizontal_axis); - paint.template add_prop_dependency(overlay.get(), vertical_axis); - paint.template add_prop_dependency(overlay.get(), vertical_axis); + auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { + prepare.add_dependency(overlay.get(), scene); + prepare.add_dependency(overlay.get(), horizontal_axis); + prepare.add_dependency(overlay.get(), vertical_axis); + paint.add_dependency(horizontal_axis, overlay.get()); + paint.add_dependency(vertical_axis, overlay.get()); + cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(overlay.get(), scene); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(overlay.get(), horizontal_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(overlay.get(), horizontal_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(overlay.get(), horizontal_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(overlay.get(), horizontal_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(overlay.get(), vertical_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(overlay.get(), vertical_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(overlay.get(), vertical_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(overlay.get(), vertical_axis); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(overlay); @@ -53,8 +61,7 @@ void Selection_Rectangle_Overlay::Private::paint(Object* object) { auto& data = static_cast(*this); const auto& state = static_cast(*data.current); const Size canvas = scene->template read_prop().viewport; - auto& cache = object->template pending_buffer(); - cache.ensure_size(canvas); cache.clear(); + auto& cache = data.paint_surface(); if (canvas.empty()) return; detail::Painter painter(cache, canvas); const auto paint_region = [&](Rect_F region) { diff --git a/render_2D/render_2D/plottable/Spectrum.cpp b/render_2D/render_2D/plottable/Spectrum.cpp index 7a67ec0..2bb7961 100644 --- a/render_2D/render_2D/plottable/Spectrum.cpp +++ b/render_2D/render_2D/plottable/Spectrum.cpp @@ -1,7 +1,5 @@ -#include "Spectrum.hpp" +#include "Spectrum.hpp" /* Spectrum 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Spectrum_Editable_Prop_Data::operator==(const Spectrum_Editable_Prop_Data&) const = default; -bool Spectrum_Prop_Data::operator==(const Spectrum_Prop_Data&) const = default; bool Spectrum_Frame::operator==(const Spectrum_Frame&) const = default; bool Spectrum::Prop::operator==(const Prop&) const = default; bool Spectrum::State::operator==(const State&) const = default; diff --git a/render_2D/render_2D/plottable/Spectrum.hpp b/render_2D/render_2D/plottable/Spectrum.hpp index 87acb69..662391e 100644 --- a/render_2D/render_2D/plottable/Spectrum.hpp +++ b/render_2D/render_2D/plottable/Spectrum.hpp @@ -23,44 +23,35 @@ struct Spectrum_Frame { bool operator==(const Spectrum_Frame&) const; }; /* 使用频率轴和功率轴分块准备、绘制当前值、保持曲线及频率标记。 */ -struct Spectrum; -struct Spectrum_Editable_Prop_Data { - using Prop_Tag = Spectrum; - Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ - std::size_t partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ - 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}; /* 是否裁掉频率轴当前范围外的线段。 */ - bool operator==(const Spectrum_Editable_Prop_Data&) const; -}; -struct Spectrum_Prop_Data { - using Prop_Tag = Spectrum; - Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */ - Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */ - Spectrum_Partition_Mode partition_mode{Spectrum_Partition_Mode::automatic}; /* Prepare 子图的分块策略。 */ - 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}; /* 扫频区域背景样式。 */ - std::vector custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */ - Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */ - bool operator==(const Spectrum_Prop_Data&) const; -}; -struct Spectrum : Def, Tagged_Buffer> { +struct Spectrum : Def, Tagged_Buffer> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop, Spectrum_Editable_Prop_Data, Spectrum_Prop_Data { + struct Prop : Prev_Prop { + Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ + std::size_t partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + 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}; /* 是否裁掉频率轴当前范围外的线段。 */ + Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */ + Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */ + Spectrum_Partition_Mode partition_mode{Spectrum_Partition_Mode::automatic}; /* Prepare 子图的分块策略。 */ + 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}; /* 扫频区域背景样式。 */ + std::vector custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */ + Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index 5f02ee9..b627028 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -102,18 +102,21 @@ std::expected, Dependency_Graph_Error> Spectrum::Builder 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_prop_dependency(spectrum.get(), scene); - prepare.template add_prop_dependency(spectrum.get(), frequency_axis); - prepare.template add_prop_dependency(spectrum.get(), frequency_axis); - prepare.template add_prop_dependency(spectrum.get(), power_axis); - prepare.template add_prop_dependency(spectrum.get(), power_axis); - paint.add(frequency_axis); - paint.add(power_axis); - paint.add(spectrum.get()); + auto dependency_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { + prepare.add_dependency(spectrum.get(), scene); + prepare.add_dependency(spectrum.get(), frequency_axis); + prepare.add_dependency(spectrum.get(), power_axis); + paint.add_dependency(frequency_axis, spectrum.get()); + paint.add_dependency(power_axis, spectrum.get()); + cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(spectrum.get(), scene); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(spectrum.get(), frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(spectrum.get(), frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(spectrum.get(), frequency_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(spectrum.get(), frequency_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::position>(spectrum.get(), power_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(spectrum.get(), power_axis); + cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(spectrum.get(), power_axis); + cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(spectrum.get(), power_axis); }); if (!dependency_result) return std::unexpected(dependency_result.error()); return std::move(spectrum); @@ -215,9 +218,7 @@ template void Spectrum::Private::paint_frame(Object* object) { auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.current); - auto& cache = object->template pending_buffer(); - cache.ensure_size(prepared.canvas_size); - cache.clear(); + auto& cache = private_data.paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas_size); if (state.sweep_region_visible && !prepared.sweep_region.empty()) painter.rect(prepared.sweep_region, Pen{.style = Line_Style::none}, state.sweep_region_brush); diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp index a1b0704..a27764c 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.cpp @@ -1,7 +1,5 @@ -#include "Sweep_Spectrum.hpp" +#include "Sweep_Spectrum.hpp" /* Sweep Spectrum 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Sweep_Spectrum_Editable_Prop_Data::operator==(const Sweep_Spectrum_Editable_Prop_Data&) const = default; -bool Sweep_Spectrum_Prop_Data::operator==(const Sweep_Spectrum_Prop_Data&) const = default; bool Sweep_Spectrum::Prop::operator==(const Prop&) const = default; bool Sweep_Spectrum::State::operator==(const State&) const = default; void Sweep_Spectrum::append_block(std::span values) { static_cast(*d).dispatch->append(this, values); } diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp index 4e29594..4d8ca15 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.hpp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.hpp @@ -9,30 +9,21 @@ #include #include namespace aethera::render_2d { -struct Sweep_Spectrum; -struct Sweep_Spectrum_Editable_Prop_Data { - using Prop_Tag = Sweep_Spectrum; - std::size_t bins_per_block{}; /* 每个扫频块期望的功率点数;零值接受首块尺寸。 */ - std::size_t block_count{1}; /* 最多保留的扫频块数;零值按 1 处理。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ - bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */ - bool operator==(const Sweep_Spectrum_Editable_Prop_Data&) const; -}; -struct Sweep_Spectrum_Prop_Data { - using Prop_Tag = Sweep_Spectrum; - Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - Pen pen{Color::yellow()}; /* 扫频折线样式。 */ - Pen current_frequency_pen{Color::red_color(), 2.0}; /* 当前扫频位置垂线样式。 */ - Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */ - std::vector> blocks{}; /* 已提交扫描块的唯一权威集合。 */ - bool operator==(const Sweep_Spectrum_Prop_Data&) const; -}; -struct Sweep_Spectrum : Def> { +struct Sweep_Spectrum : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Power_Object = Impl; - struct Prop : Prev_Prop, Sweep_Spectrum_Editable_Prop_Data, Sweep_Spectrum_Prop_Data { + struct Prop : Prev_Prop { + std::size_t bins_per_block{}; /* 每个扫频块期望的功率点数;零值接受首块尺寸。 */ + std::size_t block_count{1}; /* 最多保留的扫频块数;零值按 1 处理。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */ + Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Pen pen{Color::yellow()}; /* 扫频折线样式。 */ + Pen current_frequency_pen{Color::red_color(), 2.0}; /* 当前扫频位置垂线样式。 */ + Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */ + std::vector> blocks{}; /* 已提交扫描块的唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp index 086c918..cf48d85 100644 --- a/render_2D/render_2D/plottable/Sweep_Spectrum.ipp +++ b/render_2D/render_2D/plottable/Sweep_Spectrum.ipp @@ -45,7 +45,7 @@ Sweep_Spectrum::Builder::Builder(Scene_Object* scene_value, Frequency_Ob 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_prop_dependency(sweep.get(), scene); prepare.template add_prop_dependency(sweep.get(), frequency_axis); prepare.template add_prop_dependency(sweep.get(), frequency_axis); prepare.template add_prop_dependency(sweep.get(), power_axis); prepare.template add_prop_dependency(sweep.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(sweep.get()); }); + auto graph_result = scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(sweep.get(), scene); prepare.add_dependency(sweep.get(), frequency_axis); prepare.add_dependency(sweep.get(), power_axis); paint.add_dependency(frequency_axis, sweep.get()); paint.add_dependency(power_axis, sweep.get()); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(sweep.get(), scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(sweep.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(sweep.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(sweep.get(), frequency_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(sweep.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(sweep.get(), power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(sweep.get(), power_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(sweep.get(), power_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(sweep.get(), power_axis); }); 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))); } @@ -67,7 +67,7 @@ void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_C 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_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); } +void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); } template void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template diff --git a/render_2D/render_2D/plottable/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp index 6077f4c..45ec89a 100644 --- a/render_2D/render_2D/plottable/Waterfall.cpp +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -1,5 +1,4 @@ -#include "Waterfall.hpp" +#include "Waterfall.hpp" /* Waterfall 最终实例及三类依赖实现。 */ namespace aethera::render_2d { -bool Waterfall_Editable_Prop_Data::operator==(const Waterfall_Editable_Prop_Data&) const = default; -bool Waterfall_Prop_Data::operator==(const Waterfall_Prop_Data&) const = default; bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default; +bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default; bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { static_cast(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span values) { static_cast(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { append_row(time, std::span(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } diff --git a/render_2D/render_2D/plottable/Waterfall.hpp b/render_2D/render_2D/plottable/Waterfall.hpp index 474ab02..07933c4 100644 --- a/render_2D/render_2D/plottable/Waterfall.hpp +++ b/render_2D/render_2D/plottable/Waterfall.hpp @@ -11,27 +11,22 @@ #include namespace aethera::render_2d { struct Waterfall_Row { Plot_Time_Tick tick{}; std::vector values{}; bool operator==(const Waterfall_Row&) const; }; -struct Waterfall; -struct Waterfall_Editable_Prop_Data { - using Prop_Tag = Waterfall; - std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ - bool visible_range_only{true}; /* 是否按频率轴范围裁剪栅格。 */ - bool operator==(const Waterfall_Editable_Prop_Data&) const; -}; -struct Waterfall_Prop_Data { - using Prop_Tag = Waterfall; - Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ - Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ - Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ - Color_Map color_map{}; /* 功率到颜色的映射。 */ - std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ - bool operator==(const Waterfall_Prop_Data&) const; -}; -struct Waterfall : Def> { +struct Waterfall : Def> { using Scene_Object = Impl; using Frequency_Object = Impl; using Time_Object = Impl; - struct Prop : Prev_Prop, Hover_Tooltip_Properties, Waterfall_Editable_Prop_Data, Waterfall_Prop_Data { + struct Prop : Prev_Prop { + 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}; /* 提示背景样式。 */ + std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool visible_range_only{true}; /* 是否按频率轴范围裁剪栅格。 */ + Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ + Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ + Color_Map color_map{}; /* 功率到颜色的映射。 */ + std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp index f0dca8e..162b93a 100644 --- a/render_2D/render_2D/plottable/Waterfall.ipp +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -50,7 +50,7 @@ struct Waterfall::Private : Prev_Private { }; 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_prop_dependency(plot.get(), scene); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), frequency_axis); prepare.template add_prop_dependency(plot.get(), time_axis); prepare.template add_prop_dependency(plot.get(), time_axis); paint.add(frequency_axis); paint.add(time_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } +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, auto& cache) { prepare.add_dependency(plot.get(), scene); prepare.add_dependency(plot.get(), frequency_axis); prepare.add_dependency(plot.get(), time_axis); paint.add_dependency(frequency_axis, plot.get()); paint.add_dependency(time_axis, plot.get()); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(plot.get(), scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(plot.get(), frequency_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(plot.get(), time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(plot.get(), time_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(plot.get(), time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(plot.get(), time_axis); cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(plot.get(), time_axis); cache.template add_dependency<&Time_Axis::State::next_tick>(plot.get(), time_axis); }); 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 Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); } @@ -60,7 +60,7 @@ template void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& time_layout = time_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int rows = static_cast(state.rows.size()); const Axis_Range time_range = rows == 1 ? time_axis->coordinate_range() : Axis_Range{static_cast(state.rows.front().tick), static_cast(state.rows.back().tick)}; prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, rows, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; } template void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast(columns) * state.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const std::size_t row_index = cell / static_cast(columns); const int column = static_cast(cell % static_cast(columns)); const auto& values = state.rows[row_index].values; const std::size_t source = static_cast(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, static_cast(row_index))] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } } -template void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = object->template pending_buffer(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } } +template void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); 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_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.row_count = prop.rows.size(); state.stored_point_count = 0; for (const auto& row : prop.rows) state.stored_point_count += row.values.size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 316947d..8ae0339 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -1,4 +1,4 @@ -#include "Render_Scene_2D.hpp" +#include "Render_Scene_2D.hpp" /* 二维 Paint Taskflow、缓存失效与合成实现。 */ namespace aethera::render_2d { bool Render_Scene_2D::State::operator==(const State&) const = default; bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index a38d94d..88e715d 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -1,5 +1,5 @@ #pragma once -#include "../base/Types.hpp" +#include "../base/Renderable_2D.hpp" #include "../render/Blend2D_Cache.hpp" #include #include @@ -7,11 +7,14 @@ namespace aethera::render_2d { struct Scene_Color_Cache_Tag {}; /* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */ struct Render_Scene_2D : Def> { + Tagged_Buffer, + Dependency_Graph_Type, + Dependency_Graph_Type +> { struct Prop : Prev_Prop { - Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */ - Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */ - bool view_active{}; /* 视图是否接受 render() 产生新的完成帧。 */ + Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */ + Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */ + bool view_active{}; /* 视图是否接受 render() 产生新的完成帧。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index 5180d94..28c371a 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -1,5 +1,7 @@ #pragma once #include +#include +#include #include namespace aethera::render_2d { struct Render_Scene_2D::Private : Prev_Private { @@ -8,6 +10,18 @@ struct Render_Scene_2D::Private : Prev_Private { using Event_Run = void (*)(Root*, const Event&); using Active_Run = void (*)(Root*, bool); using Frame_View_Run = Image_View (*)(const Root*); + struct Paint_Node { + Root* object{}; /* Paint 拓扑位置对应的最终对象;Scene 不拥有。 */ + Renderable_2D_Base::Private* private_data{}; /* 对象的二维能力层;对象存活期间有效。 */ + Root* cache_owner{}; /* 所属缓存根;空值表示直接绘制最终帧。 */ + bool cache_group_last{}; /* 是否为所属缓存组在拓扑序中的最后节点。 */ + bool paint_requested{}; /* Scene 按本轮组级失效结果计算的执行许可。 */ + }; + struct Cache_Group { + Root* owner{}; /* 显式选择缓存的二维 Renderable 根。 */ + std::vector members{}; /* 本缓存覆盖的根及无冲突后继节点。 */ + bool rebuild{}; /* 本轮是否因任一成员失效而整体重绘。 */ + }; struct Dispatch { Render_Run render; /* 执行最终 Scene 并合成颜色层。 */ Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ @@ -17,11 +31,19 @@ struct Render_Scene_2D::Private : Prev_Private { }; const Dispatch* dispatch{}; /* Builder 绑定最终 Scene 类型后的静态分派表。 */ Frame_Callback frame_callback{}; /* 合成完成后的唯一像素发布出口。 */ + std::unique_ptr paint_taskflow{}; /* 仅由二维 Paint 图构建的执行图。 */ bool rendering{}; /* 是否正在同步合成当前帧。 */ bool render_pending{}; /* 合成期间是否又收到 render();多个调用合并。 */ /* Impl CRTP 实现:在对象锁内执行 Kernel Scene,再按 Paint 图拓扑顺序合成颜色层。 */ + std::vector paint_order{}; /* Paint 图当前拓扑序及 Scene 缓存分组结果。 */ + std::vector cache_groups{}; /* 仅保存显式缓存根对应的执行分组。 */ template void process(Object* object, Callback&& callback) requires std::invocable; + /* Def CRTP hook:二维 Paint 图结构变化后重建其独立 Taskflow。 */ + template + void after_advance(Object* object, Prop* pending_prop, State_Access pending_states, const Prop* current_prop, State_Access current_states); + /* 每帧在 Prepare 完成后计算组级 dirty,并为所有二维节点指定唯一绘制目标。 */ + template void prepare_paint_targets(Object* object, Blend2D_Cache& frame, Size viewport); template void render(Object* object); /* CRTP 业务实现:按 Paint 图拓扑逆序派发事件。 */ template @@ -32,6 +54,147 @@ struct Render_Scene_2D::Private : Prev_Private { template void bind_private_crtp(Object* object); }; +template +void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access, const Prop*, State_Access) { + bool rebuild = !paint_taskflow; + object->template access_pending_dependency_graph([&](auto& paint_state) { rebuild = rebuild || paint_state.dirty(); }); + if (!rebuild) return; + if (!paint_taskflow) paint_taskflow = std::make_unique(); + auto& taskflow = *paint_taskflow; + aethera::detail::clear_stage_observers(&taskflow); + taskflow.clear(); + const auto dependencies = object->template current_dependency_graph(); + paint_order.clear(); + cache_groups.clear(); + std::unordered_map cache_owner_by_object; + std::unordered_map cache_group_by_owner; + const auto order_result = dependencies.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { + auto* data = view.private_data(node); + if (!data) return; + Root* cache_owner{}; + if (data->pending_cache) cache_owner = node.object; + else { + bool conflict{}; + for (const auto* dependency : node.dependencies) { + if (!view.private_data(*dependency)) continue; + auto current = cache_owner_by_object.find(dependency->object); + const Root* dependency_owner = current == cache_owner_by_object.end() ? nullptr : current->second; + if (!dependency_owner || (cache_owner && cache_owner != dependency_owner)) { conflict = true; break; } + cache_owner = const_cast(dependency_owner); + } + if (conflict) cache_owner = nullptr; + } + cache_owner_by_object.emplace(node.object, cache_owner); + paint_order.push_back(Paint_Node{node.object, data, cache_owner, false, false}); + if (!cache_owner) return; + auto [group, inserted] = cache_group_by_owner.emplace(cache_owner, cache_groups.size()); + if (inserted) cache_groups.push_back(Cache_Group{cache_owner}); + cache_groups[group->second].members.push_back(node.object); + }); + if (!order_result) throw std::logic_error("render scene paint graph became invalid while building cache groups"); + std::unordered_set closed_groups; + for (auto current = paint_order.rbegin(); current != paint_order.rend(); ++current) + if (current->cache_owner && closed_groups.insert(current->cache_owner).second) current->cache_group_last = true; + tf::Task previous; + bool has_previous{}; + for (std::size_t index = 0; index < paint_order.size(); ++index) { + auto& paint_node = paint_order[index]; + Root* root = paint_node.object; + auto& data = *paint_node.private_data; + auto* dispatch = data.dispatch; + auto paint_if = taskflow.emplace([this, &data, dispatch, root, index] { + auto& state = *dispatch->state.get(root); + state.paint_graph_rebuilt = false; + state.paint_execution_time_ns = 0; + if (dispatch->paint.builder) { + const bool rebuild_graph = dispatch->paint.rebuild_predicate(root); + if (!data.paint_graph_built || rebuild_graph) { + *data.paint_graph = dispatch->paint.builder(root); + data.paint_graph_built = true; + state.paint_graph_rebuilt = true; + root->template mark_dirty(); + } + state.paint_task_count = data.paint_graph->num_tasks(); + } else state.paint_task_count = dispatch->paint.run ? 1 : 0; + const bool dirty = root->template dirty(); + state.paint_dirty = dirty; + state.paint_executed = paint_order[index].paint_requested && dispatch->paint.predicate(root, dirty); + return state.paint_executed ? 0 : 1; + }).name("render_2d.paint.condition"); + tf::Task paint_run; + if (dispatch->paint.builder) { + if (!data.paint_graph) data.paint_graph = std::make_unique(); + paint_run = taskflow.composed_of(*data.paint_graph).name("render_2d.paint.graph"); + } else paint_run = taskflow.emplace([dispatch, root] { if (dispatch->paint.run) dispatch->paint.run(root); }).name("render_2d.paint.data"); + auto paint_done = taskflow.emplace([dispatch, root] { + auto& state = *dispatch->state.get(root); + if (state.paint_executed) root->template take_dirty(); + dispatch->state.notify(root); + }).name("render_2d.paint.complete"); + paint_if.precede(paint_run, paint_done); + paint_run.precede(paint_done); + if (has_previous) previous.precede(paint_if); + aethera::detail::bind_stage_observer(&taskflow, paint_if.hash_value(), root, &data, aethera::detail::Renderable_Stage::Paint, aethera::detail::Stage_Observer_Point::Begin); + aethera::detail::bind_stage_observer(&taskflow, paint_done.hash_value(), root, &data, aethera::detail::Renderable_Stage::Paint, aethera::detail::Stage_Observer_Point::End); + previous = paint_done; + if (paint_node.cache_group_last) { + Root* cache_owner = paint_node.cache_owner; + auto composite = taskflow.emplace([this, object, cache_owner] { + const auto group = std::find_if(cache_groups.begin(), cache_groups.end(), [cache_owner](const Cache_Group& value) { return value.owner == cache_owner; }); + const auto owner_node = std::find_if(paint_order.begin(), paint_order.end(), [cache_owner](const Paint_Node& value) { return value.object == cache_owner; }); + if (group == cache_groups.end() || owner_node == paint_order.end()) return; + if (group->rebuild) owner_node->private_data->valid_cache = owner_node->private_data->paint_target; + if (owner_node->private_data->valid_cache) + object->template pending_buffer().composite(*owner_node->private_data->valid_cache); + }).name("render_2d.paint.cache.composite"); + paint_done.precede(composite); + previous = composite; + } + has_previous = true; + } + object->template access_pending_dependency_graph([](auto& paint_state) { if (paint_state.dirty()) paint_state.take_dirty(); }); +} +template +void Render_Scene_2D::Private::prepare_paint_targets(Object*, Blend2D_Cache& frame, Size viewport) { + for (auto& group : cache_groups) { + const auto owner_node = std::find_if(paint_order.begin(), paint_order.end(), [&](const Paint_Node& value) { return value.object == group.owner; }); + if (owner_node == paint_order.end() || !owner_node->private_data->pending_cache) + throw std::logic_error("2D cache group lost its cache owner capability"); + group.rebuild = owner_node->private_data->valid_cache == nullptr; + for (Root* member : group.members) { + const auto node = std::find_if(paint_order.begin(), paint_order.end(), [member](const Paint_Node& value) { return value.object == member; }); + if (node == paint_order.end()) continue; + group.rebuild = group.rebuild || member->template dirty(); + if (node->private_data->dispatch->paint.builder) { + group.rebuild = group.rebuild || !node->private_data->paint_graph_built || node->private_data->dispatch->paint.rebuild_predicate(member); + } + } + if (group.rebuild) { + auto* target = owner_node->private_data->pending_cache(group.owner); + target->ensure_size(viewport); + target->clear(); + for (Root* member : group.members) { + member->template mark_dirty(); + const auto node = std::find_if(paint_order.begin(), paint_order.end(), [member](const Paint_Node& value) { return value.object == member; }); + if (node != paint_order.end()) { node->private_data->paint_target = target; node->paint_requested = true; } + } + } else { + for (Root* member : group.members) { + const auto node = std::find_if(paint_order.begin(), paint_order.end(), [member](const Paint_Node& value) { return value.object == member; }); + if (node != paint_order.end()) { + node->private_data->paint_target = owner_node->private_data->valid_cache; + node->paint_requested = false; + } + } + } + } + for (auto& node : paint_order) { + if (node.cache_owner) continue; + node.private_data->paint_target = &frame; + node.paint_requested = true; + node.object->template mark_dirty(); + } +} template void Render_Scene_2D::Private::process(Object* object, Callback&& callback) requires std::invocable { @@ -42,9 +205,20 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback) if (state.viewport.empty()) { return; } - object->template current_dependency_graph().for_each( - [](const Dependency_Graph::Node& node) { node.object->template mark_dirty(); }); + object->template current_dependency_graph().for_each([](const Dependency_Graph::Node& node) { + if (!node.object->template take_dirty()) return; + node.object->template mark_dirty(); + node.object->template mark_dirty(); + }); + std::vector prepare_executions; + object->template current_dependency_graph().for_each_bound([&](Renderable* renderable, Renderable::Private& data) { + bool execute = renderable->template dirty(); + if (data.dispatch->prepare.builder) + execute = execute || !data.prepare_graph_built || data.dispatch->prepare.rebuild_predicate(renderable); + if (execute) prepare_executions.push_back(renderable); + }); Prev_Private::process(object, [&](const Scene::Private::Result&) { + for (auto* renderable : prepare_executions) renderable->template mark_dirty(); auto& frame = object->template pending_buffer(); frame.ensure_size(state.viewport); frame.clear(); @@ -54,16 +228,8 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback) static_cast(state.viewport.height)}, Pen{.style = Line_Style::none}, Brush{state.background, Brush_Style::solid}); } - const auto graph = object->template current_dependency_graph(); - const auto result = graph.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { - auto* private_data = view.private_data(node); - if (!private_data || !private_data->color_cache_visit) return; - private_data->color_cache_visit(node.object, &frame, [](void* context, const Color_Cache& cache) { - auto& destination = *static_cast(context); - if (const auto* layer = dynamic_cast(&cache)) destination.composite(*layer); - }); - }); - if (!result) throw std::logic_error("render scene paint graph became invalid during composition"); + prepare_paint_targets(object, frame, state.viewport); + if (paint_taskflow && !paint_taskflow->empty()) aethera::detail::run_taskflow(*paint_taskflow); }); std::invoke(std::forward(callback)); } diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index c49ab8b..d927a3a 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include namespace { using namespace aethera; @@ -54,21 +54,23 @@ TEST(render_cache, copy_owns_independent_pixels) { } EXPECT_TRUE(contains_color); } -TEST(axis_render, scene_prepares_and_paints_axis_cache) { +TEST(axis_render, scene_prepares_and_paints_uncached_axis_into_frame) { using Object = Impl; - using Scene_Object = Impl; + using Scene_Object = Impl; initialize_runtime(2); auto axis = build_axis(); auto scene = build_axis(); axis->set<&Abs_Axis::Prop::position>(Point_F{8.0, 8.0}); axis->set<&Abs_Axis::Prop::canvas_size>(Size{160, 64}); axis->set<&Abs_Axis::Prop::pixel_length>(120.0); + scene->set<&Render_Scene_2D::Prop::viewport>(Size{160, 64}); + scene->activate_view(); ASSERT_TRUE((scene->edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(axis.get()); paint.add(axis.get()); }).has_value())); - scene->process([](const auto&) {}); - const Image_View view = axis->pending_buffer().view(); + scene->render(); + const Image_View view = scene->frame_view(); ASSERT_FALSE(view.empty()); bool contains_color{}; for (int y = 0; y < view.height && !contains_color; ++y) { diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index 1063390..f735d92 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -107,6 +107,13 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), scene.get())); EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), frequency.get())); EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), power.get())); + const auto paint_graph = scene->pending_dependency_graph(); + EXPECT_TRUE(paint_graph.depends_on(frequency.get(), spectrum.get())); + EXPECT_TRUE(paint_graph.depends_on(power.get(), spectrum.get())); + const auto cache_graph = scene->pending_dependency_graph(); + EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), scene.get())); + EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), frequency.get())); + EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), power.get())); scene->render(); EXPECT_GT(spectrum->rendered_point_count(), 0u); const auto& render_state = spectrum->read_state(); @@ -115,6 +122,24 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { const Image_View frame = scene->frame_view(); ASSERT_FALSE(frame.empty()); EXPECT_TRUE(contains_color(frame)); + EXPECT_FALSE(spectrum->dirty()); + EXPECT_FALSE(spectrum->dirty()); + EXPECT_FALSE(frequency->dirty()); + EXPECT_FALSE(power->dirty()); + scene->render(); + EXPECT_TRUE(contains_color(scene->frame_view())); + frequency->set<&Numeric_Axis::Prop::precision>(3); + EXPECT_FALSE(spectrum->dirty()); + scene->render(); + EXPECT_TRUE(spectrum->read_state().paint_executed); + EXPECT_TRUE(frequency->read_state().paint_executed); + EXPECT_TRUE(power->read_state().paint_executed); + EXPECT_TRUE(contains_color(scene->frame_view())); + frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 120.0}); + EXPECT_TRUE(spectrum->dirty()); + scene->render(); + EXPECT_FALSE(spectrum->dirty()); + EXPECT_TRUE(spectrum->read_state().prepare_executed); spectrum->set<&Spectrum::Prop::partition_count>(2u); spectrum->update_samples(samples); scene->render(); @@ -126,7 +151,12 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas); frequency->set<&Abs_Axis::Prop::canvas_size>(resized_canvas); power->set<&Abs_Axis::Prop::canvas_size>(resized_canvas); + EXPECT_TRUE(frequency->dirty()); + EXPECT_TRUE(power->dirty()); scene->render(); + EXPECT_EQ(scene->read_prop().viewport, resized_canvas); + EXPECT_TRUE(frequency->read_state().prepare_executed); + EXPECT_TRUE(frequency->read_state().paint_executed); const Image_View resized_frame = scene->frame_view(); EXPECT_EQ(resized_frame.width, resized_canvas.width); EXPECT_EQ(resized_frame.height, resized_canvas.height); diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index 068d375..6c86d0d 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -1,4 +1,4 @@ -#include "Render_Scene_3D.hpp" +#include "Render_Scene_3D.hpp" /* 三维异步提交阶段实现。 */ namespace aethera::render_3d { bool Render_Scene_3D::Prop::operator==(const Prop&) const = default; bool Render_Scene_3D::State::operator==(const State&) const = default; diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index dab1ad2..cb9764b 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -6,8 +6,10 @@ #include #include namespace aethera::render_3d { -/* 执行 3D Renderable 图,并把 Paint 阶段无等待地提交给 Datoviz 渲染域。 */ -struct Render_Scene_3D : Def { +/* 三维 Prepare 完成后的异步后端提交阶段标签。 */ +struct Submit_Tag {}; +/* 执行共有 Prepare 图,并把三维 Submit 阶段无等待地提交给 Datoviz 渲染域。 */ +struct Render_Scene_3D : Def> { struct Prop : Prev_Prop { Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */ Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */ @@ -29,7 +31,7 @@ struct Render_Scene_3D : Def { Root* visual{}; /* 不拥有的唯一 Visual;生命周期必须覆盖 Scene。 */ Visual_Family visual_family{Visual_Family::point}; /* Visual 编译期 Spec 对应的后端 family。 */ void (*bind_visual)(Root*, std::shared_ptr){}; /* 把 Scene 拥有的弱提交上下文绑定到最终 Visual Private。 */ - Attach_Visual attach_visual{}; /* 以具体 Visual 类型安装 Prepare、Paint 和 Prop 依赖。 */ + Attach_Visual attach_visual{}; /* 以具体 Visual 类型安装 Prepare 与三维 Submit 节点。 */ std::uint32_t gpu_index{}; /* Datoviz 使用的物理 GPU 下标。 */ bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */ }; diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index 6170f93..2240af6 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -27,19 +27,19 @@ struct Render_Scene_3D::Private : Prev_Private { template void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family); /* CRTP 覆盖:绑定 Scene 机制和 Render_Scene_3D 公开薄壳。 */ template void bind_private_crtp(Object* object); - /* CRTP 覆盖:执行 Kernel Scene Taskflow;其 Paint 子图只负责异步入队。 */ + /* CRTP 覆盖:先执行 Kernel Prepare Taskflow,再运行只负责异步入队的三维 Submit 阶段。 */ template void process(Object* object, Callback&& callback) requires std::invocable; template [[nodiscard]] static const Dispatch& dispatch_for(); }; template template Render_Scene_3D::Builder::Builder(Visual_Object* visual_value, std::uint32_t gpu_index_value, bool validation_enabled_value) : Base(), visual(visual_value), visual_family(Visual_Object::Attached_Object::Specification::family), gpu_index(gpu_index_value), validation_enabled(validation_enabled_value) { bind_visual = [](Root* root, std::shared_ptr context) { auto* object = static_cast(root); Base::private_access(object).template get().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast*>(raw_context); const auto& prop = submission.scene->template read_prop(); submission.backend->render(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); }); }; - attach_visual = [](Object* scene, Root* root) { auto* object = static_cast(root); return scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { prepare.add(object); paint.add(object); prepare.template add_prop_dependency(object, scene); }); }; + attach_visual = [](Object* scene, Root* root) { auto* object = static_cast(root); return scene->template edit_dependency_graph([&](auto& prepare, auto& submit) { prepare.add(object); submit.add(object); }); }; } template std::expected, Dependency_Graph_Error> Render_Scene_3D::Builder::build() { if (!visual || !bind_visual || !attach_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual"); auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto scene = std::move(result).value(); auto& private_data = Base::private_access(scene.get()).template get(); private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, visual_family); bind_visual(visual, private_data.paint_context); auto graph_result = attach_visual(scene.get(), visual); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(scene); } template void Render_Scene_3D::Private::initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family) { const auto& prop = object->template read_prop(); backend = std::make_shared(gpu_index, validation_enabled, visual_family, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); paint_context = std::make_shared>(detail::Scene_Paint_Context{backend, object}); } -template void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable { const auto& prop = object->template read_prop(); if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return; Prev_Private::process(object, [](const Scene::Private::Result&) {}); std::invoke(std::forward(callback)); } +template void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable { const auto& prop = object->template read_prop(); if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return; Prev_Private::process(object, [&](const Scene::Private::Result&) { const auto prepare = object->template current_dependency_graph(); prepare.for_each_bound([](Renderable* renderable, Renderable::Private& data) { if (data.dispatch->state.get(renderable)->prepare_executed) renderable->template mark_dirty(); }); const auto submit = object->template current_dependency_graph(); const auto result = submit.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { auto* data = view.private_data(node); if (!data) return; Root* root = node.object; auto* dispatch = data->dispatch; auto& state = *dispatch->state.get(root); state.paint_graph_rebuilt = false; state.paint_execution_time_ns = 0; const bool dirty = root->template dirty(); state.paint_dirty = dirty; state.paint_executed = dispatch->paint.predicate(root, dirty); if (!state.paint_executed) return; if (dispatch->paint.builder) { if (!data->paint_graph) data->paint_graph = std::make_unique(); const bool rebuild = dispatch->paint.rebuild_predicate(root); if (!data->paint_graph_built || rebuild) { *data->paint_graph = dispatch->paint.builder(root); data->paint_graph_built = true; state.paint_graph_rebuilt = true; } state.paint_task_count = data->paint_graph->num_tasks(); if (!data->paint_graph->empty()) aethera::detail::run_taskflow(*data->paint_graph); } else { state.paint_task_count = dispatch->paint.run ? 1 : 0; if (dispatch->paint.run) dispatch->paint.run(root); } root->template take_dirty(); dispatch->state.notify(root); }); if (!result) throw std::logic_error("render scene submit graph became invalid during submission"); }); std::invoke(std::forward(callback)); } template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root) { auto* object = static_cast(root); object->process([] {}); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); auto& data = static_cast(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, const Event& event) { auto* object = static_cast(root); auto& data = static_cast(*object->d); if (!data.backend) return Dispatch_Event_Result::backend_unavailable; const auto viewport = object->template read_prop().viewport; switch (data.backend->dispatch_event(event, viewport)) { case detail::Async_Render_Backend::Dispatch_Event_Result::dispatched: return Dispatch_Event_Result::dispatched; case detail::Async_Render_Backend::Dispatch_Event_Result::ignored: return Dispatch_Event_Result::ignored; case detail::Async_Render_Backend::Dispatch_Event_Result::invalid_event: return Dispatch_Event_Result::invalid_event; case detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable: return Dispatch_Event_Result::backend_unavailable; } return Dispatch_Event_Result::backend_unavailable; }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } template void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } } diff --git a/web_server/src/Graph_Metadata.cpp b/web_server/src/Graph_Metadata.cpp index b408eef..efe3c34 100644 --- a/web_server/src/Graph_Metadata.cpp +++ b/web_server/src/Graph_Metadata.cpp @@ -1,17 +1,5 @@ -#include "Graph_Metadata.hpp" +#include "Graph_Metadata.hpp" /* PFR 属性与状态描述。 */ #include -namespace { -struct State_Field_Metadata { - template auto operator()(Field field) const { - if constexpr (Index == 0) return field.label("Frame sequence").description("Latest completed frame sent by this graph."); - if constexpr (Index == 1) return field.label("Input elements").description("Primary published element count read from the graph State."); - return field.label("Rendered elements").description("Prepared render element count read from the graph State."); - } -}; -} -namespace adminive { -auto Type_Descriptor::get() { return reflected_object_with("graph_state", "Published graph state", State_Field_Metadata{}); } -} namespace aethera::web { const std::vector& graph_catalog() { static const std::vector value{ @@ -28,16 +16,26 @@ const std::vector& graph_catalog() { } const Graph_Descriptor* find_graph(std::string_view graph_id) { const auto& catalog = graph_catalog(); const auto found = std::ranges::find(catalog, graph_id, &Graph_Descriptor::id); return found == catalog.end() ? nullptr : &*found; } nlohmann::json graph_catalog_json() { nlohmann::json result = nlohmann::json::array(); for (const auto& graph : graph_catalog()) result.push_back({{"id", graph.id}, {"title", graph.title}, {"category", graph.category}, {"description", graph.description}, {"dimension", graph.three_dimensional ? "3D" : "2D"}, {"websocket", "/ws/graphs/" + graph.id}, {"state", "/api/graphs/" + graph.id + "/state"}, {"descriptor", "/api/graphs/" + graph.id + "/descriptor"}}); return result; } -nlohmann::json graph_state_descriptor_json() { return adminive::to_descriptor_json(); } -nlohmann::json graph_state_json(const Graph_State_Document& state) { return adminive::model_to_json(state, true); } +nlohmann::json graph_state_descriptor_json(std::string_view graph_id) { + using namespace render_2d; + if (graph_id == "spectrum") return adminive::to_descriptor_json(); + if (graph_id == "frequency_trace") return adminive::to_descriptor_json(); + if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json(); + if (graph_id == "afterglow") return adminive::to_descriptor_json(); + if (graph_id == "waterfall") return adminive::to_descriptor_json(); + if (graph_id == "constellation") return adminive::to_descriptor_json(); + if (graph_id == "selection_overlay") return adminive::to_descriptor_json(); + return {{"fields", nlohmann::json::array()}}; +} nlohmann::json graph_prop_descriptor_json(std::string_view graph_id) { using namespace render_2d; - if (graph_id == "spectrum") return adminive::to_descriptor_json(); - if (graph_id == "frequency_trace") return adminive::to_descriptor_json(); - if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json(); - if (graph_id == "afterglow") return adminive::to_descriptor_json(); - if (graph_id == "waterfall") return adminive::to_descriptor_json(); - if (graph_id == "constellation") return adminive::to_descriptor_json(); + if (graph_id == "spectrum") return adminive::to_descriptor_json(); + if (graph_id == "frequency_trace") return adminive::to_descriptor_json(); + if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json(); + if (graph_id == "afterglow") return adminive::to_descriptor_json(); + if (graph_id == "waterfall") return adminive::to_descriptor_json(); + if (graph_id == "constellation") return adminive::to_descriptor_json(); + if (graph_id == "selection_overlay") return adminive::to_descriptor_json(); return {{"fields", nlohmann::json::array()}}; } } diff --git a/web_server/src/Graph_Metadata.hpp b/web_server/src/Graph_Metadata.hpp index 4d1b08a..22e469a 100644 --- a/web_server/src/Graph_Metadata.hpp +++ b/web_server/src/Graph_Metadata.hpp @@ -1,20 +1,14 @@ #pragma once #include -#include #include +#include #include #include -#include #include #include #include #include namespace aethera::web { -namespace detail { -struct Editable_Prop_Field_Metadata { - template auto operator()(Field field) const { return field.editable(); } -}; -} struct Graph_Descriptor { std::string id{}; /* HTTP 与 WebSocket 使用的稳定图标识。 */ std::string title{}; /* Gallery 卡片显示名称。 */ @@ -22,29 +16,10 @@ struct Graph_Descriptor { std::string description{}; /* 图用途和数据语义说明。 */ bool three_dimensional{}; /* 是否使用 Datoviz 3D 后端。 */ }; -struct Graph_State_Document { - std::uint64_t frame_sequence{}; /* 最近完成并发送的帧序号。 */ - std::uint64_t primary_count{}; /* 图自身发布的主要输入元素数量。 */ - std::uint64_t rendered_count{}; /* 最近 Prepare 发布的绘制元素数量。 */ -}; [[nodiscard]] const std::vector& graph_catalog(); [[nodiscard]] const Graph_Descriptor* find_graph(std::string_view graph_id); [[nodiscard]] nlohmann::json graph_catalog_json(); -[[nodiscard]] nlohmann::json graph_state_descriptor_json(); -[[nodiscard]] nlohmann::json graph_state_json(const Graph_State_Document& state); +[[nodiscard]] nlohmann::json graph_state_descriptor_json(std::string_view graph_id); [[nodiscard]] nlohmann::json graph_prop_descriptor_json(std::string_view graph_id); } -namespace adminive { -template <> struct Reflection_Adapter : Boost_Pfr_Reflection_Adapter {}; -template <> struct Type_Descriptor { static auto get(); }; -#define AETHERA_DECLARE_PROP_REFLECTION(Type, Id, Label) \ -template <> struct Reflection_Adapter : Boost_Pfr_Reflection_Adapter {}; \ -template <> struct Type_Descriptor { static auto get() { return reflected_object_with(Id, Label, aethera::web::detail::Editable_Prop_Field_Metadata{}); } }; -AETHERA_DECLARE_PROP_REFLECTION(Spectrum_Editable_Prop_Data, "spectrum_prop", "Spectrum properties") -AETHERA_DECLARE_PROP_REFLECTION(Frequency_Trace_Editable_Prop_Data, "frequency_trace_prop", "Frequency trace properties") -AETHERA_DECLARE_PROP_REFLECTION(Sweep_Spectrum_Editable_Prop_Data, "sweep_spectrum_prop", "Sweep spectrum properties") -AETHERA_DECLARE_PROP_REFLECTION(Afterglow_Editable_Prop_Data, "afterglow_prop", "Afterglow properties") -AETHERA_DECLARE_PROP_REFLECTION(Waterfall_Editable_Prop_Data, "waterfall_prop", "Waterfall properties") -AETHERA_DECLARE_PROP_REFLECTION(Constellation_Diagram_Editable_Prop_Data, "constellation_prop", "Constellation properties") -#undef AETHERA_DECLARE_PROP_REFLECTION -} +#include "Graph_Metadata.ipp" /* 模板和反射特化实现。 */ diff --git a/web_server/src/Graph_Metadata.ipp b/web_server/src/Graph_Metadata.ipp new file mode 100644 index 0000000..36d28e3 --- /dev/null +++ b/web_server/src/Graph_Metadata.ipp @@ -0,0 +1,113 @@ +#pragma once +#include +#include +#include +#include +namespace aethera::web::detail { +struct Editable_Field_Metadata { + template auto operator()(Field field) const; +}; +template +struct Member_Pfr_Reflection_Adapter { + static constexpr std::size_t field_count = boost::pfr::tuple_size_v; + template static decltype(auto) get(Object& value) noexcept; + template static decltype(auto) get(const Object& value) noexcept; + template static constexpr std::string_view name() noexcept; +}; +#define AETHERA_MEMBER(Type, Name) decltype(&Type::Name) Name{&Type::Name} +using namespace render_2d; +struct Spectrum_Prop_Members { AETHERA_MEMBER(Spectrum::Prop, center_frequency); AETHERA_MEMBER(Spectrum::Prop, partition_count); AETHERA_MEMBER(Spectrum::Prop, max_hold_visible); AETHERA_MEMBER(Spectrum::Prop, min_hold_visible); AETHERA_MEMBER(Spectrum::Prop, max_marker_visible); AETHERA_MEMBER(Spectrum::Prop, min_marker_visible); AETHERA_MEMBER(Spectrum::Prop, sweep_region_visible); AETHERA_MEMBER(Spectrum::Prop, visible_range_only); AETHERA_MEMBER(Spectrum::Prop, frequency_range); AETHERA_MEMBER(Spectrum::Prop, sweep_frequency_range); AETHERA_MEMBER(Spectrum::Prop, partition_mode); AETHERA_MEMBER(Spectrum::Prop, interpolation_mode); AETHERA_MEMBER(Spectrum::Prop, max_brush); AETHERA_MEMBER(Spectrum::Prop, current_brush); AETHERA_MEMBER(Spectrum::Prop, min_brush); AETHERA_MEMBER(Spectrum::Prop, max_pen); AETHERA_MEMBER(Spectrum::Prop, current_pen); AETHERA_MEMBER(Spectrum::Prop, min_pen); AETHERA_MEMBER(Spectrum::Prop, selected_marker_pen); AETHERA_MEMBER(Spectrum::Prop, marker_pen); AETHERA_MEMBER(Spectrum::Prop, middle_frequency_pen); AETHERA_MEMBER(Spectrum::Prop, sweep_region_brush); AETHERA_MEMBER(Spectrum::Prop, custom_markers); AETHERA_MEMBER(Spectrum::Prop, selected_marker); }; +struct Frequency_Trace_Prop_Members { AETHERA_MEMBER(Frequency_Trace::Prop, partition_count); AETHERA_MEMBER(Frequency_Trace::Prop, pen); AETHERA_MEMBER(Frequency_Trace::Prop, partition_mode); AETHERA_MEMBER(Frequency_Trace::Prop, samples); }; +struct Sweep_Spectrum_Prop_Members { AETHERA_MEMBER(Sweep_Spectrum::Prop, bins_per_block); AETHERA_MEMBER(Sweep_Spectrum::Prop, block_count); AETHERA_MEMBER(Sweep_Spectrum::Prop, partition_count); AETHERA_MEMBER(Sweep_Spectrum::Prop, visible_range_only); AETHERA_MEMBER(Sweep_Spectrum::Prop, frequency_range); AETHERA_MEMBER(Sweep_Spectrum::Prop, partition_mode); AETHERA_MEMBER(Sweep_Spectrum::Prop, pen); AETHERA_MEMBER(Sweep_Spectrum::Prop, current_frequency_pen); AETHERA_MEMBER(Sweep_Spectrum::Prop, interpolation_mode); AETHERA_MEMBER(Sweep_Spectrum::Prop, blocks); }; +struct Afterglow_Prop_Members { AETHERA_MEMBER(Afterglow::Prop, frequency_point_size); AETHERA_MEMBER(Afterglow::Prop, power_point_size); AETHERA_MEMBER(Afterglow::Prop, partition_count); AETHERA_MEMBER(Afterglow::Prop, interpolate); AETHERA_MEMBER(Afterglow::Prop, attenuation_rate); AETHERA_MEMBER(Afterglow::Prop, frequency_range); AETHERA_MEMBER(Afterglow::Prop, power_range); AETHERA_MEMBER(Afterglow::Prop, partition_mode); AETHERA_MEMBER(Afterglow::Prop, color_map); AETHERA_MEMBER(Afterglow::Prop, spectra); }; +struct Waterfall_Prop_Members { AETHERA_MEMBER(Waterfall::Prop, tooltip_enabled); AETHERA_MEMBER(Waterfall::Prop, tooltip_font); AETHERA_MEMBER(Waterfall::Prop, tooltip_text_pen); AETHERA_MEMBER(Waterfall::Prop, tooltip_background_brush); AETHERA_MEMBER(Waterfall::Prop, frequency_bin_count); AETHERA_MEMBER(Waterfall::Prop, partition_count); AETHERA_MEMBER(Waterfall::Prop, visible_range_only); AETHERA_MEMBER(Waterfall::Prop, frequency_range); AETHERA_MEMBER(Waterfall::Prop, power_range); AETHERA_MEMBER(Waterfall::Prop, partition_mode); AETHERA_MEMBER(Waterfall::Prop, interpolation_mode); AETHERA_MEMBER(Waterfall::Prop, color_map); AETHERA_MEMBER(Waterfall::Prop, rows); }; +struct Constellation_Prop_Members { AETHERA_MEMBER(Constellation_Diagram::Prop, point_lifetime_ms); AETHERA_MEMBER(Constellation_Diagram::Prop, type); AETHERA_MEMBER(Constellation_Diagram::Prop, phase_offset_radians); AETHERA_MEMBER(Constellation_Diagram::Prop, i_range); AETHERA_MEMBER(Constellation_Diagram::Prop, q_range); AETHERA_MEMBER(Constellation_Diagram::Prop, point_color); AETHERA_MEMBER(Constellation_Diagram::Prop, anchor_color); AETHERA_MEMBER(Constellation_Diagram::Prop, points); }; +struct Selection_Prop_Members { AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, label_font); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, label_pen); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, selection_brush); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, selection_border_pen); AETHERA_MEMBER(Selection_Rectangle_Overlay::Prop, selected_regions); }; +struct Color_Map_Members { AETHERA_MEMBER(Color_Map, stops); }; +#define AETHERA_RENDER_STATE_MEMBERS(Type) AETHERA_MEMBER(Type, prepare_dirty); AETHERA_MEMBER(Type, paint_dirty); AETHERA_MEMBER(Type, prepare_executed); AETHERA_MEMBER(Type, paint_executed); AETHERA_MEMBER(Type, prepare_graph_rebuilt); AETHERA_MEMBER(Type, paint_graph_rebuilt); AETHERA_MEMBER(Type, prepare_task_count); AETHERA_MEMBER(Type, paint_task_count); AETHERA_MEMBER(Type, prepare_execution_time_ns); AETHERA_MEMBER(Type, paint_execution_time_ns) +struct Spectrum_State_Members { AETHERA_RENDER_STATE_MEMBERS(Spectrum::State); AETHERA_MEMBER(Spectrum::State, sample_count); AETHERA_MEMBER(Spectrum::State, rendered_point_count); AETHERA_MEMBER(Spectrum::State, selectable_marker_count); }; +struct Frequency_Trace_State_Members { AETHERA_RENDER_STATE_MEMBERS(Frequency_Trace::State); AETHERA_MEMBER(Frequency_Trace::State, sample_count); AETHERA_MEMBER(Frequency_Trace::State, rendered_point_count); }; +struct Sweep_Spectrum_State_Members { AETHERA_RENDER_STATE_MEMBERS(Sweep_Spectrum::State); AETHERA_MEMBER(Sweep_Spectrum::State, stored_block_count); AETHERA_MEMBER(Sweep_Spectrum::State, stored_point_count); AETHERA_MEMBER(Sweep_Spectrum::State, rendered_point_count); }; +struct Afterglow_State_Members { AETHERA_RENDER_STATE_MEMBERS(Afterglow::State); AETHERA_MEMBER(Afterglow::State, history_count); AETHERA_MEMBER(Afterglow::State, latest_spectrum_point_count); AETHERA_MEMBER(Afterglow::State, rendered_cell_count); }; +struct Waterfall_State_Members { AETHERA_RENDER_STATE_MEMBERS(Waterfall::State); AETHERA_MEMBER(Waterfall::State, row_count); AETHERA_MEMBER(Waterfall::State, stored_point_count); AETHERA_MEMBER(Waterfall::State, rendered_cell_count); }; +struct Constellation_State_Members { AETHERA_RENDER_STATE_MEMBERS(Constellation_Diagram::State); AETHERA_MEMBER(Constellation_Diagram::State, point_count); }; +struct Selection_State_Members { AETHERA_RENDER_STATE_MEMBERS(Selection_Rectangle_Overlay::State); AETHERA_MEMBER(Selection_Rectangle_Overlay::State, selected_region_count); }; +#undef AETHERA_RENDER_STATE_MEMBERS +#undef AETHERA_MEMBER +} +namespace adminive { +template struct Value_Adapter, Json> { + using value_type = std::vector; + static constexpr std::string_view type_name{"array"}; + static Json encode(const value_type& values); + static void decode(value_type& target, const Json& value); +}; +#define AETHERA_PLAIN_REFLECTION(Type, Id, Label) template <> struct Reflection_Adapter : Boost_Pfr_Reflection_Adapter {}; template <> struct Type_Descriptor { static auto get(); }; +AETHERA_PLAIN_REFLECTION(aethera::Color, "color", "Color") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Point_F, "point", "Point") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Rect_F, "rectangle", "Rectangle") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Axis_Range, "axis_range", "Axis range") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Pen, "pen", "Pen") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Brush, "brush", "Brush") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Font, "font", "Font") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Frequency_Trace_Sample, "frequency_trace_sample", "Frequency trace sample") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Waterfall_Row, "waterfall_row", "Waterfall row") +AETHERA_PLAIN_REFLECTION(aethera::render_2d::Constellation_Point, "constellation_point", "Constellation point") +#undef AETHERA_PLAIN_REFLECTION +#define AETHERA_MEMBER_REFLECTION(Type, Members, Id, Label, Customizer) template <> struct Reflection_Adapter : aethera::web::detail::Member_Pfr_Reflection_Adapter {}; template <> struct Type_Descriptor { static auto get(); }; +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Color_Map, Color_Map_Members, "color_map", "Color map", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Spectrum::Prop, Spectrum_Prop_Members, "spectrum_prop", "Spectrum properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Frequency_Trace::Prop, Frequency_Trace_Prop_Members, "frequency_trace_prop", "Frequency trace properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Sweep_Spectrum::Prop, Sweep_Spectrum_Prop_Members, "sweep_spectrum_prop", "Sweep spectrum properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Afterglow::Prop, Afterglow_Prop_Members, "afterglow_prop", "Afterglow properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Waterfall::Prop, Waterfall_Prop_Members, "waterfall_prop", "Waterfall properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Constellation_Diagram::Prop, Constellation_Prop_Members, "constellation_prop", "Constellation properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Selection_Rectangle_Overlay::Prop, Selection_Prop_Members, "selection_prop", "Selection properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Spectrum::State, Spectrum_State_Members, "spectrum_state", "Spectrum state", Identity_Field_Customizer) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Frequency_Trace::State, Frequency_Trace_State_Members, "frequency_trace_state", "Frequency trace state", Identity_Field_Customizer) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Sweep_Spectrum::State, Sweep_Spectrum_State_Members, "sweep_spectrum_state", "Sweep spectrum state", Identity_Field_Customizer) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Afterglow::State, Afterglow_State_Members, "afterglow_state", "Afterglow state", Identity_Field_Customizer) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Waterfall::State, Waterfall_State_Members, "waterfall_state", "Waterfall state", Identity_Field_Customizer) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Constellation_Diagram::State, Constellation_State_Members, "constellation_state", "Constellation state", Identity_Field_Customizer) +AETHERA_MEMBER_REFLECTION(aethera::render_2d::Selection_Rectangle_Overlay::State, Selection_State_Members, "selection_state", "Selection state", Identity_Field_Customizer) +#undef AETHERA_MEMBER_REFLECTION +} +namespace aethera::web::detail { +template auto Editable_Field_Metadata::operator()(Field field) const { return field.editable(); } +template template decltype(auto) Member_Pfr_Reflection_Adapter::get(Object& value) noexcept { return value.*boost::pfr::get(Members{}); } +template template decltype(auto) Member_Pfr_Reflection_Adapter::get(const Object& value) noexcept { return value.*boost::pfr::get(Members{}); } +template template constexpr std::string_view Member_Pfr_Reflection_Adapter::name() noexcept { return boost::pfr::get_name(); } +} +namespace adminive { +template Json Value_Adapter, Json>::encode(const value_type& values) { Json result = json_array(); for (const auto& value : values) json_append(result, encode_json_value(value)); return result; } +template void Value_Adapter, Json>::decode(value_type& target, const Json& value) { if (!Json_Adapter::is_array(value)) throw std::invalid_argument("value must be an array"); value_type updated; updated.reserve(Json_Adapter::size(value)); for (std::size_t index = 0; index < Json_Adapter::size(value); ++index) { Value item{}; assign_json_value(item, Json_Adapter::at(value, index)); updated.push_back(std::move(item)); } target = std::move(updated); } +#define AETHERA_DEFINE_PLAIN_DESCRIPTOR(Type, Id, Label) inline auto Type_Descriptor::get() { return reflected_object_with(Id, Label, aethera::web::detail::Editable_Field_Metadata{}); } +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::Color, "color", "Color") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Point_F, "point", "Point") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Rect_F, "rectangle", "Rectangle") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Axis_Range, "axis_range", "Axis range") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Pen, "pen", "Pen") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Brush, "brush", "Brush") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Font, "font", "Font") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Frequency_Trace_Sample, "frequency_trace_sample", "Frequency trace sample") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Waterfall_Row, "waterfall_row", "Waterfall row") +AETHERA_DEFINE_PLAIN_DESCRIPTOR(aethera::render_2d::Constellation_Point, "constellation_point", "Constellation point") +#undef AETHERA_DEFINE_PLAIN_DESCRIPTOR +#define AETHERA_DEFINE_MEMBER_DESCRIPTOR(Type, Id, Label, Customizer) inline auto Type_Descriptor::get() { return reflected_object_with(Id, Label, Customizer{}); } +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Color_Map, "color_map", "Color map", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Spectrum::Prop, "spectrum_prop", "Spectrum properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Frequency_Trace::Prop, "frequency_trace_prop", "Frequency trace properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Sweep_Spectrum::Prop, "sweep_spectrum_prop", "Sweep spectrum properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Afterglow::Prop, "afterglow_prop", "Afterglow properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Waterfall::Prop, "waterfall_prop", "Waterfall properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Constellation_Diagram::Prop, "constellation_prop", "Constellation properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Selection_Rectangle_Overlay::Prop, "selection_prop", "Selection properties", aethera::web::detail::Editable_Field_Metadata) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Spectrum::State, "spectrum_state", "Spectrum state", Identity_Field_Customizer) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Frequency_Trace::State, "frequency_trace_state", "Frequency trace state", Identity_Field_Customizer) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Sweep_Spectrum::State, "sweep_spectrum_state", "Sweep spectrum state", Identity_Field_Customizer) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Afterglow::State, "afterglow_state", "Afterglow state", Identity_Field_Customizer) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Waterfall::State, "waterfall_state", "Waterfall state", Identity_Field_Customizer) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Constellation_Diagram::State, "constellation_state", "Constellation state", Identity_Field_Customizer) +AETHERA_DEFINE_MEMBER_DESCRIPTOR(aethera::render_2d::Selection_Rectangle_Overlay::State, "selection_state", "Selection state", Identity_Field_Customizer) +#undef AETHERA_DEFINE_MEMBER_DESCRIPTOR +} diff --git a/web_server/src/Graph_Session.cpp b/web_server/src/Graph_Session.cpp index ea24810..d1044de 100644 --- a/web_server/src/Graph_Session.cpp +++ b/web_server/src/Graph_Session.cpp @@ -1,4 +1,4 @@ -#include "Graph_Session.hpp" +#include "Graph_Session.hpp" /* 图对象异步会话。 */ #include #include #include @@ -48,14 +48,16 @@ struct Plot_2D { std::unique_ptr time{}; /* 时间轴;不用时为空。 */ std::unique_ptr plot{}; /* 具体 Plottable 的唯一所有权。 */ std::function update{}; /* 根据浏览器时钟更新权威 Prop。 */ - std::function state{}; /* 从具体 State 即时计算 HTTP 文档。 */ + std::function state{}; /* 直接遍历具体图完整 State 的即时 HTTP 文档。 */ std::function prop{}; std::function patch_prop{}; }; -template +template void bind_prop_api(Plot_2D& plot, Object* object) { - plot.prop = [object] { const auto& prop = object->template read_prop(); return adminive::model_to_json(static_cast(prop), true); }; - plot.patch_prop = [object](const nlohmann::json& patch) { Data updated = static_cast(object->template read_prop()); const auto result = adminive::apply_frontend_patch(updated, patch); if (!result.success) return result.to_json(); ([&] { if (object->template get() != updated.*Members) object->template set(updated.*Members); }(), ...); auto output = result.to_json(); output["prop"] = adminive::model_to_json(updated, true); return output; }; + using Prop = typename Definition::Prop; + plot.prop = [object] { return adminive::model_to_json(static_cast(object->template read_prop()), true); }; + plot.state = [object] { return adminive::model_to_json(static_cast(object->template read_state()), true); }; + plot.patch_prop = [object](const nlohmann::json& patch) { Prop updated = static_cast(object->template read_prop()); const auto result = adminive::apply_frontend_patch(updated, patch); if (!result.success) return result.to_json(); ([&] { if (object->template get() != updated.*Members) object->template set(updated.*Members); }(), ...); auto output = result.to_json(); output["prop"] = adminive::model_to_json(static_cast(object->template read_prop()), true); return output; }; } Plot_2D make_plot_2d(std::string_view id) { Plot_2D result; @@ -65,21 +67,24 @@ Plot_2D make_plot_2d(std::string_view id) { result.scene->set<&Render_Scene_2D::Prop::background>(Color{7, 13, 24, 255}); result.scene->activate_view(); auto make_frequency = [&] { result.frequency = build(); configure_axis(result.frequency.get(), Axis_Orientation::horizontal, Point_F{64.0, 370.0}, 620.0, canvas); result.frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); }; - auto make_vertical = [&](Axis_Range range) { result.vertical = build(); configure_axis(result.vertical.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); result.vertical->set<&Numeric_Axis::Prop::coordinate_range>(range); }; + auto make_vertical = [&](Axis_Range range) { + result.vertical = build(); + configure_axis(result.vertical.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); result.vertical->set<&Numeric_Axis::Prop::coordinate_range>(range); + }; if (id == "spectrum") { - make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Spectrum::Prop::max_hold_visible>(true); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast(i) / samples.size(); samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(time * 0.001), 2.0)) + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + 2.5 * std::sin(i * 0.31 + time * 0.004); } raw->update_samples(samples); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.sample_count, state.rendered_point_count}; }; result.plot = std::move(object); + make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Spectrum::Prop::max_hold_visible>(true); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast(i) / samples.size(); samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(time * 0.001), 2.0)) + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + 2.5 * std::sin(i * 0.31 + time * 0.004); } raw->update_samples(samples); }; result.plot = std::move(object); } else if (id == "frequency_trace") { - result.time = build(); configure_axis(result.time.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.time.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { raw->append_sample((*tick)++, std::sin(time * 0.0025) * 0.8 + std::sin(time * 0.0007) * 0.2); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.sample_count, state.rendered_point_count}; }; result.plot = std::move(object); + result.time = build(); configure_axis(result.time.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.time.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { raw->append_sample((*tick)++, std::sin(time * 0.0025) * 0.8 + std::sin(time * 0.0007) * 0.2); }; result.plot = std::move(object); } else if (id == "sweep_spectrum") { - make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -90.0 + 35.0 * std::sin(i * 0.08 + time * 0.002); raw->append_block(values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.stored_block_count, state.rendered_point_count}; }; result.plot = std::move(object); + make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -90.0 + 35.0 * std::sin(i * 0.08 + time * 0.002); raw->append_block(values); }; result.plot = std::move(object); } else if (id == "afterglow") { - make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Afterglow::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Afterglow::Prop::power_range>(Axis_Range{-110.0, 0.0}); object->set<&Afterglow::Prop::power_point_size>(96); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.18 * std::sin(time * 0.0008), 2.0)); raw->append_spectrum(values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.history_count, state.rendered_cell_count}; }; result.plot = std::move(object); + make_frequency(); make_vertical({-110.0, 0.0}); auto object = build>(result.scene.get(), result.frequency.get(), result.vertical.get()); object->set<&Afterglow::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Afterglow::Prop::power_range>(Axis_Range{-110.0, 0.0}); object->set<&Afterglow::Prop::power_point_size>(96); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.18 * std::sin(time * 0.0008), 2.0)); raw->append_spectrum(values); }; result.plot = std::move(object); } else if (id == "waterfall") { - make_frequency(); result.time = build(); configure_axis(result.time.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); auto object = build>(result.scene.get(), result.frequency.get(), result.time.get()); object->set<&Waterfall::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Waterfall::Prop::power_range>(Axis_Range{-110.0, 0.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.22 * std::sin(time * 0.0006), 2.0)); raw->append_row((*tick)++, values); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.row_count, state.rendered_cell_count}; }; result.plot = std::move(object); + make_frequency(); result.time = build(); configure_axis(result.time.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); auto object = build>(result.scene.get(), result.frequency.get(), result.time.get()); object->set<&Waterfall::Prop::frequency_range>(Axis_Range{0.0, 100.0}); object->set<&Waterfall::Prop::power_range>(Axis_Range{-110.0, 0.0}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); auto tick = std::make_shared(); result.update = [raw, tick](double time) { std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow(static_cast(i) / values.size() - 0.5 - 0.22 * std::sin(time * 0.0006), 2.0)); raw->append_row((*tick)++, values); }; result.plot = std::move(object); } else if (id == "constellation") { - result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.2, 1.2}); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); object->set<&Constellation_Diagram::Prop::i_range>(Axis_Range{-1.2, 1.2}); object->set<&Constellation_Diagram::Prop::q_range>(Axis_Range{-1.2, 1.2}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { const double phase = time * 0.003; raw->append_point({std::cos(phase) * 0.82 + 0.04 * std::sin(phase * 7.0), std::sin(phase) * 0.82 + 0.04 * std::cos(phase * 5.0)}); }; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.point_count, state.point_count}; }; result.plot = std::move(object); + result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.2, 1.2}); make_vertical({-1.2, 1.2}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); object->set<&Constellation_Diagram::Prop::i_range>(Axis_Range{-1.2, 1.2}); object->set<&Constellation_Diagram::Prop::q_range>(Axis_Range{-1.2, 1.2}); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [raw](double time) { const double phase = time * 0.003; raw->append_point({std::cos(phase) * 0.82 + 0.04 * std::sin(phase * 7.0), std::sin(phase) * 0.82 + 0.04 * std::cos(phase * 5.0)}); }; result.plot = std::move(object); } else { - result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); make_vertical({0.0, 100.0}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); result.prop = [] { return nlohmann::json::object(); }; result.patch_prop = [](const nlohmann::json&) { return nlohmann::json{{"success", true}, {"prop", nlohmann::json::object()}}; }; result.update = [](double) {}; result.state = [raw](std::uint64_t sequence) { const auto& state = raw->read_state(); return Graph_State_Document{sequence, state.paint_task_count, state.paint_executed ? 1U : 0U}; }; result.plot = std::move(object); + result.horizontal = build(); configure_axis(result.horizontal.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); result.horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); make_vertical({0.0, 100.0}); auto object = build>(result.scene.get(), result.horizontal.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty(); raw->mark_dirty(); bind_prop_api(result, raw); result.update = [](double) {}; result.plot = std::move(object); } return result; } @@ -100,14 +105,14 @@ struct Graph_Session::Private { std::atomic_uint64_t frame_sequence{}; /* 传输协议的完成帧序号。 */ explicit Private(asio::any_io_executor executor, const Graph_Descriptor& graph) : strand(asio::make_strand(std::move(executor))), events(strand, 32), descriptor(graph), plot(make_plot(graph)) {} void publish(std::string pixels) { std::vector outputs; { std::lock_guard lock(handlers_mutex); outputs.reserve(handlers.size()); for (const auto& [owner, handler] : handlers) outputs.push_back(handler); } for (auto& output : outputs) output(pixels); } - Graph_State_Document state_document() const { const auto sequence = frame_sequence.load(std::memory_order_acquire); if (const auto* value = std::get_if(&plot)) return value->state(sequence); const auto& value = std::get(plot); const auto& state = value.visual->read_state(); return {sequence, state.item_count, state.prepared_item_count}; } + nlohmann::json state_document() const { if (const auto* value = std::get_if(&plot)) return value->state(); const auto& state = std::get(plot).visual->read_state(); return {{"prepare_dirty", state.prepare_dirty}, {"paint_dirty", state.paint_dirty}, {"prepare_executed", state.prepare_executed}, {"paint_executed", state.paint_executed}, {"prepare_graph_rebuilt", state.prepare_graph_rebuilt}, {"paint_graph_rebuilt", state.paint_graph_rebuilt}, {"prepare_task_count", state.prepare_task_count}, {"paint_task_count", state.paint_task_count}, {"prepare_execution_time_ns", state.prepare_execution_time_ns}, {"paint_execution_time_ns", state.paint_execution_time_ns}, {"item_count", state.item_count}, {"prepared_item_count", state.prepared_item_count}, {"prepared_revision", state.prepared_revision}}; } nlohmann::json prop_document() const { if (const auto* value = std::get_if(&plot)) return value->prop(); return nlohmann::json::object(); } nlohmann::json patch_prop(const nlohmann::json& patch) { if (auto* value = std::get_if(&plot)) return value->patch_prop(patch); return {{"success", true}, {"prop", nlohmann::json::object()}}; } }; Graph_Session::Graph_Session(std::unique_ptr private_data) : d(std::move(private_data)) {} std::shared_ptr Graph_Session::create(asio::any_io_executor executor, const Graph_Descriptor& descriptor) { auto result = std::shared_ptr(new Graph_Session(std::make_unique(std::move(executor), descriptor))); result->start(); return result; } Graph_Session::~Graph_Session() { d->events.close(); } -void Graph_Session::start() { auto self = shared_from_this(); if (auto* plot = std::get_if(&d->plot)) plot->scene->set_frame_callback([weak = weak_from_this()](Image_View image) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(image, sequence)); } }); else std::get(d->plot).scene->set_frame_callback([weak = weak_from_this()](std::shared_ptr frame) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(*frame, sequence)); } }); asio::co_spawn(d->strand, [self]() -> asio::awaitable { for (;;) { asio::error_code error; auto event = co_await self->d->events.async_receive(asio::redirect_error(asio::use_awaitable, error)); if (error) co_return; if (auto* state = std::get_if(&event)) { state->handler(graph_state_json(self->d->state_document())); continue; } if (auto* prop = std::get_if(&event)) { prop->handler(self->d->prop_document()); continue; } if (auto* patch = std::get_if(&event)) { patch->handler(self->d->patch_prop(patch->patch)); continue; } const auto value = std::get(event); if (auto* plot = std::get_if(&self->d->plot)) { const Size viewport{static_cast(std::clamp(value.width, 160U, 1920U)), static_cast(std::clamp(value.height, 120U, 1080U))}; plot->scene->set<&Render_Scene_2D::Prop::viewport>(viewport); if (plot->frequency) plot->frequency->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->horizontal) plot->horizontal->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->vertical) plot->vertical->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->time) plot->time->set<&Abs_Axis::Prop::canvas_size>(viewport); plot->update(value.time_milliseconds); plot->scene->render(); } else { auto& plot_3d = std::get(self->d->plot); plot_3d.scene->set<&Render_Scene_3D::Prop::viewport>(render_3d::Extent{std::clamp(value.width, 160U, 1920U), std::clamp(value.height, 120U, 1080U)}); plot_3d.scene->render(); } } }, [](std::exception_ptr exception) { if (exception) std::rethrow_exception(exception); }); } +void Graph_Session::start() { auto self = shared_from_this(); if (auto* plot = std::get_if(&d->plot)) plot->scene->set_frame_callback([weak = weak_from_this()](Image_View image) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(image, sequence)); } }); else std::get(d->plot).scene->set_frame_callback([weak = weak_from_this()](std::shared_ptr frame) { if (auto owner = weak.lock()) { const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1; owner->d->publish(encode_frame(*frame, sequence)); } }); asio::co_spawn(d->strand, [self]() -> asio::awaitable { for (;;) { asio::error_code error; auto event = co_await self->d->events.async_receive(asio::redirect_error(asio::use_awaitable, error)); if (error) co_return; if (auto* state = std::get_if(&event)) { state->handler(self->d->state_document()); continue; } if (auto* prop = std::get_if(&event)) { prop->handler(self->d->prop_document()); continue; } if (auto* patch = std::get_if(&event)) { patch->handler(self->d->patch_prop(patch->patch)); continue; } const auto value = std::get(event); if (auto* plot = std::get_if(&self->d->plot)) { const Size viewport{static_cast(std::clamp(value.width, 160U, 1920U)), static_cast(std::clamp(value.height, 120U, 1080U))}; plot->scene->set<&Render_Scene_2D::Prop::viewport>(viewport); if (plot->frequency) plot->frequency->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->horizontal) plot->horizontal->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->vertical) plot->vertical->set<&Abs_Axis::Prop::canvas_size>(viewport); if (plot->time) plot->time->set<&Abs_Axis::Prop::canvas_size>(viewport); plot->update(value.time_milliseconds); plot->scene->render(); } else { auto& plot_3d = std::get(self->d->plot); plot_3d.scene->set<&Render_Scene_3D::Prop::viewport>(render_3d::Extent{std::clamp(value.width, 160U, 1920U), std::clamp(value.height, 120U, 1080U)}); plot_3d.scene->render(); } } }, [](std::exception_ptr exception) { if (exception) std::rethrow_exception(exception); }); } void Graph_Session::attach(const void* owner, Frame_Handler handler) { { std::lock_guard lock(d->handlers_mutex); d->handlers.insert_or_assign(owner, std::move(handler)); } submit({}); } void Graph_Session::detach(const void* owner) { std::lock_guard lock(d->handlers_mutex); d->handlers.erase(owner); } void Graph_Session::submit(Graph_Event event) { static_cast(d->events.try_send(asio::error_code{}, Session_Event{event})); } diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp index bc2e597..d593316 100644 --- a/web_server/src/Graph_WebSocket.cpp +++ b/web_server/src/Graph_WebSocket.cpp @@ -1,4 +1,4 @@ -#include "Graph_WebSocket.hpp" +#include "Graph_WebSocket.hpp" /* 图像素流 WebSocket。 */ #include #include #include diff --git a/web_server/src/Web_Server.cpp b/web_server/src/Web_Server.cpp index bd98723..9402a64 100644 --- a/web_server/src/Web_Server.cpp +++ b/web_server/src/Web_Server.cpp @@ -1,5 +1,5 @@ #include "Web_Server.hpp" -#include "Graph_Metadata.hpp" +#include "Graph_Metadata.hpp" /* 按图请求描述数据。 */ #include "Graph_Session.hpp" #include "Graph_WebSocket.hpp" #include @@ -20,7 +20,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) auto websocket = std::make_shared(registry); auto& app = drogon::app(); app.registerHandler("/api/graphs", [](const drogon::HttpRequestPtr&, std::function&& callback) { callback(json_response(graph_catalog_json())); }, {drogon::Get}); - app.registerHandler("/api/graphs/{1}/descriptor", [](const drogon::HttpRequestPtr&, std::function&& callback, std::string graph_id) { if (!find_graph(graph_id)) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } callback(json_response({{"prop", graph_prop_descriptor_json(graph_id)}, {"state", graph_state_descriptor_json()}, {"state_api", "/api/graphs/" + graph_id + "/state"}, {"prop_api", "/api/graphs/" + graph_id + "/prop"}})); }, {drogon::Get}); + app.registerHandler("/api/graphs/{1}/descriptor", [](const drogon::HttpRequestPtr&, std::function&& callback, std::string graph_id) { if (!find_graph(graph_id)) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } callback(json_response({{"prop", graph_prop_descriptor_json(graph_id)}, {"state", graph_state_descriptor_json(graph_id)}, {"state_api", "/api/graphs/" + graph_id + "/state"}, {"prop_api", "/api/graphs/" + graph_id + "/prop"}})); }, {drogon::Get}); app.registerHandler("/api/graphs/{1}/state", [registry](const drogon::HttpRequestPtr&, std::function&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } auto output = std::make_shared>(std::move(callback)); session->async_state([output](nlohmann::json state) { (*output)(json_response(std::move(state))); }); }, {drogon::Get}); app.registerHandler("/api/graphs/{1}/prop", [registry](const drogon::HttpRequestPtr&, std::function&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } auto output = std::make_shared>(std::move(callback)); session->async_prop([output](nlohmann::json prop) { (*output)(json_response(std::move(prop))); }); }, {drogon::Get}); app.registerHandler("/api/graphs/{1}/prop", [registry](const drogon::HttpRequestPtr& request, std::function&& callback, std::string graph_id) { auto session = registry->acquire(graph_id); if (!session) { callback(error_response(drogon::k404NotFound, "unknown graph")); return; } nlohmann::json patch; try { patch = nlohmann::json::parse(request->body()); } catch (const nlohmann::json::exception&) { callback(error_response(drogon::k400BadRequest, "invalid JSON patch")); return; } auto output = std::make_shared>(std::move(callback)); session->async_patch_prop(std::move(patch), [output](nlohmann::json result) { (*output)(json_response(std::move(result))); }); }, {drogon::Patch}); diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 6b36e6b..f7e65cb 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -1,12 +1,14 @@ import {memo, useEffect, useMemo, useRef, useState} from "react"; type Graph = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; state: string; descriptor: string}; -type Field = {name: string; value_type: string; editable: boolean; presentation: {label: string; description?: string; control: string; options?: Array<{value: string; label: string}>}}; -type Descriptor = {state?: {fields: Field[]}; prop?: {fields: Field[]}; state_api: string; prop_api: string}; +type Field = {name: string; value_type: string; editable: boolean; children?: Field[]; presentation: {label: string; description?: string; control: string; options?: Array<{value: string; label: string}>}}; +type Descriptor = {state: {fields: Field[]}; prop: {fields: Field[]}; state_api: string; prop_api: string}; const protocolHeaderSize = 24; function socketUrl(path: string) { return `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${path}`; } function drawPixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer) { if (bytes.byteLength < protocolHeaderSize) return; const view = new DataView(bytes); if (view.getUint32(0, true) !== 0x41544852 || view.getUint16(4, true) !== 1) return; const width = view.getUint32(8, true); const height = view.getUint32(12, true); const pixels = new Uint8ClampedArray(bytes, protocolHeaderSize); if (pixels.byteLength !== width * height * 4) return; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } canvas.getContext("2d", {alpha: false})?.putImageData(new ImageData(pixels, width, height), 0, 0); } function useGraphStream(graph: Graph, canvasRef: React.RefObject) { const statusRef = useRef(null); useEffect(() => { let stopped = false; let animation = 0; const socket = new WebSocket(socketUrl(graph.websocket)); socket.binaryType = "arraybuffer"; socket.onopen = () => { if (statusRef.current) statusRef.current.textContent = "LIVE"; }; socket.onclose = () => { if (statusRef.current) statusRef.current.textContent = "OFFLINE"; }; socket.onmessage = event => { if (event.data instanceof ArrayBuffer && canvasRef.current) drawPixels(canvasRef.current, event.data); }; const tick = (time: number) => { const canvas = canvasRef.current; if (!stopped && socket.readyState === WebSocket.OPEN && canvas) socket.send(JSON.stringify({time, width: Math.max(320, canvas.clientWidth * devicePixelRatio), height: Math.max(220, canvas.clientHeight * devicePixelRatio)})); if (!stopped) animation = requestAnimationFrame(tick); }; animation = requestAnimationFrame(tick); return () => { stopped = true; cancelAnimationFrame(animation); socket.close(); }; }, [graph.websocket, canvasRef]); return statusRef; } -function FieldControl({field, value, onChange}: {field: Field; value: unknown; onChange: (value: unknown) => void}) { const control = field.presentation.control === "automatic" ? field.value_type === "boolean" ? "boolean" : field.value_type === "number" || field.value_type === "integer" ? "number" : field.presentation.options ? "select" : "text" : field.presentation.control; if (control === "boolean") return ; if (control === "select" && field.presentation.options) return ; return ; } -function Inspector({graph, descriptor}: {graph: Graph; descriptor: Descriptor}) { const [state, setState] = useState>({}); const [prop, setProp] = useState>({}); useEffect(() => { const requests = [fetch(descriptor.state_api).then(response => response.json()).then(setState)]; if (descriptor.prop_api) requests.push(fetch(descriptor.prop_api).then(response => response.ok ? response.json() : {}).then(setProp)); void Promise.all(requests); }, [descriptor.prop_api, descriptor.state_api]); const update = async (name: string, value: unknown) => { const next = {...prop, [name]: value}; setProp(next); await fetch(descriptor.prop_api, {method: "PATCH", headers: {"Content-Type": "application/json"}, body: JSON.stringify({[name]: value})}); }; return

Editable Prop

{descriptor.prop?.fields.filter(field => field.editable).map(field => void update(field.name, value)}/>) ??

No editable Prop fields published.

}

Published State

{descriptor.state?.fields.map(field =>
{field.presentation.label}
{String(state[field.name] ?? "—")}
)}
; } -const GraphCard = memo(function GraphCard({graph}: {graph: Graph}) { const canvasRef = useRef(null); const statusRef = useGraphStream(graph, canvasRef); const [descriptor, setDescriptor] = useState(null); const [open, setOpen] = useState(false); const inspect = async () => { if (!descriptor) setDescriptor(await fetch(graph.descriptor).then(response => response.json())); setOpen(value => !value); }; return
{graph.category} · {graph.dimension}

{graph.title}

CONNECTING

{graph.description}

{open && descriptor ? : null}
; }); -export function App() { const [graphs, setGraphs] = useState([]); const [category, setCategory] = useState("All"); useEffect(() => { void fetch("/api/graphs").then(response => response.json()).then(setGraphs); }, []); const categories = useMemo(() => ["All", ...new Set(graphs.map(graph => graph.category))], [graphs]); const visible = category === "All" ? graphs : graphs.filter(graph => graph.category === category); return
AETHERA RENDER LAB

Live chart gallery

Browser time drives each isolated Scene. Completed pixels return directly from the Scene callback; properties and state are fetched only when inspected.

{graphs.length}live components
{visible.map(graph => )}
; } +function JsonControl({field, value, onChange}: {field: Field; value: unknown; onChange: (value: unknown) => void}) { const [draft, setDraft] = useState(() => JSON.stringify(value, null, 2)); const [invalid, setInvalid] = useState(false); useEffect(() => setDraft(JSON.stringify(value, null, 2)), [value]); const apply = () => { try { onChange(JSON.parse(draft)); setInvalid(false); } catch { setInvalid(true); } }; return