2D 绘图缓存节点

This commit is contained in:
2026-08-21 10:16:16 +08:00
parent 26a89335b4
commit 855e25981d
55 changed files with 772 additions and 488 deletions
+2
View File
@@ -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。
@@ -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 <detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::Dependency_Object Source>
Node* add_dependency(Target* target, Source* source) {
return edit_dependency_graph->template add_dependency_runtime<Bound_Object, Target, Source>(target, source, nullptr, nullptr, nullptr, bind_node_data);
}
/* 添加 Prop 字段级依赖;source 通过 set 修改该字段时目标阶段变脏。 */
template <auto Member, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::Prop_Dependency_Source<Member> 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(
-2
View File
@@ -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 任务类型的累计统计。 */
+1 -1
View File
@@ -71,7 +71,7 @@ struct Renderable : Def<Renderable, Root> {
/* 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 子图。 */
+1 -1
View File
@@ -1,4 +1,4 @@
#include "scene.hpp"
#include "scene.hpp" /* 后端共有 Prepare Scene 实现。 */
namespace aethera {
Scene::Private::Private() : runtime(std::make_unique<Runtime>()) {}
Scene::Private::~Private() {
+9 -9
View File
@@ -3,21 +3,21 @@
namespace aethera {
/* Scene 状态标签,用于访问和订阅 Scene::State。 */
/*
* Scene 汇总 Prepare/Paint 两张 Dependency_Graph 图并构建 Taskflow。
* Scene 汇总跨渲染后端共有的 Prepare 数据依赖图并构建 Taskflow。
* 用户最终通过 Impl<Scene> 创建可使用实例;编辑 Dependency_Graph 后调用 advance() 提交结构变化,再调用 process(...) 执行当前场景。
*/
struct Scene : Def<Scene, Root, Dependency_Graph_Type<Prepare_Data_Tag, Renderable>, Dependency_Graph_Type<Paint_Tag, Renderable>> {
struct Scene : Def<Scene, Root, Dependency_Graph_Type<Prepare_Data_Tag, Renderable>> {
/* 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;
};
+16 -78
View File
@@ -5,9 +5,9 @@
#include <vector>
namespace aethera {
struct Scene::Private : Prev_Private {
struct Result {}; /* process(...) 完成回调的结果类型;当前仅表示完成。 */
struct Runtime; /* Scene 的 Taskflow 构建产物;完整定义位于本文件下方。 */
std::unique_ptr<Runtime> runtime; /* Scene 唯一运行时构建产物的所有权。 */
struct Result {}; /* process(...) 完成回调的结果类型;当前仅表示完成。 */
struct Runtime; /* Scene 的 Taskflow 构建产物;完整定义位于本文件下方。 */
std::unique_ptr<Runtime> 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<Tag>() 访问状态层。 */
};
struct Scene::Private::Runtime {
std::unique_ptr<tf::Taskflow> taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */
std::unique_ptr<tf::Taskflow> taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */
};
template <Attached Object, typename Callback>
void Scene::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback, const Result&> {
@@ -56,41 +56,29 @@ void Scene::Private::after_advance(Object* object,
}
);
bool taskflow_dirty = !runtime->taskflow;
object->template access_pending_dependency_graph<Prepare_Data_Tag, Paint_Tag>(
[&](auto& prepare_state, auto& paint_state) {
taskflow_dirty = taskflow_dirty || prepare_state.dirty() || paint_state.dirty();
}
);
object->template access_pending_dependency_graph<Prepare_Data_Tag>(
[&](auto& prepare_state) { taskflow_dirty = taskflow_dirty || prepare_state.dirty(); });
if (!taskflow_dirty) return;
if (!runtime->taskflow) runtime->taskflow = std::make_unique<tf::Taskflow>();
auto& taskflow = *runtime->taskflow;
auto prepare_dependencies = object->template current_dependency_graph<Prepare_Data_Tag>();
auto paint_dependencies = object->template current_dependency_graph<Paint_Tag>();
std::pmr::unordered_set<Renderable*> 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<Root*, Stage_Tasks> 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<Prepare_Data_Tag>();
root->template mark_dirty<Paint_Tag>();
if (state.prepare_executed) root->template take_dirty<Prepare_Data_Tag>();
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<Paint_Tag>();
}
state.paint_task_count = data->paint_graph->num_tasks();
}
else {
state.paint_task_count = 1;
}
bool dirty = root->template dirty<Paint_Tag>();
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<tf::Taskflow>();
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<Paint_Tag>();
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<Prepare_Data_Tag, Paint_Tag>(
[](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<Prepare_Data_Tag>(
[](auto& prepare_state) { if (prepare_state.dirty()) prepare_state.take_dirty(); });
scene_state.taskflow_rebuilt = true;
}
}
+16
View File
@@ -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<Graph_Tag>());
}
TEST(dependency_graph, whole_object_dependency_only_controls_topology) {
auto source = build_object<Node>();
auto target = build_object<Node>();
auto graph = build_object<Graph>();
ASSERT_TRUE(graph->edit_dependency_graph<Graph_Tag>([&](auto& editor) { editor.add_dependency(target.get(), source.get()); }).has_value());
graph->advance();
std::vector<double_buffer::Root*> order;
ASSERT_TRUE(graph->current_dependency_graph<Graph_Tag>().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<Graph_Tag>();
EXPECT_FALSE(target->dirty<Graph_Tag>());
}
TEST(dependency_graph, state_dependency_granularity_is_selectable) {
auto member_source = build_object<Node>();
auto member_target = build_object<Node>();
+6 -13
View File
@@ -85,12 +85,7 @@ std::unique_ptr<Object_Type> build_object() {
}
template <typename Renderable>
void add_renderable(Scene& scene, Renderable* renderable) {
ASSERT_TRUE((scene.edit_dependency_graph<aethera::Prepare_Data_Tag, aethera::Paint_Tag>(
[&](auto& prepare, auto& paint) {
prepare.add(renderable);
paint.add(renderable);
}
).has_value()));
ASSERT_TRUE(scene.edit_dependency_graph<aethera::Prepare_Data_Tag>([&](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<aethera::Renderable::Private&>(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<aethera::Renderable::Base_Tag>([&](const auto& state) {
++renderable_updates;
EXPECT_TRUE(state.prepare_executed);
EXPECT_TRUE(state.paint_executed);
EXPECT_FALSE(state.paint_executed);
});
scene->set_state_callback<aethera::Scene::Base_Tag>([&](const auto& state) {
++scene_updates;
@@ -190,13 +185,11 @@ TEST(scene_condition, upstream_change_makes_downstream_run_in_same_taskflow) {
auto source = build_object<Dependency>();
auto target = build_object<Dependency>();
auto scene = build_object<Scene>();
ASSERT_TRUE((scene->edit_dependency_graph<aethera::Prepare_Data_Tag, aethera::Paint_Tag>(
[&](auto& prepare, auto& paint) {
ASSERT_TRUE((scene->edit_dependency_graph<aethera::Prepare_Data_Tag>(
[&](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&) {});
+1 -1
View File
@@ -1,4 +1,4 @@
#include "Abs_Axis.hpp"
#include "Abs_Axis.hpp" /* 轴属性缓存失效实现。 */
#include <algorithm>
#include <cmath>
#include <iomanip>
+2 -3
View File
@@ -1,6 +1,6 @@
#pragma once
#include "Axis_Types.hpp"
#include "../render/Blend2D_Cache.hpp"
#include "../base/Renderable_2D.hpp"
#include <renderable.hpp>
#include <string>
namespace aethera::render_2d {
@@ -19,8 +19,7 @@ concept Axis_Object = Renderable_Object<T> && std::derived_from<T, Abs_Axis> &&
{ private_data.sub_tick_count(object, tick) } -> std::same_as<Axis_Tick_Count>;
};
/* 所有二维坐标轴共享的定义层;最终通过 Impl<Derived_Axis> 创建运行时对象。 */
struct Abs_Axis : Def<Abs_Axis, Renderable,
Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Abs_Axis : Def<Abs_Axis, Renderable_2D<>> {
struct Prop : Prev_Prop {
Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */
Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */
+49 -33
View File
@@ -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<Prepared_Line> lines{}; /* 本轮 Prepare 生成的轴线与刻度线。 */
std::vector<Prepared_Label> labels{}; /* 本轮 Prepare 生成的刻度标签。 */
Point_F unit_position{}; /* 单位文本左上角位置,单位为画布像素。 */
double unit_width{}; /* 单位文本背景的估算宽度,单位为像素。 */
bool valid{}; /* 本轮 Prepare 是否生成了可绘制内容。 */
std::vector<Prepared_Line> lines{}; /* 本轮 Prepare 生成的轴线与刻度线。 */
std::vector<Prepared_Label> 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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> 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<decltype(object)>;
auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*private_data.current);
auto& cache = object->template pending_buffer<Color_Cache>();
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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void Abs_Axis::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
object->template mark_dirty<Prepare_Data_Tag>();
}
inline int Abs_Axis::Private::sub_tick_count(const Attached auto*, double) const {
return 4;
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -1,4 +1,4 @@
#include "Time_Axis.hpp"
#include "Time_Axis.hpp" /* 继承轴属性缓存失效实现。 */
#include <iomanip>
#include <sstream>
namespace aethera::render_2d {
@@ -0,0 +1,37 @@
#pragma once
#include "../render/Blend2D_Cache.hpp"
#include <renderable.hpp>
namespace aethera::render_2d {
/* 二维 Renderable 是否拥有可跨帧复用的完整颜色缓存。 */
enum class Renderable_2D_Cache {
disabled,
enabled
};
/*
* 二维绘制能力层。业务类型在该模板位置选择是否拥有颜色缓存;默认不分配完整缓存。
* Paint 实现只向 Scene 本轮指定的绘制目标输出,不直接选择或清理双缓冲角色。
*/
template <Renderable_2D_Cache Cache = Renderable_2D_Cache::disabled>
struct Renderable_2D;
template <>
struct Renderable_2D<Renderable_2D_Cache::disabled> : Def<Renderable_2D<Renderable_2D_Cache::disabled>, Renderable> {
struct Prop : Prev_Prop {};
struct State : Prev_State {
bool operator==(const State&) const;
};
struct Private;
};
template <>
struct Renderable_2D<Renderable_2D_Cache::enabled>
: Def<Renderable_2D<Renderable_2D_Cache::enabled>,
Renderable_2D<Renderable_2D_Cache::disabled>,
Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Prop : Prev_Prop {};
struct State : Prev_State {
bool operator==(const State&) const;
};
struct Private;
};
using Renderable_2D_Base = Renderable_2D<Renderable_2D_Cache::disabled>;
}
#include "Renderable_2D.ipp"
@@ -0,0 +1,35 @@
#pragma once
#include <stdexcept>
namespace aethera::render_2d {
struct Renderable_2D<Renderable_2D_Cache::disabled>::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 <Attached Object> void bind_private_crtp(Object* object);
};
struct Renderable_2D<Renderable_2D_Cache::enabled>::Private : Prev_Private {
/* CRTP 覆盖:继续绑定二维绘制能力;缓存机制由本定义层静态加入。 */
template <Attached Object> void bind_private_crtp(Object* object);
};
inline bool Renderable_2D<Renderable_2D_Cache::disabled>::State::operator==(const State&) const = default;
inline bool Renderable_2D<Renderable_2D_Cache::enabled>::State::operator==(const State&) const = default;
inline Blend2D_Cache& Renderable_2D<Renderable_2D_Cache::disabled>::Private::paint_surface() {
if (!paint_target) throw std::logic_error("2D renderable painted without a scene paint target");
return *paint_target;
}
template <Attached Object>
void Renderable_2D<Renderable_2D_Cache::disabled>::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
}
template <Attached Object>
void Renderable_2D<Renderable_2D_Cache::enabled>::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
this->pending_cache = [](Root* root) {
return &static_cast<Object*>(root)->template pending_buffer<Color_Cache>();
};
}
}
+37 -33
View File
@@ -5,22 +5,26 @@
#include <cstddef>
#include <cstdint>
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<std::uint32_t>(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;
}
+2 -3
View File
@@ -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<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector<Plot_Value>&& values) { append_spectrum(std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast<const Private&>(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast<const Private&>(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast<const Private&>(*d).dispatch->rendered_count(this); } }
+12 -21
View File
@@ -9,28 +9,19 @@
#include <span>
#include <vector>
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<std::vector<Plot_Value>> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */
bool operator==(const Afterglow_Prop_Data&) const;
};
struct Afterglow : Def<Afterglow, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Afterglow : Def<Afterglow, Renderable_2D<Renderable_2D_Cache::enabled>> {
using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Power_Object = Impl<Numeric_Axis>;
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<std::vector<Plot_Value>> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
+2 -2
View File
@@ -42,7 +42,7 @@ struct Afterglow::Private : Prev_Private {
};
template <typename Object> Afterglow::Builder<Object>::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 <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Afterglow::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, power_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(plot.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(plot.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), power_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(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<std::unique_ptr<Object>, Dependency_Graph_Error> Afterglow::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, power_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <typename Values> void Afterglow::append_spectrum(const Values& values) { append_spectrum(std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object> 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<std::size_t>(1, columns)); }
template <Attached Object>
@@ -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<Plot_Ratio>(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end())); }
template <Attached Object>
void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Afterglow::Base_Tag>(); 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<std::size_t>(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<std::size_t>(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast<int>(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } }
template <Attached Object> void Afterglow::Private::paint_frame(Object* object) { auto& cache = object->template pending_buffer<Color_Cache>(); 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 <Attached Object> 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 <typename Object, typename Owner, typename Member, typename Prop_Type> void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Afterglow::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Afterglow::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*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 <Attached Object>
@@ -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<Private&>(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast<const Private&>(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast<Private&>(*d).dispatch->fit(this); } }
@@ -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<Constellation_Point> points{}; /* 已提交且尚未过期的点。 */
bool operator==(const Constellation_Diagram_Prop_Data&) const;
};
struct Constellation_Diagram : Def<Constellation_Diagram, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Constellation_Diagram : Def<Constellation_Diagram, Renderable_2D<Renderable_2D_Cache::enabled>> {
using Scene_Object = Impl<Render_Scene_2D>; using Axis_Object = Impl<Numeric_Axis>;
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<Constellation_Point> points{}; /* 已提交且尚未过期的点。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
@@ -35,10 +35,10 @@ struct Constellation_Diagram::Private : Prev_Private {
};
template <typename Object> Constellation_Diagram::Builder<Object>::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 <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Constellation_Diagram::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, i_axis, q_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(i_axis); prepare.add(q_axis); prepare.add(plot.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(plot.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), i_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), i_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), q_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(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<std::unique_ptr<Object>, Dependency_Graph_Error> Constellation_Diagram::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, i_axis, q_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <Attached Object>
void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); const auto& i_layout = i_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& q_layout = q_axis->template read_prop<Abs_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<int>(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<Paint_Tag>(); }
template <Attached Object> void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); 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 <Attached Object> void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); 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 <typename Object, typename Owner, typename Member, typename Prop_Type> void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Constellation_Diagram::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { pending_states.template get<Constellation_Diagram::Base_Tag>().point_count = static_cast<const Prop&>(*current_prop).points.size(); }
template <Attached Object>
@@ -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;
@@ -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<Frequency_Trace_Sample> samples{}; /* 已提交轨迹样本的唯一权威集合。 */
bool operator==(const Frequency_Trace_Prop_Data&) const;
};
struct Frequency_Trace : Def<Frequency_Trace, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Frequency_Trace : Def<Frequency_Trace, Renderable_2D<Renderable_2D_Cache::enabled>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Time_Object = Impl<Time_Axis>;
using Value_Object = Impl<Numeric_Axis>;
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<Frequency_Trace_Sample> samples{}; /* 已提交轨迹样本的唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
@@ -47,12 +47,13 @@ template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Frequency_Trace::Builder<Object>::build() {
auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto trace = std::move(result).value();
static_cast<typename Object::Private&>(*trace->d).bind_sources(scene, time_axis, value_axis);
auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
prepare.add(time_axis); prepare.add(value_axis); prepare.add(trace.get());
prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(trace.get(), scene);
prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(trace.get(), time_axis); prepare.template add_prop_dependency<Time_Axis::Base_Tag>(trace.get(), time_axis);
prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(trace.get(), value_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(trace.get(), value_axis);
paint.add(time_axis); paint.add(value_axis); paint.add(trace.get());
auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <Attached Object>
void Frequency_Trace::Private::paint_frame(Object* object) {
const auto& state = object->template read_prop<Frequency_Trace::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); 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<Frequency_Trace::Base_Tag>(); 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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void Frequency_Trace::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
@@ -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<Rect_F> Selection_Rectangle_Overlay::selected_regions() const { return static_cast<const Private&>(*d).dispatch->selected_regions(this); }
@@ -7,24 +7,19 @@
#include <memory>
#include <vector>
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<Rect_F> selected_regions{}; /* 已完成选择的轴坐标矩形。 */
bool operator==(const Selection_Rectangle_Overlay_Prop_Data&) const;
};
struct Selection_Rectangle_Overlay : Def<Selection_Rectangle_Overlay, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Selection_Rectangle_Overlay : Def<Selection_Rectangle_Overlay, Renderable_2D<>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Axis_Object = Impl<Numeric_Axis>;
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<Rect_F> 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<Selection_Rectangle_Overlay, Renderable
Builder(Scene_Object* scene, Axis_Object* horizontal_axis, Axis_Object* vertical_axis);
[[nodiscard]] std::expected<std::unique_ptr<Object>, 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<Rect_F> selected_regions() const;
void clear_selected_regions();
@@ -37,13 +37,21 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Selection_Rectang
if (!result) return std::unexpected(result.error());
auto overlay = std::move(result).value();
static_cast<typename Object::Private&>(*overlay->d).bind_sources(scene, horizontal_axis, vertical_axis);
auto graph_result = scene->template edit_dependency_graph<Paint_Tag>([&](auto& paint) {
paint.add(overlay.get());
paint.template add_prop_dependency<Render_Scene_2D::Base_Tag>(overlay.get(), scene);
paint.template add_prop_dependency<Abs_Axis::Base_Tag>(overlay.get(), horizontal_axis);
paint.template add_prop_dependency<Numeric_Axis::Base_Tag>(overlay.get(), horizontal_axis);
paint.template add_prop_dependency<Abs_Axis::Base_Tag>(overlay.get(), vertical_axis);
paint.template add_prop_dependency<Numeric_Axis::Base_Tag>(overlay.get(), vertical_axis);
auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*data.current);
const Size canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
auto& cache = object->template pending_buffer<Color_Cache>();
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) {
+1 -3
View File
@@ -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;
+26 -35
View File
@@ -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<Spectrum_Frequency> custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */
Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */
bool operator==(const Spectrum_Prop_Data&) const;
};
struct Spectrum : Def<Spectrum, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>, Tagged_Buffer<Spectrum_Frame_Tag, Spectrum_Frame>> {
struct Spectrum : Def<Spectrum, Renderable_2D<Renderable_2D_Cache::enabled>, Tagged_Buffer<Spectrum_Frame_Tag, Spectrum_Frame>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>;
using Power_Object = Impl<Numeric_Axis>;
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<Spectrum_Frequency> custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */
Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
+16 -15
View File
@@ -102,18 +102,21 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Spectrum::Builder
if (!result) return std::unexpected(result.error());
auto spectrum = std::move(result).value();
static_cast<typename Object::Private&>(*spectrum->d).bind_render_sources(scene, frequency_axis, power_axis);
auto dependency_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
prepare.add(frequency_axis);
prepare.add(power_axis);
prepare.add(spectrum.get());
prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(spectrum.get(), scene);
prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(spectrum.get(), frequency_axis);
prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(spectrum.get(), frequency_axis);
prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(spectrum.get(), power_axis);
prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(spectrum.get(), power_axis);
paint.add(frequency_axis);
paint.add(power_axis);
paint.add(spectrum.get());
auto dependency_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <Attached Object>
void Spectrum::Private::paint_frame(Object* object) {
auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*private_data.current);
auto& cache = object->template pending_buffer<Color_Cache>();
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);
@@ -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<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); }
@@ -9,30 +9,21 @@
#include <span>
#include <vector>
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<std::vector<Plot_Value>> blocks{}; /* 已提交扫描块的唯一权威集合。 */
bool operator==(const Sweep_Spectrum_Prop_Data&) const;
};
struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable_2D<Renderable_2D_Cache::enabled>> {
using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>;
using Power_Object = Impl<Numeric_Axis>;
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<std::vector<Plot_Value>> blocks{}; /* 已提交扫描块的唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
@@ -45,7 +45,7 @@ Sweep_Spectrum::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Ob
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Sweep_Spectrum::Builder<Object>::build() {
auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto sweep = std::move(result).value(); static_cast<typename Object::Private&>(*sweep->d).bind_sources(scene, frequency_axis, power_axis);
auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(sweep.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(sweep.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(sweep.get(), frequency_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(sweep.get(), frequency_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(sweep.get(), power_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(sweep.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(sweep.get()); });
auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <typename Values> void Sweep_Spectrum::append_block(const Values& values) { append_block(std::span<const Plot_Value>(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<const Plot_Value>(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 <Attached Object>
void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); 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<Sweep_Spectrum::Base_Tag>(); 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 <typename Object, typename Owner, typename Member, typename Prop_Type>
void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type>
+2 -3
View File
@@ -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<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector<Plot_Value>&& values) { append_row(tick, std::span<const Plot_Value>(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector<Plot_Value>&& values) { append_row(time, std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast<const Private&>(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast<const Private&>(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast<const Private&>(*d).dispatch->rendered_count(this); } }
+15 -20
View File
@@ -11,27 +11,22 @@
#include <vector>
namespace aethera::render_2d {
struct Waterfall_Row { Plot_Time_Tick tick{}; std::vector<Plot_Value> 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<Waterfall_Row> rows{}; /* 从旧到新的瀑布行唯一权威集合。 */
bool operator==(const Waterfall_Prop_Data&) const;
};
struct Waterfall : Def<Waterfall, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Waterfall : Def<Waterfall, Renderable_2D<Renderable_2D_Cache::enabled>> {
using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Time_Object = Impl<Time_Axis>;
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<Waterfall_Row> rows{}; /* 从旧到新的瀑布行唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
+2 -2
View File
@@ -50,7 +50,7 @@ struct Waterfall::Private : Prev_Private {
};
template <typename Object> Waterfall::Builder<Object>::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 <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Waterfall::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, time_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(time_axis); prepare.add(plot.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(plot.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), time_axis); prepare.template add_prop_dependency<Time_Axis::Base_Tag>(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<std::unique_ptr<Object>, Dependency_Graph_Error> Waterfall::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, time_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag, Paint_Cache_Tag>([&](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 <typename Values> void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { append_row(tick, std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <typename Values> void Waterfall::append_row(Time_Of_Day time, const Values& values) { append_row(time, std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object> 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 <Attached Object>
void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop<Waterfall::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().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<int>(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<int>(state.rows.size()); const Axis_Range time_range = rows == 1 ? time_axis->coordinate_range() : Axis_Range{static_cast<Axis_Coordinate>(state.rows.front().tick), static_cast<Axis_Coordinate>(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<std::size_t>(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 <Attached Object>
void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Waterfall::Base_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast<std::size_t>(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<std::size_t>(columns); const int column = static_cast<int>(cell % static_cast<std::size_t>(columns)); const auto& values = state.rows[row_index].values; const std::size_t source = static_cast<std::size_t>(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, static_cast<int>(row_index))] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } }
template <Attached Object> void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Waterfall::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); 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 <Attached Object> void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Waterfall::Base_Tag>(); 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 <Attached Object> void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty<Paint_Tag>(); }
template <typename Object, typename Owner, typename Member, typename Prop_Type> void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Waterfall::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*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; }
@@ -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;
@@ -1,5 +1,5 @@
#pragma once
#include "../base/Types.hpp"
#include "../base/Renderable_2D.hpp"
#include "../render/Blend2D_Cache.hpp"
#include <scene.hpp>
#include <functional>
@@ -7,11 +7,14 @@ namespace aethera::render_2d {
struct Scene_Color_Cache_Tag {};
/* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */
struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
Tagged_Buffer<Scene_Color_Cache_Tag, Blend2D_Cache>> {
Tagged_Buffer<Scene_Color_Cache_Tag, Blend2D_Cache>,
Dependency_Graph_Type<Paint_Tag, Renderable_2D_Base>,
Dependency_Graph_Type<Paint_Cache_Tag, Renderable_2D_Base>
> {
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 {
+178 -12
View File
@@ -1,5 +1,7 @@
#pragma once
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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<Root*> 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<tf::Taskflow> paint_taskflow{}; /* 仅由二维 Paint 图构建的执行图。 */
bool rendering{}; /* 是否正在同步合成当前帧。 */
bool render_pending{}; /* 合成期间是否又收到 render();多个调用合并。 */
/* Impl CRTP 实现:在对象锁内执行 Kernel Scene,再按 Paint 图拓扑顺序合成颜色层。 */
std::vector<Paint_Node> paint_order{}; /* Paint 图当前拓扑序及 Scene 缓存分组结果。 */
std::vector<Cache_Group> cache_groups{}; /* 仅保存显式缓存根对应的执行分组。 */
template <Attached Object, typename Callback>
void process(Object* object, Callback&& callback) requires std::invocable<Callback>;
/* Def CRTP hook:二维 Paint 图结构变化后重建其独立 Taskflow。 */
template <Attached Object>
void after_advance(Object* object, Prop* pending_prop, State_Access<State> pending_states, const Prop* current_prop, State_Access<State> current_states);
/* 每帧在 Prepare 完成后计算组级 dirty,并为所有二维节点指定唯一绘制目标。 */
template <Attached Object> void prepare_paint_targets(Object* object, Blend2D_Cache& frame, Size viewport);
template <Attached Object> void render(Object* object);
/* CRTP 业务实现:按 Paint 图拓扑逆序派发事件。 */
template <Attached Object>
@@ -32,6 +54,147 @@ struct Render_Scene_2D::Private : Prev_Private {
template <Attached Object>
void bind_private_crtp(Object* object);
};
template <Attached Object>
void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access<State>, const Prop*, State_Access<State>) {
bool rebuild = !paint_taskflow;
object->template access_pending_dependency_graph<Paint_Tag>([&](auto& paint_state) { rebuild = rebuild || paint_state.dirty(); });
if (!rebuild) return;
if (!paint_taskflow) paint_taskflow = std::make_unique<tf::Taskflow>();
auto& taskflow = *paint_taskflow;
aethera::detail::clear_stage_observers(&taskflow);
taskflow.clear();
const auto dependencies = object->template current_dependency_graph<Paint_Tag>();
paint_order.clear();
cache_groups.clear();
std::unordered_map<Root*, Root*> cache_owner_by_object;
std::unordered_map<Root*, std::size_t> 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<Root*>(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<Root*> 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<Paint_Tag>();
}
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<Paint_Tag>();
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<tf::Taskflow>();
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<Paint_Tag>();
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<Scene_Color_Cache_Tag>().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<Paint_Tag>([](auto& paint_state) { if (paint_state.dirty()) paint_state.take_dirty(); });
}
template <Attached Object>
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<Paint_Tag>();
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<Paint_Tag>();
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<Paint_Tag>();
}
}
template <Attached Object, typename Callback>
void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
requires std::invocable<Callback> {
@@ -42,9 +205,20 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
if (state.viewport.empty()) {
return;
}
object->template current_dependency_graph<Paint_Tag>().for_each(
[](const Dependency_Graph::Node& node) { node.object->template mark_dirty<Paint_Tag>(); });
object->template current_dependency_graph<Paint_Cache_Tag>().for_each([](const Dependency_Graph::Node& node) {
if (!node.object->template take_dirty<Paint_Cache_Tag>()) return;
node.object->template mark_dirty<Prepare_Data_Tag>();
node.object->template mark_dirty<Paint_Tag>();
});
std::vector<Renderable*> prepare_executions;
object->template current_dependency_graph<Prepare_Data_Tag>().for_each_bound([&](Renderable* renderable, Renderable::Private& data) {
bool execute = renderable->template dirty<Prepare_Data_Tag>();
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<Paint_Tag>();
auto& frame = object->template pending_buffer<Scene_Color_Cache_Tag>();
frame.ensure_size(state.viewport);
frame.clear();
@@ -54,16 +228,8 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
static_cast<double>(state.viewport.height)},
Pen{.style = Line_Style::none}, Brush{state.background, Brush_Style::solid});
}
const auto graph = object->template current_dependency_graph<Paint_Tag>();
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<Blend2D_Cache*>(context);
if (const auto* layer = dynamic_cast<const Blend2D_Cache*>(&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>(callback));
}
+7 -5
View File
@@ -1,6 +1,6 @@
#include <render_2D/axis/Axis.hpp>
#include <render_2D/event/Event.hpp>
#include <scene.hpp>
#include <render_2D/scene/Render_Scene_2D.hpp>
#include <gtest/gtest.h>
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<Numeric_Axis>;
using Scene_Object = Impl<Scene>;
using Scene_Object = Impl<Render_Scene_2D>;
initialize_runtime(2);
auto axis = build_axis<Object>();
auto scene = build_axis<Scene_Object>();
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<Prepare_Data_Tag, Paint_Tag>([&](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<Color_Cache>().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) {
+30
View File
@@ -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<Paint_Tag>();
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<Paint_Cache_Tag>();
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<Renderable::Base_Tag>();
@@ -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<Paint_Cache_Tag>());
EXPECT_FALSE(spectrum->dirty<Paint_Tag>());
EXPECT_FALSE(frequency->dirty<Paint_Tag>());
EXPECT_FALSE(power->dirty<Paint_Tag>());
scene->render();
EXPECT_TRUE(contains_color(scene->frame_view()));
frequency->set<&Numeric_Axis::Prop::precision>(3);
EXPECT_FALSE(spectrum->dirty<Paint_Cache_Tag>());
scene->render();
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().paint_executed);
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().paint_executed);
EXPECT_TRUE(power->read_state<Renderable::Base_Tag>().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<Paint_Cache_Tag>());
scene->render();
EXPECT_FALSE(spectrum->dirty<Paint_Cache_Tag>());
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().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<Prepare_Data_Tag>());
EXPECT_TRUE(power->dirty<Prepare_Data_Tag>());
scene->render();
EXPECT_EQ(scene->read_prop<Render_Scene_2D::Base_Tag>().viewport, resized_canvas);
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().prepare_executed);
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().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);
@@ -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;
@@ -6,8 +6,10 @@
#include <functional>
#include <memory>
namespace aethera::render_3d {
/* 执行 3D Renderable 图,并把 Paint 阶段无等待地提交给 Datoviz 渲染域。 */
struct Render_Scene_3D : Def<Render_Scene_3D, Scene> {
/* 三维 Prepare 完成后的异步后端提交阶段标签。 */
struct Submit_Tag {};
/* 执行共有 Prepare 图,并把三维 Submit 阶段无等待地提交给 Datoviz 渲染域。 */
struct Render_Scene_3D : Def<Render_Scene_3D, Scene, Dependency_Graph_Type<Submit_Tag, Renderable>> {
struct Prop : Prev_Prop {
Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */
Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */
@@ -29,7 +31,7 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene> {
Root* visual{}; /* 不拥有的唯一 Visual;生命周期必须覆盖 Scene。 */
Visual_Family visual_family{Visual_Family::point}; /* Visual 编译期 Spec 对应的后端 family。 */
void (*bind_visual)(Root*, std::shared_ptr<void>){}; /* 把 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 验证。 */
};
@@ -27,19 +27,19 @@ struct Render_Scene_3D::Private : Prev_Private {
template <Attached Object> void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family);
/* CRTP 覆盖:绑定 Scene 机制和 Render_Scene_3D 公开薄壳。 */
template <Attached Object> void bind_private_crtp(Object* object);
/* CRTP 覆盖:执行 Kernel Scene Taskflow;其 Paint 子图只负责异步入队。 */
/* CRTP 覆盖:执行 Kernel Prepare Taskflow,再运行只负责异步入队的三维 Submit 阶段。 */
template <Attached Object, typename Callback> void process(Object* object, Callback&& callback) requires std::invocable<Callback>;
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
};
template <typename Object> template <Attached Visual_Object>
Render_Scene_3D::Builder<Object>::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<void> context) { auto* object = static_cast<Visual_Object*>(root); Base::private_access(object).template get<typename Visual_Object::Attached_Object::Base_Tag>().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast<detail::Scene_Paint_Context<Object>*>(raw_context); const auto& prop = submission.scene->template read_prop<Render_Scene_3D::Base_Tag>(); submission.backend->render(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); }); };
attach_visual = [](Object* scene, Root* root) { auto* object = static_cast<Visual_Object*>(root); return scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(object); paint.add(object); prepare.template add_prop_dependency<Render_Scene_3D::Base_Tag>(object, scene); }); };
attach_visual = [](Object* scene, Root* root) { auto* object = static_cast<Visual_Object*>(root); return scene->template edit_dependency_graph<Prepare_Data_Tag, Submit_Tag>([&](auto& prepare, auto& submit) { prepare.add(object); submit.add(object); }); };
}
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Render_Scene_3D::Builder<Object>::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<Render_Scene_3D::Base_Tag>(); 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 <Attached Object> 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<Render_Scene_3D::Base_Tag>(); backend = std::make_shared<detail::Async_Render_Backend>(gpu_index, validation_enabled, visual_family, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); paint_context = std::make_shared<detail::Scene_Paint_Context<Object>>(detail::Scene_Paint_Context<Object>{backend, object}); }
template <Attached Object, typename Callback> void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback> { const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>(); if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return; Prev_Private::process(object, [](const Scene::Private::Result&) {}); std::invoke(std::forward<Callback>(callback)); }
template <Attached Object, typename Callback> void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback> { const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>(); 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_Data_Tag>(); prepare.for_each_bound([](Renderable* renderable, Renderable::Private& data) { if (data.dispatch->state.get(renderable)->prepare_executed) renderable->template mark_dirty<Submit_Tag>(); }); const auto submit = object->template current_dependency_graph<Submit_Tag>(); 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<Submit_Tag>(); 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<tf::Taskflow>(); 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<Submit_Tag>(); dispatch->state.notify(root); }); if (!result) throw std::logic_error("render scene submit graph became invalid during submission"); }); std::invoke(std::forward<Callback>(callback)); }
template <Attached Object> const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root) { auto* object = static_cast<Object*>(root); object->process([] {}); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, const Event& event) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); if (!data.backend) return Dispatch_Event_Result::backend_unavailable; const auto viewport = object->template read_prop<Render_Scene_3D::Base_Tag>().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<Object*>(root)->template set<&Prop::view_active>(active); }}; return value; }
template <Attached Object> void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
}
+19 -21
View File
@@ -1,17 +1,5 @@
#include "Graph_Metadata.hpp"
#include "Graph_Metadata.hpp" /* PFR 属性与状态描述。 */
#include <algorithm>
namespace {
struct State_Field_Metadata {
template <std::size_t Index, typename Field> 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<aethera::web::Graph_State_Document>::get() { return reflected_object_with<aethera::web::Graph_State_Document>("graph_state", "Published graph state", State_Field_Metadata{}); }
}
namespace aethera::web {
const std::vector<Graph_Descriptor>& graph_catalog() {
static const std::vector<Graph_Descriptor> value{
@@ -28,16 +16,26 @@ const std::vector<Graph_Descriptor>& 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_Document>(); }
nlohmann::json graph_state_json(const Graph_State_Document& state) { return adminive::model_to_json<nlohmann::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<nlohmann::json, Spectrum::State>();
if (graph_id == "frequency_trace") return adminive::to_descriptor_json<nlohmann::json, Frequency_Trace::State>();
if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json<nlohmann::json, Sweep_Spectrum::State>();
if (graph_id == "afterglow") return adminive::to_descriptor_json<nlohmann::json, Afterglow::State>();
if (graph_id == "waterfall") return adminive::to_descriptor_json<nlohmann::json, Waterfall::State>();
if (graph_id == "constellation") return adminive::to_descriptor_json<nlohmann::json, Constellation_Diagram::State>();
if (graph_id == "selection_overlay") return adminive::to_descriptor_json<nlohmann::json, Selection_Rectangle_Overlay::State>();
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<nlohmann::json, Spectrum_Editable_Prop_Data>();
if (graph_id == "frequency_trace") return adminive::to_descriptor_json<nlohmann::json, Frequency_Trace_Editable_Prop_Data>();
if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json<nlohmann::json, Sweep_Spectrum_Editable_Prop_Data>();
if (graph_id == "afterglow") return adminive::to_descriptor_json<nlohmann::json, Afterglow_Editable_Prop_Data>();
if (graph_id == "waterfall") return adminive::to_descriptor_json<nlohmann::json, Waterfall_Editable_Prop_Data>();
if (graph_id == "constellation") return adminive::to_descriptor_json<nlohmann::json, Constellation_Diagram_Editable_Prop_Data>();
if (graph_id == "spectrum") return adminive::to_descriptor_json<nlohmann::json, Spectrum::Prop>();
if (graph_id == "frequency_trace") return adminive::to_descriptor_json<nlohmann::json, Frequency_Trace::Prop>();
if (graph_id == "sweep_spectrum") return adminive::to_descriptor_json<nlohmann::json, Sweep_Spectrum::Prop>();
if (graph_id == "afterglow") return adminive::to_descriptor_json<nlohmann::json, Afterglow::Prop>();
if (graph_id == "waterfall") return adminive::to_descriptor_json<nlohmann::json, Waterfall::Prop>();
if (graph_id == "constellation") return adminive::to_descriptor_json<nlohmann::json, Constellation_Diagram::Prop>();
if (graph_id == "selection_overlay") return adminive::to_descriptor_json<nlohmann::json, Selection_Rectangle_Overlay::Prop>();
return {{"fields", nlohmann::json::array()}};
}
}
+3 -28
View File
@@ -1,20 +1,14 @@
#pragma once
#include <adminive/adapters/boost_pfr.hpp>
#include <adminive/adapters/nlohmann_json.hpp>
#include <adminive/adapters/magic_enum.hpp>
#include <adminive/adapters/nlohmann_json.hpp>
#include <adminive/adminive.hpp>
#include <render_2D/plottable/Plottables.hpp>
#include <cstdint>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
#include <vector>
namespace aethera::web {
namespace detail {
struct Editable_Prop_Field_Metadata {
template <std::size_t Index, typename Field> 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_Descriptor>& 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<aethera::web::Graph_State_Document> : Boost_Pfr_Reflection_Adapter<aethera::web::Graph_State_Document> {};
template <> struct Type_Descriptor<aethera::web::Graph_State_Document> { static auto get(); };
#define AETHERA_DECLARE_PROP_REFLECTION(Type, Id, Label) \
template <> struct Reflection_Adapter<aethera::render_2d::Type> : Boost_Pfr_Reflection_Adapter<aethera::render_2d::Type> {}; \
template <> struct Type_Descriptor<aethera::render_2d::Type> { static auto get() { return reflected_object_with<aethera::render_2d::Type>(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" /* 模板和反射特化实现。 */
+113
View File
@@ -0,0 +1,113 @@
#pragma once
#include <boost/pfr.hpp>
#include <stdexcept>
#include <type_traits>
#include <utility>
namespace aethera::web::detail {
struct Editable_Field_Metadata {
template <std::size_t Index, typename Field> auto operator()(Field field) const;
};
template <typename Object, typename Members>
struct Member_Pfr_Reflection_Adapter {
static constexpr std::size_t field_count = boost::pfr::tuple_size_v<Members>;
template <std::size_t Index> static decltype(auto) get(Object& value) noexcept;
template <std::size_t Index> static decltype(auto) get(const Object& value) noexcept;
template <std::size_t Index> 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 <typename Value, Json_Type Json> struct Value_Adapter<std::vector<Value>, Json> {
using value_type = std::vector<Value>;
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<Type> : Boost_Pfr_Reflection_Adapter<Type> {}; template <> struct Type_Descriptor<Type> { 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<Type> : aethera::web::detail::Member_Pfr_Reflection_Adapter<Type, aethera::web::detail::Members> {}; template <> struct Type_Descriptor<Type> { 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 <std::size_t Index, typename Field> auto Editable_Field_Metadata::operator()(Field field) const { return field.editable(); }
template <typename Object, typename Members> template <std::size_t Index> decltype(auto) Member_Pfr_Reflection_Adapter<Object, Members>::get(Object& value) noexcept { return value.*boost::pfr::get<Index>(Members{}); }
template <typename Object, typename Members> template <std::size_t Index> decltype(auto) Member_Pfr_Reflection_Adapter<Object, Members>::get(const Object& value) noexcept { return value.*boost::pfr::get<Index>(Members{}); }
template <typename Object, typename Members> template <std::size_t Index> constexpr std::string_view Member_Pfr_Reflection_Adapter<Object, Members>::name() noexcept { return boost::pfr::get_name<Index, Members>(); }
}
namespace adminive {
template <typename Value, Json_Type Json> Json Value_Adapter<std::vector<Value>, Json>::encode(const value_type& values) { Json result = json_array<Json>(); for (const auto& value : values) json_append(result, encode_json_value<Json>(value)); return result; }
template <typename Value, Json_Type Json> void Value_Adapter<std::vector<Value>, Json>::decode(value_type& target, const Json& value) { if (!Json_Adapter<Json>::is_array(value)) throw std::invalid_argument("value must be an array"); value_type updated; updated.reserve(Json_Adapter<Json>::size(value)); for (std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) { Value item{}; assign_json_value<Json>(item, Json_Adapter<Json>::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<Type>::get() { return reflected_object_with<Type>(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<Type>::get() { return reflected_object_with<Type>(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
}
+20 -15
View File
@@ -1,4 +1,4 @@
#include "Graph_Session.hpp"
#include "Graph_Session.hpp" /* 图对象异步会话。 */
#include <asio/co_spawn.hpp>
#include <asio/error_code.hpp>
#include <asio/experimental/concurrent_channel.hpp>
@@ -48,14 +48,16 @@ struct Plot_2D {
std::unique_ptr<Time_Axis_Object> time{}; /* 时间轴;不用时为空。 */
std::unique_ptr<Root> plot{}; /* 具体 Plottable 的唯一所有权。 */
std::function<void(double)> update{}; /* 根据浏览器时钟更新权威 Prop。 */
std::function<Graph_State_Document(std::uint64_t)> state{}; /* 从具体 State 即时计算 HTTP 文档。 */
std::function<nlohmann::json()> state{}; /* 直接遍历具体图完整 State 即时 HTTP 文档。 */
std::function<nlohmann::json()> prop{};
std::function<nlohmann::json(const nlohmann::json&)> patch_prop{};
};
template <typename Data, auto... Members, typename Object>
template <typename Definition, auto... Members, typename Object>
void bind_prop_api(Plot_2D& plot, Object* object) {
plot.prop = [object] { const auto& prop = object->template read_prop<typename Data::Prop_Tag>(); return adminive::model_to_json<nlohmann::json>(static_cast<const Data&>(prop), true); };
plot.patch_prop = [object](const nlohmann::json& patch) { Data updated = static_cast<const Data&>(object->template read_prop<typename Data::Prop_Tag>()); const auto result = adminive::apply_frontend_patch<nlohmann::json>(updated, patch); if (!result.success) return result.to_json<nlohmann::json>(); ([&] { if (object->template get<Members>() != updated.*Members) object->template set<Members>(updated.*Members); }(), ...); auto output = result.to_json<nlohmann::json>(); output["prop"] = adminive::model_to_json<nlohmann::json>(updated, true); return output; };
using Prop = typename Definition::Prop;
plot.prop = [object] { return adminive::model_to_json<nlohmann::json>(static_cast<const Prop&>(object->template read_prop<typename Definition::Base_Tag>()), true); };
plot.state = [object] { return adminive::model_to_json<nlohmann::json>(static_cast<const typename Definition::State&>(object->template read_state<typename Definition::Base_Tag>()), true); };
plot.patch_prop = [object](const nlohmann::json& patch) { Prop updated = static_cast<const Prop&>(object->template read_prop<typename Definition::Base_Tag>()); const auto result = adminive::apply_frontend_patch<nlohmann::json>(updated, patch); if (!result.success) return result.to_json<nlohmann::json>(); ([&] { if (object->template get<Members>() != updated.*Members) object->template set<Members>(updated.*Members); }(), ...); auto output = result.to_json<nlohmann::json>(); output["prop"] = adminive::model_to_json<nlohmann::json>(static_cast<const Prop&>(object->template read_prop<typename Definition::Base_Tag>()), 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<Frequency_Axis_Object>(); 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<Numeric_Axis_Object>(); 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<Numeric_Axis_Object>();
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<Impl<Spectrum>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Spectrum_Editable_Prop_Data, &Spectrum_Editable_Prop_Data::center_frequency, &Spectrum_Editable_Prop_Data::partition_count, &Spectrum_Editable_Prop_Data::max_hold_visible, &Spectrum_Editable_Prop_Data::min_hold_visible, &Spectrum_Editable_Prop_Data::max_marker_visible, &Spectrum_Editable_Prop_Data::min_marker_visible, &Spectrum_Editable_Prop_Data::sweep_region_visible, &Spectrum_Editable_Prop_Data::visible_range_only>(result, raw); result.update = [raw](double time) { std::array<double, 256> samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast<double>(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<Spectrum::Base_Tag>(); 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<Impl<Spectrum>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Spectrum, &Spectrum::Prop::center_frequency, &Spectrum::Prop::partition_count, &Spectrum::Prop::max_hold_visible, &Spectrum::Prop::min_hold_visible, &Spectrum::Prop::max_marker_visible, &Spectrum::Prop::min_marker_visible, &Spectrum::Prop::sweep_region_visible, &Spectrum::Prop::visible_range_only, &Spectrum::Prop::frequency_range, &Spectrum::Prop::sweep_frequency_range, &Spectrum::Prop::partition_mode, &Spectrum::Prop::interpolation_mode, &Spectrum::Prop::max_brush, &Spectrum::Prop::current_brush, &Spectrum::Prop::min_brush, &Spectrum::Prop::max_pen, &Spectrum::Prop::current_pen, &Spectrum::Prop::min_pen, &Spectrum::Prop::selected_marker_pen, &Spectrum::Prop::marker_pen, &Spectrum::Prop::middle_frequency_pen, &Spectrum::Prop::sweep_region_brush, &Spectrum::Prop::custom_markers, &Spectrum::Prop::selected_marker>(result, raw); result.update = [raw](double time) { std::array<double, 256> samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast<double>(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<Time_Axis_Object>(); configure_axis(result.time.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); make_vertical({-1.2, 1.2}); auto object = build<Impl<Frequency_Trace>>(result.scene.get(), result.time.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Frequency_Trace_Editable_Prop_Data, &Frequency_Trace_Editable_Prop_Data::partition_count>(result, raw); auto tick = std::make_shared<std::uint64_t>(); 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<Frequency_Trace::Base_Tag>(); return Graph_State_Document{sequence, state.sample_count, state.rendered_point_count}; }; result.plot = std::move(object);
result.time = build<Time_Axis_Object>(); configure_axis(result.time.get(), Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas); make_vertical({-1.2, 1.2}); auto object = build<Impl<Frequency_Trace>>(result.scene.get(), result.time.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Frequency_Trace, &Frequency_Trace::Prop::partition_count, &Frequency_Trace::Prop::pen, &Frequency_Trace::Prop::partition_mode, &Frequency_Trace::Prop::samples>(result, raw); auto tick = std::make_shared<std::uint64_t>(); 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<Impl<Sweep_Spectrum>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Sweep_Spectrum_Editable_Prop_Data, &Sweep_Spectrum_Editable_Prop_Data::bins_per_block, &Sweep_Spectrum_Editable_Prop_Data::block_count, &Sweep_Spectrum_Editable_Prop_Data::partition_count, &Sweep_Spectrum_Editable_Prop_Data::visible_range_only>(result, raw); result.update = [raw](double time) { std::array<double, 64> 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<Sweep_Spectrum::Base_Tag>(); 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<Impl<Sweep_Spectrum>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Sweep_Spectrum, &Sweep_Spectrum::Prop::bins_per_block, &Sweep_Spectrum::Prop::block_count, &Sweep_Spectrum::Prop::partition_count, &Sweep_Spectrum::Prop::visible_range_only, &Sweep_Spectrum::Prop::frequency_range, &Sweep_Spectrum::Prop::partition_mode, &Sweep_Spectrum::Prop::pen, &Sweep_Spectrum::Prop::current_frequency_pen, &Sweep_Spectrum::Prop::interpolation_mode, &Sweep_Spectrum::Prop::blocks>(result, raw); result.update = [raw](double time) { std::array<double, 64> 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<Impl<Afterglow>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Afterglow_Editable_Prop_Data, &Afterglow_Editable_Prop_Data::frequency_point_size, &Afterglow_Editable_Prop_Data::power_point_size, &Afterglow_Editable_Prop_Data::partition_count, &Afterglow_Editable_Prop_Data::interpolate, &Afterglow_Editable_Prop_Data::attenuation_rate>(result, raw); result.update = [raw](double time) { std::array<double, 192> 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<double>(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<Afterglow::Base_Tag>(); 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<Impl<Afterglow>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Afterglow, &Afterglow::Prop::frequency_point_size, &Afterglow::Prop::power_point_size, &Afterglow::Prop::partition_count, &Afterglow::Prop::interpolate, &Afterglow::Prop::attenuation_rate, &Afterglow::Prop::frequency_range, &Afterglow::Prop::power_range, &Afterglow::Prop::partition_mode, &Afterglow::Prop::color_map, &Afterglow::Prop::spectra>(result, raw); result.update = [raw](double time) { std::array<double, 192> 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<double>(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<Time_Axis_Object>(); configure_axis(result.time.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); auto object = build<Impl<Waterfall>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Waterfall_Editable_Prop_Data, &Waterfall_Editable_Prop_Data::frequency_bin_count, &Waterfall_Editable_Prop_Data::partition_count, &Waterfall_Editable_Prop_Data::visible_range_only>(result, raw); auto tick = std::make_shared<std::uint64_t>(); result.update = [raw, tick](double time) { std::array<double, 192> 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<double>(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<Waterfall::Base_Tag>(); return Graph_State_Document{sequence, state.row_count, state.rendered_cell_count}; }; result.plot = std::move(object);
make_frequency(); result.time = build<Time_Axis_Object>(); configure_axis(result.time.get(), Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas); auto object = build<Impl<Waterfall>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Waterfall, &Waterfall::Prop::tooltip_enabled, &Waterfall::Prop::tooltip_font, &Waterfall::Prop::tooltip_text_pen, &Waterfall::Prop::tooltip_background_brush, &Waterfall::Prop::frequency_bin_count, &Waterfall::Prop::partition_count, &Waterfall::Prop::visible_range_only, &Waterfall::Prop::frequency_range, &Waterfall::Prop::power_range, &Waterfall::Prop::partition_mode, &Waterfall::Prop::interpolation_mode, &Waterfall::Prop::color_map, &Waterfall::Prop::rows>(result, raw); auto tick = std::make_shared<std::uint64_t>(); result.update = [raw, tick](double time) { std::array<double, 192> 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<double>(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<Numeric_Axis_Object>(); 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<Impl<Constellation_Diagram>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Constellation_Diagram_Editable_Prop_Data, &Constellation_Diagram_Editable_Prop_Data::point_lifetime_ms, &Constellation_Diagram_Editable_Prop_Data::type, &Constellation_Diagram_Editable_Prop_Data::phase_offset_radians>(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<Constellation_Diagram::Base_Tag>(); return Graph_State_Document{sequence, state.point_count, state.point_count}; }; result.plot = std::move(object);
result.horizontal = build<Numeric_Axis_Object>(); 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<Impl<Constellation_Diagram>>(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<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Constellation_Diagram, &Constellation_Diagram::Prop::point_lifetime_ms, &Constellation_Diagram::Prop::type, &Constellation_Diagram::Prop::phase_offset_radians, &Constellation_Diagram::Prop::i_range, &Constellation_Diagram::Prop::q_range, &Constellation_Diagram::Prop::point_color, &Constellation_Diagram::Prop::anchor_color, &Constellation_Diagram::Prop::points>(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<Numeric_Axis_Object>(); 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<Impl<Selection_Rectangle_Overlay>>(result.scene.get(), result.horizontal.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); 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<Renderable::Base_Tag>(); return Graph_State_Document{sequence, state.paint_task_count, state.paint_executed ? 1U : 0U}; }; result.plot = std::move(object);
result.horizontal = build<Numeric_Axis_Object>(); 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<Impl<Selection_Rectangle_Overlay>>(result.scene.get(), result.horizontal.get(), result.vertical.get()); auto* raw = object.get(); raw->mark_dirty<Prepare_Data_Tag>(); raw->mark_dirty<Paint_Tag>(); bind_prop_api<Selection_Rectangle_Overlay, &Selection_Rectangle_Overlay::Prop::label_font, &Selection_Rectangle_Overlay::Prop::label_pen, &Selection_Rectangle_Overlay::Prop::selection_brush, &Selection_Rectangle_Overlay::Prop::selection_border_pen, &Selection_Rectangle_Overlay::Prop::selected_regions>(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<Frame_Handler> 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_2D>(&plot)) return value->state(sequence); const auto& value = std::get<Plot_3D>(plot); const auto& state = value.visual->read_state<Point_Visual::Base_Tag>(); return {sequence, state.item_count, state.prepared_item_count}; }
nlohmann::json state_document() const { if (const auto* value = std::get_if<Plot_2D>(&plot)) return value->state(); const auto& state = std::get<Plot_3D>(plot).visual->read_state<Point_Visual::Base_Tag>(); 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_2D>(&plot)) return value->prop(); return nlohmann::json::object(); }
nlohmann::json patch_prop(const nlohmann::json& patch) { if (auto* value = std::get_if<Plot_2D>(&plot)) return value->patch_prop(patch); return {{"success", true}, {"prop", nlohmann::json::object()}}; }
};
Graph_Session::Graph_Session(std::unique_ptr<Private> private_data) : d(std::move(private_data)) {}
std::shared_ptr<Graph_Session> Graph_Session::create(asio::any_io_executor executor, const Graph_Descriptor& descriptor) { auto result = std::shared_ptr<Graph_Session>(new Graph_Session(std::make_unique<Private>(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<Plot_2D>(&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<Plot_3D>(d->plot).scene->set_frame_callback([weak = weak_from_this()](std::shared_ptr<const render_3d::Pixel_Frame> 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<void> { 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<State_Query>(&event)) { state->handler(graph_state_json(self->d->state_document())); continue; } if (auto* prop = std::get_if<Prop_Query>(&event)) { prop->handler(self->d->prop_document()); continue; } if (auto* patch = std::get_if<Prop_Patch>(&event)) { patch->handler(self->d->patch_prop(patch->patch)); continue; } const auto value = std::get<Graph_Event>(event); if (auto* plot = std::get_if<Plot_2D>(&self->d->plot)) { const Size viewport{static_cast<int>(std::clamp(value.width, 160U, 1920U)), static_cast<int>(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<Plot_3D>(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<Plot_2D>(&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<Plot_3D>(d->plot).scene->set_frame_callback([weak = weak_from_this()](std::shared_ptr<const render_3d::Pixel_Frame> 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<void> { 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<State_Query>(&event)) { state->handler(self->d->state_document()); continue; } if (auto* prop = std::get_if<Prop_Query>(&event)) { prop->handler(self->d->prop_document()); continue; } if (auto* patch = std::get_if<Prop_Patch>(&event)) { patch->handler(self->d->patch_prop(patch->patch)); continue; } const auto value = std::get<Graph_Event>(event); if (auto* plot = std::get_if<Plot_2D>(&self->d->plot)) { const Size viewport{static_cast<int>(std::clamp(value.width, 160U, 1920U)), static_cast<int>(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<Plot_3D>(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<void>(d->events.try_send(asio::error_code{}, Session_Event{event})); }
+1 -1
View File
@@ -1,4 +1,4 @@
#include "Graph_WebSocket.hpp"
#include "Graph_WebSocket.hpp" /* 图像素流 WebSocket。 */
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
+2 -2
View File
@@ -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 <asio/thread_pool.hpp>
@@ -20,7 +20,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
auto websocket = std::make_shared<Graph_WebSocket_Controller>(registry);
auto& app = drogon::app();
app.registerHandler("/api/graphs", [](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) { callback(json_response(graph_catalog_json())); }, {drogon::Get});
app.registerHandler("/api/graphs/{1}/descriptor", [](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& 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<void(const drogon::HttpResponsePtr&)>&& 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<void(const drogon::HttpResponsePtr&)>&& 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::function<void(const drogon::HttpResponsePtr&)>>(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<void(const drogon::HttpResponsePtr&)>&& 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::function<void(const drogon::HttpResponsePtr&)>>(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<void(const drogon::HttpResponsePtr&)>&& 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::function<void(const drogon::HttpResponsePtr&)>>(std::move(callback)); session->async_patch_prop(std::move(patch), [output](nlohmann::json result) { (*output)(json_response(std::move(result))); }); }, {drogon::Patch});
+8 -6
View File
@@ -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<HTMLCanvasElement | null>) { const statusRef = useRef<HTMLSpanElement>(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 <label className="control"><span>{field.presentation.label}</span><input type="checkbox" checked={Boolean(value)} onChange={event => onChange(event.target.checked)}/></label>; if (control === "select" && field.presentation.options) return <label className="control"><span>{field.presentation.label}</span><select value={String(value ?? "")} onChange={event => onChange(event.target.value)}>{field.presentation.options.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>; return <label className="control"><span>{field.presentation.label}</span><input type={control === "number" ? "number" : "text"} value={String(value ?? "")} onChange={event => onChange(control === "number" ? Number(event.target.value) : event.target.value)}/><small>{field.presentation.description}</small></label>; }
function Inspector({graph, descriptor}: {graph: Graph; descriptor: Descriptor}) { const [state, setState] = useState<Record<string, unknown>>({}); const [prop, setProp] = useState<Record<string, unknown>>({}); 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 <div className="inspector"><section><h4>Editable Prop</h4>{descriptor.prop?.fields.filter(field => field.editable).map(field => <FieldControl key={field.name} field={field} value={prop[field.name]} onChange={value => void update(field.name, value)}/>) ?? <p className="muted">No editable Prop fields published.</p>}</section><section><h4>Published State</h4><dl>{descriptor.state?.fields.map(field => <div key={field.name}><dt>{field.presentation.label}</dt><dd>{String(state[field.name] ?? "")}</dd></div>)}</dl></section></div>; }
const GraphCard = memo(function GraphCard({graph}: {graph: Graph}) { const canvasRef = useRef<HTMLCanvasElement>(null); const statusRef = useGraphStream(graph, canvasRef); const [descriptor, setDescriptor] = useState<Descriptor | null>(null); const [open, setOpen] = useState(false); const inspect = async () => { if (!descriptor) setDescriptor(await fetch(graph.descriptor).then(response => response.json())); setOpen(value => !value); }; return <article className="card"><header><div><span className="eyebrow">{graph.category} · {graph.dimension}</span><h2>{graph.title}</h2></div><span ref={statusRef} className="status">CONNECTING</span></header><p>{graph.description}</p><canvas ref={canvasRef}/><button className="inspect" onClick={() => void inspect()}>{open ? "Close inspector" : "Inspect Prop & State"}</button>{open && descriptor ? <Inspector graph={graph} descriptor={descriptor}/> : null}</article>; });
export function App() { const [graphs, setGraphs] = useState<Graph[]>([]); 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 <main><section className="hero"><div><span className="eyebrow">AETHERA RENDER LAB</span><h1>Live chart gallery</h1><p>Browser time drives each isolated Scene. Completed pixels return directly from the Scene callback; properties and state are fetched only when inspected.</p></div><div className="heroMetric"><strong>{graphs.length}</strong><span>live components</span></div></section><nav>{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => setCategory(value)}>{value}</button>)}</nav><section className="grid">{visible.map(graph => <GraphCard key={graph.id} graph={graph}/>)}</section></main>; }
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 <label className="control controlJson"><span>{field.presentation.label}</span><textarea value={draft} onChange={event => setDraft(event.target.value)} onBlur={apply}/><small className={invalid ? "error" : ""}>{invalid ? "Invalid JSON" : field.presentation.description ?? "Edit as JSON; changes apply when focus leaves the field."}</small></label>; }
function FieldControl({field, value, onChange}: {field: Field; value: unknown; onChange: (value: unknown) => void}) { if (field.value_type === "object" || field.value_type === "array") return <JsonControl field={field} value={value} onChange={onChange}/>; 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 <label className="control controlBoolean"><span>{field.presentation.label}</span><input type="checkbox" checked={Boolean(value)} onChange={event => onChange(event.target.checked)}/></label>; if (control === "select" && field.presentation.options) return <label className="control"><span>{field.presentation.label}</span><select value={String(value ?? "")} onChange={event => onChange(event.target.value)}>{field.presentation.options.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>; return <label className="control"><span>{field.presentation.label}</span><input type={control === "number" ? "number" : "text"} value={String(value ?? "")} onChange={event => onChange(control === "number" ? Number(event.target.value) : event.target.value)}/><small>{field.presentation.description}</small></label>; }
function StateValue({value}: {value: unknown}) { if (value !== null && typeof value === "object") return <pre>{JSON.stringify(value, null, 2)}</pre>; return <code>{String(value ?? "—")}</code>; }
function InspectorSidebar({graph, onClose}: {graph: Graph; onClose: () => void}) { const [descriptor, setDescriptor] = useState<Descriptor | null>(null); const [tab, setTab] = useState<"prop" | "state">("prop"); const [state, setState] = useState<Record<string, unknown>>({}); const [prop, setProp] = useState<Record<string, unknown>>({}); const [busy, setBusy] = useState(true); useEffect(() => { let active = true; setBusy(true); void fetch(graph.descriptor).then(response => response.json()).then((value: Descriptor) => { if (!active) return; setDescriptor(value); return fetch(value.prop_api); }).then(response => response?.json()).then(value => { if (active && value) setProp(value); }).finally(() => { if (active) setBusy(false); }); return () => { active = false; }; }, [graph]); const loadState = async () => { if (!descriptor) return; setBusy(true); try { setState(await fetch(descriptor.state_api).then(response => response.json())); } finally { setBusy(false); } }; useEffect(() => { if (tab === "state") void loadState(); }, [tab, descriptor]); const update = async (name: string, value: unknown) => { const previous = prop; setProp(current => ({...current, [name]: value})); const result = await fetch(descriptor!.prop_api, {method: "PATCH", headers: {"Content-Type": "application/json"}, body: JSON.stringify({[name]: value})}).then(response => response.json()); if (result.success && result.prop) setProp(result.prop); else setProp(previous); }; const fields = descriptor?.prop.fields ?? []; return <><button className="backdrop" aria-label="Close inspector" onClick={onClose}/><aside className="sidebar" aria-label={`${graph.title} inspector`}><header><div><span className="eyebrow">{graph.category} · {graph.dimension}</span><h2>{graph.title}</h2></div><button className="close" onClick={onClose} aria-label="Close">×</button></header><div className="tabs"><button className={tab === "prop" ? "active" : ""} onClick={() => setTab("prop")}>Editable Prop <span>{fields.length}</span></button><button className={tab === "state" ? "active" : ""} onClick={() => setTab("state")}>Published State <span>{descriptor?.state.fields.length ?? 0}</span></button></div><div className="sidebarBody">{busy && !descriptor ? <p className="muted">Loading descriptor</p> : tab === "prop" ? <section className="propGrid">{fields.map(field => <FieldControl key={field.name} field={field} value={prop[field.name]} onChange={value => void update(field.name, value)}/>)}</section> : <section><div className="stateHeading"><p>Complete published State</p><button onClick={() => void loadState()}>Refresh</button></div><dl className="stateList">{descriptor?.state.fields.map(field => <div key={field.name}><dt>{field.presentation.label}</dt><dd><StateValue value={state[field.name]}/></dd></div>)}</dl></section>}</div></aside></>; }
const GraphCard = memo(function GraphCard({graph, onInspect}: {graph: Graph; onInspect: (graph: Graph) => void}) { const canvasRef = useRef<HTMLCanvasElement>(null); const statusRef = useGraphStream(graph, canvasRef); return <article className="card"><header><div><span className="eyebrow">{graph.category} · {graph.dimension}</span><h2>{graph.title}</h2></div><span ref={statusRef} className="status">CONNECTING</span></header><p>{graph.description}</p><canvas ref={canvasRef}/><button className="inspect" onClick={() => onInspect(graph)}>Inspect Prop & State</button></article>; });
export function App() { const [graphs, setGraphs] = useState<Graph[]>([]); const [category, setCategory] = useState("All"); const [selected, setSelected] = useState<Graph | null>(null); useEffect(() => { void fetch("/api/graphs").then(response => response.json()).then(setGraphs); }, []); useEffect(() => { document.body.classList.toggle("sidebarOpen", selected !== null); return () => document.body.classList.remove("sidebarOpen"); }, [selected]); const categories = useMemo(() => ["All", ...new Set(graphs.map(graph => graph.category))], [graphs]); const visible = category === "All" ? graphs : graphs.filter(graph => graph.category === category); return <><main><section className="hero"><div><span className="eyebrow">AETHERA RENDER LAB</span><h1>Live chart gallery</h1><p>Browser time drives each isolated Scene. Completed pixels return directly from the Scene callback; properties and state are fetched only when inspected.</p></div><div className="heroMetric"><strong>{graphs.length}</strong><span>live components</span></div></section><nav>{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => setCategory(value)}>{value}</button>)}</nav><section className="grid">{visible.map(graph => <GraphCard key={graph.id} graph={graph} onInspect={setSelected}/>)}</section></main>{selected ? <InspectorSidebar graph={selected} onClose={() => setSelected(null)}/> : null}</>; }
File diff suppressed because one or more lines are too long