From b51833499f4a63b3b536792a69f6e4969be97397 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Wed, 12 Aug 2026 06:16:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E5=AE=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_server/app/Gallery_Actions.h | 172 ++++++++++++++++++ web_server/app/Gallery_Protocol.cpp | 140 +++----------- web_server/app/Gallery_Protocol.h | 8 +- web_server/app/Gallery_Renderables.h | 172 +++++++++++++++--- .../app/Gallery_Session_Control_Adminive.h | 18 ++ web_server/tests/Web_Bridge_Tests.cpp | 22 +++ 6 files changed, 386 insertions(+), 146 deletions(-) create mode 100644 web_server/app/Gallery_Actions.h diff --git a/web_server/app/Gallery_Actions.h b/web_server/app/Gallery_Actions.h new file mode 100644 index 0000000..c544c31 --- /dev/null +++ b/web_server/app/Gallery_Actions.h @@ -0,0 +1,172 @@ +#pragma once + +#include "renderive/scene/Scene2D_Context.hpp" +#include + +#include +#include +#include +#include +#include + +namespace renderive::web { + +enum class Gallery_Frame_Mode : std::uint8_t { + Manual, + Low_Latency, + Playback +}; + +struct Gallery_Action_Category {}; + +struct Gallery_Action_Attribute { + using attribute_category = Gallery_Action_Category; + static constexpr bool single_valued = false; + static constexpr bool inheritable = false; + + std::string_view id; + std::string_view label; + std::string_view api; + std::string_view group; + std::string_view description; + std::string_view argument_input; + std::string_view argument_label; + double argument_default{}; + bool request_frame{}; +}; + +constexpr Gallery_Action_Attribute gallery_action( + std::string_view id, std::string_view label, std::string_view api, + std::string_view group, std::string_view description = {}, + std::string_view argument_input = {}, std::string_view argument_label = {}, + double argument_default = 0.0, bool request_frame = false) { + return {id, label, api, group, description, argument_input, argument_label, + argument_default, request_frame}; +} + +struct Gallery_Action_Model { + std::string id; + std::string label; + std::string api; + std::string description; + std::string group; + std::string argument_input; + std::string argument_label; + double argument_default{}; + bool request_frame{}; +}; + +inline Gallery_Action_Model gallery_action_model( + const Gallery_Action_Attribute& action) { + return {std::string(action.id), std::string(action.label), std::string(action.api), + std::string(action.description), std::string(action.group), + std::string(action.argument_input), std::string(action.argument_label), + action.argument_default, action.request_frame}; +} + +template +void append_gallery_actions(std::vector& target) { + structive::type_descriptor() + .template for_each_type_attribute( + [&target](const auto& action) { + const auto found = std::find_if( + target.begin(), target.end(), + [&action](const auto& existing) { return existing.id == action.id; }); + if (found == target.end()) + target.push_back(gallery_action_model(action)); + }); +} + +} // namespace renderive::web + +namespace structive { + +template +struct Type_Descriptor> { + using T = Manual_Refresh_Strategy; + static auto get() { + using renderive::web::gallery_action; + return object(type_metadata( + gallery_action("mode_prepare", "准备帧", + "Manual_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame", + "Manual_Refresh_Strategy"), + gallery_action("mode_refresh", "提交手动刷新", + "Manual_Refresh_Strategy::refresh / Plot_Core::refresh_manual_frame", + "Manual_Refresh_Strategy"), + gallery_action("mode_render", "渲染已刷新帧", + "Manual_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Manual_Refresh_Strategy", {}, {}, {}, 0.0, true), + gallery_action("mode_discard", "丢弃待刷新帧", + "Plot_Core::discard_pending_frame", + "Manual_Refresh_Strategy"), + gallery_action("mode_cycle", "执行完整手动帧周期", + "Plot_Core::render_frame", "Manual_Refresh_Strategy", + {}, {}, {}, 0.0, true))); + } +}; + +template +struct Type_Descriptor> { + using T = Low_Latency_Strategy; + static auto get() { + using renderive::web::gallery_action; + return object(type_metadata( + gallery_action("mode_prepare", "发布低延迟帧", + "Low_Latency_Strategy::acquire_painter / Plot_Core::prepare_frame", + "Low_Latency_Strategy"), + gallery_action("mode_render", "消费最新帧", + "Low_Latency_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Low_Latency_Strategy", {}, {}, {}, 0.0, true), + gallery_action("mode_discard", "丢弃陈旧帧", + "Plot_Core::discard_pending_frame", "Low_Latency_Strategy"), + gallery_action("mode_cycle", "执行低延迟帧周期", + "Plot_Core::render_frame", "Low_Latency_Strategy", + {}, {}, {}, 0.0, true))); + } +}; + +template +struct Type_Descriptor> { + using T = Flow_Refresh_Strategy; + static auto get() { + using renderive::web::gallery_action; + return object(type_metadata( + gallery_action("mode_enqueue", "压入一帧", + "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame", + "Flow_Refresh_Strategy"), + gallery_action("mode_enqueue_burst", "批量压入回放队列", + "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame x N", + "Flow_Refresh_Strategy", {}, "number", "帧数", 8.0), + gallery_action("mode_dequeue", "消费队首帧", + "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Flow_Refresh_Strategy", {}, {}, {}, 0.0, true), + gallery_action("mode_cycle", "执行回放帧周期", + "Plot_Core::render_frame", "Flow_Refresh_Strategy", + {}, {}, {}, 0.0, true))); + } +}; + +} // namespace structive + +namespace renderive::web { + +inline std::vector gallery_frame_actions(Gallery_Frame_Mode mode) { + std::vector result; + switch (mode) { + case Gallery_Frame_Mode::Manual: + append_gallery_actions>>(result); + break; + case Gallery_Frame_Mode::Low_Latency: + append_gallery_actions>>(result); + break; + case Gallery_Frame_Mode::Playback: + append_gallery_actions>>(result); + break; + } + return result; +} + +} // namespace renderive::web diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index f1377d2..ac01a17 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -31,21 +31,7 @@ struct Case_Model { int preferred_height = 320; }; -struct Action_Model { - std::string id; - std::string label; - std::string api; - std::string description; - std::string group; - std::string argument_input; - std::string argument_label; - double argument_default{}; - bool request_frame{}; -}; - -struct Action_Definition { - Action_Model model; -}; +using Action_Model = Gallery_Action_Model; const std::vector& cases() { static const std::vector value{ @@ -69,103 +55,6 @@ const std::vector& cases() { return value; } -Action_Definition action(std::string id, std::string label, std::string api, - std::string group, std::string description = {}, - std::string argument_input = {}, std::string argument_label = {}, - double argument_default = 0) { - return {{std::move(id), std::move(label), std::move(api), std::move(description), - std::move(group), std::move(argument_input), std::move(argument_label), - argument_default}}; -} - -Action_Definition frame_action(std::string id, std::string label, std::string api, - std::string group) { - auto result = action(std::move(id), std::move(label), std::move(api), std::move(group)); - result.model.request_frame = true; - return result; -} - -std::vector actions(std::string_view case_id, - Gallery_Frame_Mode frame_mode) { - std::vector result{ - action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "会话"), - action("toggle_view", "切换 View 生命周期", "Plot_Core::activate_view / deactivate_view", "会话"), - action("axis_probe", "读取坐标映射与刻度", "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", "Axis") - }; - if (frame_mode == Gallery_Frame_Mode::Manual) { - result.push_back(action("mode_prepare", "准备帧", "Manual_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame", - "Manual_Refresh_Strategy")); - result.push_back(action("mode_refresh", "提交手动刷新", "Manual_Refresh_Strategy::refresh / Plot_Core::refresh_manual_frame", - "Manual_Refresh_Strategy")); - result.push_back(frame_action("mode_render", "渲染已刷新帧", "Manual_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", - "Manual_Refresh_Strategy")); - result.push_back(action("mode_discard", "丢弃待刷新帧", "Plot_Core::discard_pending_frame", - "Manual_Refresh_Strategy")); - result.push_back(frame_action("mode_cycle", "执行完整手动帧周期", "Plot_Core::render_frame", - "Manual_Refresh_Strategy")); - } else if (frame_mode == Gallery_Frame_Mode::Low_Latency) { - result.push_back(action("mode_prepare", "发布低延迟帧", "Low_Latency_Strategy::acquire_painter / Plot_Core::prepare_frame", - "Low_Latency_Strategy")); - result.push_back(frame_action("mode_render", "消费最新帧", "Low_Latency_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", - "Low_Latency_Strategy")); - result.push_back(action("mode_discard", "丢弃陈旧帧", "Plot_Core::discard_pending_frame", - "Low_Latency_Strategy")); - result.push_back(frame_action("mode_cycle", "执行低延迟帧周期", "Plot_Core::render_frame", - "Low_Latency_Strategy")); - } else { - result.push_back(action("mode_enqueue", "压入一帧", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame", - "Flow_Refresh_Strategy")); - result.push_back(action("mode_enqueue_burst", "批量压入回放队列", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame × N", - "Flow_Refresh_Strategy", {}, "number", "帧数", 8)); - result.push_back(frame_action("mode_dequeue", "消费队首帧", "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", - "Flow_Refresh_Strategy")); - result.push_back(frame_action("mode_cycle", "执行回放帧周期", "Plot_Core::render_frame", - "Flow_Refresh_Strategy")); - } - if (case_id == "axis_lab") { - result.push_back(action("append_time", "追加时间点", "Time_Axis::append_time / tick_to_time", "Time_Axis")); - result.push_back(action("read_data_shape", "读取轴采样点状态", "Abs_Axis::transform / Time_Axis::time_point_count", "观察者")); - } else if (case_id == "spectrum" || case_id == "selection_overlay") { - result.push_back(action("push_samples", "推送一帧样本", "Spectrum::update_samples", "Spectrum")); - result.push_back(action("power_at", "查询指定频率功率", "Spectrum::power_at", "Spectrum", {}, "number", "频率", 98e6)); - result.push_back(action("add_marker", "添加点 Marker", "Spectrum::add_custom_marker", "Marker", {}, "number", "频率", 96e6)); - result.push_back(action("add_line_marker", "添加线 Marker", "Spectrum::add_custom_line_marker", "Marker", {}, "number", "频率", 100e6)); - result.push_back(action("remove_marker", "按频率删除 Marker", "Spectrum::remove_custom_marker", "Marker", {}, "number", "频率", 96e6)); - result.push_back(action("remove_selected_marker", "删除选中 Marker", "Spectrum::remove_selected_marker", "Marker")); - result.push_back(action("clear_markers", "清空 Marker", "Spectrum::clear_custom_markers", "Marker")); - result.push_back(action("select_marker", "选择 Marker 索引", "Spectrum::set_selected_marker_index", "Marker", {}, "number", "索引", 0)); - result.push_back(action("select_next_marker", "选择下一个 Marker", "Spectrum::select_next_marker", "Marker")); - result.push_back(action("select_previous_marker", "选择上一个 Marker", "Spectrum::select_previous_marker", "Marker")); - result.push_back(action("clear_marker_selection", "清除 Marker 选择", "Spectrum::clear_marker_selection", "Marker")); - result.push_back(action("set_marker_frequency", "修改选中 Marker 频率", - "Spectrum::set_marker_frequency / set_current_marker_frequency", - "Marker", {}, "number", "频率", 99e6)); - result.push_back(action("read_data_shape", "读取输入与绘制点数", "Spectrum::sample_count / rendered_point_count", "观察者")); - if (case_id == "selection_overlay") { - result.push_back(action("clear_selection", "清空框选区域", "Selection_Rectangle_Overlay::clear_selected_regions", "Selection Overlay")); - } - } else if (case_id == "waterfall") { - result.push_back(action("append_row", "追加一行", "Waterfall::append_row", "Waterfall")); - result.push_back(action("append_tick_row", "按 tick 追加一行", "Time_Axis::append_time / Waterfall::append_row(int, span)", "Waterfall")); - result.push_back(action("read_data_shape", "读取行、点与渲染单元", "Waterfall::row_count / stored_point_count / rendered_cell_count", "观察者")); - } else if (case_id == "afterglow") { - result.push_back(action("append_spectrum", "追加一帧频谱", "Afterglow::append_spectrum", "Afterglow")); - result.push_back(action("read_data_shape", "读取历史帧与渲染网格", "Afterglow::history_count / latest_spectrum_point_count / rendered_cell_count", "观察者")); - } else if (case_id == "sweep_spectrum") { - result.push_back(action("append_block", "追加一个扫频块", "Sweep_Spectrum::append_block", "Sweep Spectrum")); - result.push_back(action("read_data_shape", "读取块与绘制点数", "Sweep_Spectrum::stored_block_count / stored_point_count / rendered_point_count", "观察者")); - } else if (case_id == "frequency_trace") { - result.push_back(action("append_sample", "追加一个样本", "Frequency_Trace::append_sample", "Frequency Trace")); - result.push_back(action("append_tick_sample", "按 tick 追加样本", "Time_Axis::append_time / Frequency_Trace::append_sample(int, double)", "Frequency Trace")); - result.push_back(action("read_data_shape", "读取轨迹点数", "Frequency_Trace::sample_count / rendered_point_count / Time_Axis::time_point_count", "观察者")); - } else if (case_id == "constellation") { - result.push_back(action("append_points", "追加一组 IQ 点", "Constellation_Diagram::append_point", "Constellation")); - result.push_back(action("fit_square", "按轴拟合正方形", "Constellation_Diagram::fit_square_to_axes", "Constellation")); - result.push_back(action("read_data_shape", "读取星座点与锚点数", "Constellation_Diagram::point_count", "观察者")); - } - return result; -} - const Case_Model* find_case(std::string_view id) { const auto& all = cases(); const auto found = std::find_if(all.begin(), all.end(), @@ -173,6 +62,17 @@ const Case_Model* find_case(std::string_view id) { return found == all.end() ? nullptr : &*found; } +std::vector registered_actions(std::string_view case_id, + Gallery_Frame_Mode frame_mode) { + std::vector result; + append_gallery_actions(result); + for (auto& action : gallery_frame_actions(frame_mode)) { + result.push_back(std::move(action)); + } + append_gallery_renderable_actions(case_id, result); + return result; +} + Json parse_request(std::string_view message) { return Json::parse(message.begin(), message.end()); } @@ -493,7 +393,8 @@ std::string Gallery_Protocol::catalog_json() { gallery_renderable_control_count(item.id); for (const auto mode : frame_modes) { const std::string key(frame_mode_id(mode)); - const std::size_t actions = gallery_detail::actions(item.id, mode).size(); + const std::size_t actions = + gallery_detail::registered_actions(item.id, mode).size(); entry["control_count_by_mode"][key] = controls; entry["action_count_by_mode"][key] = actions; controls_total += controls; @@ -501,7 +402,8 @@ std::string Gallery_Protocol::catalog_json() { } entry["control_count"] = controls; entry["action_count"] = - gallery_detail::actions(item.id, Gallery_Frame_Mode::Low_Latency).size(); + gallery_detail::registered_actions( + item.id, Gallery_Frame_Mode::Low_Latency).size(); result["cases"].push_back(std::move(entry)); } result["coverage"] = {{"case_count", gallery_detail::cases().size()}, @@ -541,9 +443,9 @@ std::string Gallery_Protocol::case_json_from_controls( gallery_detail::action_view())}, {"data", Json::array()} }; - for (const auto& definition : gallery_detail::actions(case_id, frame_mode)) + for (const auto& action : gallery_detail::registered_actions(case_id, frame_mode)) result["actions"]["data"].push_back( - adminive::to_frontend_json(definition.model)); + adminive::to_frontend_json(action)); try { result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); } catch (const std::exception&) { @@ -647,10 +549,10 @@ std::optional Gallery_Protocol::control_patch_req bool Gallery_Protocol::action_available(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view action_id) { - const auto available = gallery_detail::actions(case_id, frame_mode); + const auto available = gallery_detail::registered_actions(case_id, frame_mode); return std::any_of(available.begin(), available.end(), - [action_id](const gallery_detail::Action_Definition& item) { - return item.model.id == action_id; + [action_id](const gallery_detail::Action_Model& item) { + return item.id == action_id; }); } diff --git a/web_server/app/Gallery_Protocol.h b/web_server/app/Gallery_Protocol.h index 015c91a..916da46 100644 --- a/web_server/app/Gallery_Protocol.h +++ b/web_server/app/Gallery_Protocol.h @@ -1,15 +1,11 @@ #pragma once -#include +#include "Gallery_Actions.h" + #include #include #include #include namespace renderive::web { -enum class Gallery_Frame_Mode : std::uint8_t { - Manual, - Low_Latency, - Playback -}; struct Gallery_Open_Request { std::string case_id; Gallery_Frame_Mode frame_mode = Gallery_Frame_Mode::Low_Latency; diff --git a/web_server/app/Gallery_Renderables.h b/web_server/app/Gallery_Renderables.h index 0d14665..5b89929 100644 --- a/web_server/app/Gallery_Renderables.h +++ b/web_server/app/Gallery_Renderables.h @@ -1,5 +1,6 @@ #pragma once +#include "Gallery_Actions.h" #include "Gallery_Renderable_Types.h" #include "adminive/adminive.hpp" @@ -538,6 +539,112 @@ RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Constellation_Diagram, Constellation_Diagra } // namespace adminive +namespace renderive::web::gallery_action_registry { + +constexpr auto axis_metadata() { + return structive::type_metadata( + gallery_action("axis_probe", "读取坐标映射与刻度", + "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", + "Axis")); +} + +} // namespace renderive::web::gallery_action_registry + +namespace structive { + +#define RENDERIVE_TYPE_ACTIONS(Type, ...) \ + template <> struct Type_Descriptor { \ + static auto get() { \ + using T = renderive::web::Type; \ + using renderive::web::gallery_action; \ + return object(type_metadata(__VA_ARGS__)); \ + } \ + } + +template <> +struct Type_Descriptor { + static auto get() { + return object( + renderive::web::gallery_action_registry::axis_metadata()); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + return object( + renderive::web::gallery_action_registry::axis_metadata()); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::web::Gallery_Time_Axis; + using renderive::web::gallery_action; + return object(type_metadata( + gallery_action("axis_probe", "读取坐标映射与刻度", + "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", + "Axis"), + gallery_action("append_time", "追加时间点", + "Time_Axis::append_time / tick_to_time", "Time_Axis"))); + } +}; + +RENDERIVE_TYPE_ACTIONS(Gallery_Spectrum, + gallery_action("push_samples", "推送一帧样本", "Spectrum::update_samples", "Spectrum"), + gallery_action("power_at", "查询指定频率功率", "Spectrum::power_at", "Spectrum", + {}, "number", "频率", 98e6), + gallery_action("add_marker", "添加点 Marker", "Spectrum::add_custom_marker", "Marker", + {}, "number", "频率", 96e6), + gallery_action("add_line_marker", "添加线 Marker", "Spectrum::add_custom_line_marker", "Marker", + {}, "number", "频率", 100e6), + gallery_action("remove_marker", "按频率删除 Marker", "Spectrum::remove_custom_marker", "Marker", + {}, "number", "频率", 96e6), + gallery_action("remove_selected_marker", "删除选中 Marker", + "Spectrum::remove_selected_marker", "Marker"), + gallery_action("clear_markers", "清空 Marker", "Spectrum::clear_custom_markers", "Marker"), + gallery_action("select_marker", "选择 Marker 索引", "Spectrum::set_selected_marker_index", "Marker", + {}, "number", "索引", 0.0), + gallery_action("select_next_marker", "选择下一个 Marker", "Spectrum::select_next_marker", "Marker"), + gallery_action("select_previous_marker", "选择上一个 Marker", "Spectrum::select_previous_marker", "Marker"), + gallery_action("clear_marker_selection", "清除 Marker 选择", + "Spectrum::clear_marker_selection", "Marker"), + gallery_action("set_marker_frequency", "修改选中 Marker 频率", + "Spectrum::set_marker_frequency / set_current_marker_frequency", "Marker", + {}, "number", "频率", 99e6)); + +RENDERIVE_TYPE_ACTIONS(Gallery_Waterfall, + gallery_action("append_row", "追加一行", "Waterfall::append_row", "Waterfall"), + gallery_action("append_tick_row", "按 tick 追加一行", + "Time_Axis::append_time / Waterfall::append_row(int, span)", "Waterfall")); + +RENDERIVE_TYPE_ACTIONS(Gallery_Afterglow, + gallery_action("append_spectrum", "追加一帧频谱", "Afterglow::append_spectrum", "Afterglow")); + +RENDERIVE_TYPE_ACTIONS(Gallery_Sweep_Spectrum, + gallery_action("append_block", "追加一个扫频块", "Sweep_Spectrum::append_block", "Sweep Spectrum")); + +RENDERIVE_TYPE_ACTIONS(Gallery_Frequency_Trace, + gallery_action("append_sample", "追加一个样本", "Frequency_Trace::append_sample", "Frequency Trace"), + gallery_action("append_tick_sample", "按 tick 追加样本", + "Time_Axis::append_time / Frequency_Trace::append_sample(int, double)", + "Frequency Trace")); + +RENDERIVE_TYPE_ACTIONS(Gallery_Selection_Rectangle_Overlay, + gallery_action("clear_selection", "清空框选区域", + "Selection_Rectangle_Overlay::clear_selected_regions", "Selection Overlay")); + +RENDERIVE_TYPE_ACTIONS(Gallery_Constellation_Diagram, + gallery_action("append_points", "追加一组 IQ 点", + "Constellation_Diagram::append_point", "Constellation"), + gallery_action("fit_square", "按轴拟合正方形", + "Constellation_Diagram::fit_square_to_axes", "Constellation")); + +#undef RENDERIVE_TYPE_ACTIONS + +} // namespace structive + namespace renderive::web { template @@ -557,28 +664,51 @@ std::size_t gallery_control_count() { return count_fields(count_fields, descriptor.at("fields")); } +struct Gallery_Case_Type_Registration { + std::string_view case_id; + std::size_t (*control_count)(); + void (*append_actions)(std::vector&); +}; + +template +Gallery_Case_Type_Registration gallery_case_types(std::string_view case_id) { + return { + case_id, + [] { return (std::size_t{} + ... + gallery_control_count()); }, + [](std::vector& actions) { + (append_gallery_actions(actions), ...); + } + }; +} + +inline const Gallery_Case_Type_Registration* gallery_case_registration( + std::string_view case_id) { + static const std::array registrations{ + gallery_case_types("axis_lab"), + gallery_case_types("spectrum"), + gallery_case_types("afterglow"), + gallery_case_types("sweep_spectrum"), + gallery_case_types("waterfall"), + gallery_case_types("frequency_trace"), + gallery_case_types("selection_overlay"), + gallery_case_types("constellation") + }; + const auto found = std::find_if( + registrations.begin(), registrations.end(), + [case_id](const auto& registration) { return registration.case_id == case_id; }); + return found == registrations.end() ? nullptr : &*found; +} + inline std::size_t gallery_renderable_control_count(std::string_view case_id) { - const auto axis = gallery_control_count(); - const auto frequency_axis = gallery_control_count(); - const auto time_axis = gallery_control_count(); - if (case_id == "axis_lab") - return frequency_axis + time_axis; - if (case_id == "spectrum") - return frequency_axis + axis + gallery_control_count(); - if (case_id == "selection_overlay") - return frequency_axis + axis + gallery_control_count() + - gallery_control_count(); - if (case_id == "waterfall") - return frequency_axis + time_axis + gallery_control_count(); - if (case_id == "afterglow") - return frequency_axis + axis + gallery_control_count(); - if (case_id == "sweep_spectrum") - return 2 * axis + gallery_control_count(); - if (case_id == "frequency_trace") - return time_axis + axis + gallery_control_count(); - if (case_id == "constellation") - return 2 * axis + gallery_control_count(); - return 0; + const auto* registration = gallery_case_registration(case_id); + return registration ? registration->control_count() : 0; +} + +inline void append_gallery_renderable_actions( + std::string_view case_id, std::vector& actions) { + if (const auto* registration = gallery_case_registration(case_id)) + registration->append_actions(actions); } } // namespace renderive::web diff --git a/web_server/app/Gallery_Session_Control_Adminive.h b/web_server/app/Gallery_Session_Control_Adminive.h index 997f535..11eccd5 100644 --- a/web_server/app/Gallery_Session_Control_Adminive.h +++ b/web_server/app/Gallery_Session_Control_Adminive.h @@ -120,3 +120,21 @@ static_assert(Described_Type); static_assert(Direct_Described_Object); } // namespace adminive + +namespace structive { + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::web::Gallery_Session_Control; + using renderive::web::gallery_action; + return object(type_metadata( + gallery_action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "Session"), + gallery_action("toggle_view", "切换 View 生命周期", + "Plot_Core::activate_view / deactivate_view", "Session"), + gallery_action("read_data_shape", "读取数据形状", + "Renderable data query / observer telemetry", "Observer"))); + } +}; + +} // namespace structive diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 405bf22..ce9d07c 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -1,5 +1,6 @@ #include "../app/Gallery_Plot_Session.h" #include "../app/Gallery_Protocol.h" +#include "../app/Gallery_Renderables.h" #include "../app/Pixel_Frame.h" #include "../app/Web_Event_Adapter.h" #include "../app/Web_Plot_Session.h" @@ -267,6 +268,27 @@ TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) { EXPECT_TRUE(action->at("request_frame")); } +TEST(RenderiveWebGallery, ActionsComeFromStructiveTypeMetadata) { + using Spectrum_Schema = structive::type_descriptor_schema_t; + using Time_Axis_Schema = structive::type_descriptor_schema_t; + using Manual_Strategy = + Manual_Refresh_Strategy>; + using Manual_Schema = structive::type_descriptor_schema_t; + static_assert(Spectrum_Schema::template type_attribute_count< + Gallery_Action_Category> == 12); + static_assert(Time_Axis_Schema::template type_attribute_count< + Gallery_Action_Category> == 2); + static_assert(Manual_Schema::template type_attribute_count< + Gallery_Action_Category> == 5); + + std::vector actions; + append_gallery_actions(actions); + EXPECT_EQ(actions.size(), 12U); + EXPECT_TRUE(std::any_of(actions.begin(), actions.end(), [](const auto& action) { + return action.id == "power_at" && action.argument_input == "number"; + })); +} + TEST(RenderiveWebBridge, ComposesAdditionalEventDecoders) { const auto event = Extended_Web_Event_Adapter::decode( R"({"category":"event","type":"spatial_test","layer":7})");