#include "Gallery_Protocol.h" #include "adminive/adminive.hpp" #include "adminive/adapters/nlohmann_json.hpp" #include #include #include #include #include #include #include namespace renderive::web::gallery_detail { using Json = nlohmann::json; struct Case_Model { std::string id; std::string title; std::string component; std::string category; std::string description; int order{}; int preferred_width = 560; int preferred_height = 320; }; struct Control_Model { std::string id; std::string label; std::string api; std::string description; std::string group; std::string input; bool integer{}; double minimum{}; double maximum{}; double step{}; }; 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{}; }; struct Control_Definition { Control_Model model; Gallery_Value initial; std::vector options; }; struct Action_Definition { Action_Model model; }; const std::vector& cases() { static const std::vector value{ {"axis_lab", "坐标轴实验室", "Axis / Frequency_Axis / Time_Axis", "基础控件", "普通轴、频率轴和时间轴的布局、格式化、滚轮与拖拽 API。", 10}, {"spectrum", "实时频谱", "Spectrum", "三种频率图", "频率控制模式 1:曲线、保持线、扫频区、峰值和自定义 Marker。", 20}, {"afterglow", "余辉频谱", "Afterglow", "三种频率图", "频率控制模式 2:二维功率密度、衰减和功率 bin 插值。", 30}, {"sweep_spectrum", "扫频频谱", "Sweep_Spectrum", "三种频率图", "频率控制模式 3:分块扫频、当前频率游标和六种线插值。", 40}, {"waterfall", "瀑布图", "Waterfall", "热力图控件", "逐行时频热力图;Nearest、Bilinear、Bicubic 三种图像模式均可切换。", 50}, {"frequency_trace", "时间轨迹", "Frequency_Trace", "曲线控件", "Time_Axis 驱动的连续轨迹,覆盖时间格式和 Builder 笔刷 API。", 60}, {"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件", "鼠标框选、多区域保留、标注样式、清空与轴重新绑定。", 70}, {"constellation", "星座图", "Constellation_Diagram", "点图控件", "PSK4/PSK8/PSK16 Builder 模式、相位、寿命、坐标范围和方形拟合。", 80} }; return value; } Control_Definition flag(std::string id, std::string label, std::string api, bool initial, std::string group, std::string description = {}) { return {{std::move(id), std::move(label), std::move(api), std::move(description), std::move(group), "boolean"}, initial, {}}; } Control_Definition number(std::string id, std::string label, std::string api, double initial, double minimum, double maximum, double step, std::string group, bool integer = false, std::string description = {}) { return {{std::move(id), std::move(label), std::move(api), std::move(description), std::move(group), "number", integer, minimum, maximum, step}, initial, {}}; } Control_Definition text_control(std::string id, std::string label, std::string api, std::string initial, std::string group, std::string description = {}) { return {{std::move(id), std::move(label), std::move(api), std::move(description), std::move(group), "text"}, std::move(initial), {}}; } Control_Definition color(std::string id, std::string label, std::string api, std::string initial, std::string group, std::string description = {}) { auto result = text_control(std::move(id), std::move(label), std::move(api), std::move(initial), std::move(group), std::move(description)); result.model.input = "color"; return result; } Control_Definition select(std::string id, std::string label, std::string api, std::string initial, std::vector options, std::string group, std::string description = {}) { return {{std::move(id), std::move(label), std::move(api), std::move(description), std::move(group), "select"}, std::move(initial), std::move(options)}; } void append_common(std::vector& result, Gallery_Frame_Mode frame_mode) { result.push_back(color("background_color", "背景颜色", "Plot_Core::set_background_color", "#07111f", "Plot_Core")); if (frame_mode == Gallery_Frame_Mode::Low_Latency) { result.push_back(number("max_render_fps", "最大渲染 FPS", "Plot_Core::set_max_render_fps", 30, 0.01, 1'000'000'000, 1, "Low_Latency_Strategy", false, "Kernel 接受任意有限正频率;高目标用于触发并观察 Painter 或后台 Render 瓶颈。")); } result.push_back(flag("performance_overlay", "性能叠层", "set_performance_overlay_enabled", false, "Plot_Core")); result.push_back(flag("renderable_visible", "控件可见", "Renderable::set_visible", true, "Renderable")); result.push_back(select("cache_mode", "缓存模式", "Renderable::set_cache_mode", "Local_Pixel", {"Direct", "Local_Pixel"}, "Renderable")); result.push_back(text_control("object_name", "对象名称", "Renderable::set_object_name", "Gallery_Renderable", "Renderable")); } void append_axis(std::vector& result) { result.push_back(select("axis_orientation", "主轴方向", "Abs_Axis::set_orientation", "Horizontal", {"Horizontal", "Vertical"}, "Abs_Axis")); result.push_back(number("axis_x_offset", "X 偏移", "Abs_Axis::set_x", 0, -160, 160, 1, "Abs_Axis", true)); result.push_back(number("axis_y_offset", "Y 偏移", "Abs_Axis::set_y", 0, -160, 160, 1, "Abs_Axis", true)); result.push_back(number("axis_length_percent", "轴长百分比", "Abs_Axis::set_pixel_length", 100, 25, 125, 1, "Abs_Axis", true)); result.push_back(number("tick_length", "主刻度长度", "Abs_Axis::set_tick_length", 8, -24, 24, 1, "Abs_Axis", true)); result.push_back(number("sub_tick_length", "次刻度长度", "Abs_Axis::set_sub_tick_length", 4, -16, 16, 1, "Abs_Axis", true)); result.push_back(color("axis_color", "轴颜色", "Abs_Axis::set_color", "#7691b8", "Abs_Axis")); result.push_back(select("decimal_separator", "小数点", "Abs_Axis::set_locale", ".", {".", ","}, "Abs_Axis")); result.push_back(text_control("unit_text", "单位文本", "Abs_Axis::set_unit_text", "Hz", "Abs_Axis")); result.push_back(number("unit_font_size", "单位字号", "Abs_Axis::set_unit_text_font", 12, 6, 36, 1, "Abs_Axis")); result.push_back(number("unit_font_weight", "单位字重", "Abs_Axis::set_unit_text_font", 500, 100, 900, 100, "Abs_Axis", true)); result.push_back(flag("unit_font_italic", "单位斜体", "Abs_Axis::set_unit_text_font", false, "Abs_Axis")); result.push_back(color("unit_pen_color", "单位文字颜色", "Abs_Axis::set_unit_text_pen", "#dce9ff", "Abs_Axis")); result.push_back(color("unit_background_color", "单位背景颜色", "Abs_Axis::set_unit_text_background_brush", "#07111f", "Abs_Axis")); result.push_back(number("label_rotation", "标签旋转角度", "Abs_Axis::set_label_rotation_degrees", 0, -90, 90, 1, "Abs_Axis", true)); result.push_back(number("coord_origin", "坐标起点", "Axis::set_coord_range", 88'000'000, -1e10, 1e10, 1000, "Axis")); result.push_back(number("coord_target", "坐标终点", "Axis::set_coord_range", 108'000'000, -1e10, 1e10, 1000, "Axis")); result.push_back(number("label_precision", "标签精度", "Axis::set_label_precision", 2, 0, 9, 1, "Axis", true)); result.push_back(flag("use_wheel", "启用滚轮缩放", "Axis::set_use_wheel", true, "Axis")); result.push_back(flag("use_drag", "启用拖拽平移", "Axis::set_use_drag", true, "Axis")); } void append_time_axis(std::vector& result) { result.push_back(number("time_visible_count", "可见时间点", "Time_Axis::set_visible_time_point_count", 80, 8, 400, 1, "Time_Axis", true)); result.push_back(number("time_label_spacing", "时间标签间距", "Time_Axis::set_tick_label_spacing_px", 28, 0, 120, 1, "Time_Axis", true)); result.push_back(text_control("time_format", "时间格式", "Time_Axis::set_time_format", "mm:ss.zzz", "Time_Axis")); result.push_back(number("time_font_size", "时间字体", "Time_Axis::set_font", 11, 6, 32, 1, "Time_Axis")); result.push_back(flag("time_newest_at_start", "最新数据位于轴起点", "Time_Axis::set_newest_at_axis_start", false, "Time_Axis")); } void append_hover(std::vector& result) { result.push_back(flag("hover_enabled", "悬浮信息", "Hover_Tooltip_Mixin::set_use_hover_info", true, "Hover Tooltip")); result.push_back(number("hover_font_size", "悬浮字体", "Hover_Tooltip_Mixin::set_hover_tooltip_font", 12, 6, 32, 1, "Hover Tooltip")); result.push_back(color("hover_text_color", "悬浮文字", "Hover_Tooltip_Mixin::set_tooltip_text_pen", "#ffffff", "Hover Tooltip")); result.push_back(color("hover_background", "悬浮背景", "Hover_Tooltip_Mixin::set_hover_tooltip_background_brush", "#111827", "Hover Tooltip")); } std::vector controls(std::string_view case_id, Gallery_Frame_Mode frame_mode) { std::vector result; append_common(result, frame_mode); append_axis(result); const auto set_initial = [&result](std::string_view id, Gallery_Value value) { const auto found = std::find_if(result.begin(), result.end(), [id](const auto& item) { return item.model.id == id; }); if (found != result.end()) found->initial = std::move(value); }; if (case_id == "sweep_spectrum") { set_initial("coord_origin", 0.0); set_initial("coord_target", 300.0); set_initial("unit_text", std::string("MHz")); } else if (case_id == "frequency_trace") { set_initial("coord_origin", -1.0); set_initial("coord_target", 1.0); set_initial("unit_text", std::string("value")); } else if (case_id == "constellation") { set_initial("coord_origin", -1.25); set_initial("coord_target", 1.25); set_initial("unit_text", std::string("I / Q")); } if (case_id == "axis_lab") { append_time_axis(result); return result; } if (case_id == "spectrum" || case_id == "selection_overlay") { result.push_back(number("frequency_point_size", "频率点数", "Spectrum::set_frequency_point_size", 512, 16, 4096, 16, "Spectrum", true)); result.push_back(number("frequency_origin", "数据频率起点", "Spectrum::set_frequency_range", 88e6, -1e10, 1e10, 1000, "Spectrum")); result.push_back(number("frequency_target", "数据频率终点", "Spectrum::set_frequency_range", 108e6, -1e10, 1e10, 1000, "Spectrum")); result.push_back(number("center_frequency", "中心频率", "Spectrum::set_center_frequency", 98e6, -1e10, 1e10, 1000, "Spectrum")); result.push_back(number("sweep_origin", "扫频区起点", "Spectrum::set_sweep_frequency_range", 94e6, -1e10, 1e10, 1000, "Spectrum")); result.push_back(number("sweep_target", "扫频区终点", "Spectrum::set_sweep_frequency_range", 102e6, -1e10, 1e10, 1000, "Spectrum")); result.push_back(flag("max_hold_visible", "最大保持线", "Spectrum::set_max_hold_visible", true, "Spectrum")); result.push_back(flag("min_hold_visible", "最小保持线", "Spectrum::set_min_hold_visible", false, "Spectrum")); result.push_back(flag("max_marker_visible", "峰值 Marker", "Spectrum::set_max_marker_visible", true, "Spectrum")); result.push_back(flag("use_min_marker", "最小值 Marker", "Spectrum::set_use_min_marker", false, "Spectrum")); result.push_back(flag("sweep_region_visible", "扫频区域", "Spectrum::set_sweep_region_visible", true, "Spectrum")); result.push_back(flag("visible_range_only", "仅绘制可见范围", "Spectrum::set_visible_range_only", true, "Spectrum")); result.push_back(select("line_interpolation", "线插值模式", "Spectrum::set_interpolation_mode", "Linear_Value", {"Nearest_Sample", "Linear_Value", "Linear_Power_Domain", "Step_Left", "Step_Right", "Cubic_Value"}, "Spectrum")); result.push_back(color("max_brush", "最大保持填充", "Spectrum::set_max_brush", "#332810", "Spectrum")); result.push_back(color("current_brush", "当前曲线填充", "Spectrum::set_current_brush", "#0e3a35", "Spectrum")); result.push_back(color("min_brush", "最小保持填充", "Spectrum::set_min_brush", "#19213a", "Spectrum")); result.push_back(color("max_pen", "最大保持线颜色", "Spectrum::set_max_pen", "#ffd166", "Spectrum")); result.push_back(color("current_pen", "当前曲线颜色", "Spectrum::set_current_pen", "#35e6b2", "Spectrum")); result.push_back(color("min_pen", "最小保持线颜色", "Spectrum::set_min_pen", "#7aa2ff", "Spectrum")); result.push_back(color("selected_marker_pen", "选中 Marker", "Spectrum::set_selected_marker_pen", "#ff4d8d", "Spectrum")); result.push_back(color("marker_pen", "Marker 颜色", "Spectrum::set_marker_pen", "#ff7a59", "Spectrum")); result.push_back(color("middle_frequency_pen", "中心频率线", "Spectrum::set_middle_frequency_pen", "#6bc8ff", "Spectrum")); result.push_back(color("sweep_region_brush", "扫频区填充", "Spectrum::set_sweep_region_brush", "#293257", "Spectrum")); append_hover(result); if (case_id == "selection_overlay") { result.push_back(number("selection_font_size", "框选标签字体", "Selection_Rectangle_Overlay::set_label_font", 12, 6, 32, 1, "Selection Overlay")); result.push_back(color("selection_label_pen", "框选标签颜色", "Selection_Rectangle_Overlay::set_label_pen", "#ffffff", "Selection Overlay")); result.push_back(color("selection_brush", "框选填充", "Selection_Rectangle_Overlay::set_selection_brush", "#173b66", "Selection Overlay")); result.push_back(color("selection_border", "框选边框", "Selection_Rectangle_Overlay::set_selection_border_pen", "#7dd3fc", "Selection Overlay")); } return result; } if (case_id == "waterfall") { append_time_axis(result); result.push_back(number("frequency_origin", "频率起点", "Waterfall::set_frequency_range", 88e6, -1e10, 1e10, 1000, "Waterfall")); result.push_back(number("frequency_target", "频率终点", "Waterfall::set_frequency_range", 108e6, -1e10, 1e10, 1000, "Waterfall")); result.push_back(number("power_origin", "功率起点", "Waterfall::set_power_range", -120, -240, 120, 1, "Waterfall")); result.push_back(number("power_target", "功率终点", "Waterfall::set_power_range", -20, -240, 120, 1, "Waterfall")); result.push_back(number("frequency_bin_count", "频率 Bin", "Waterfall::set_frequency_bin_count", 256, 16, 2048, 16, "Waterfall", true)); result.push_back(flag("visible_range_only", "仅绘制可见范围", "Waterfall::set_visible_range_only", true, "Waterfall")); result.push_back(select("image_interpolation", "图像插值(三模式)", "Waterfall::set_interpolation_mode", "Nearest", {"Nearest", "Bilinear", "Bicubic"}, "Waterfall")); result.push_back(select("color_map", "颜色映射(重建 Builder)", "Waterfall::Builder::set_color_map / Color_Map::set_colors", "Spectrum", {"Spectrum", "Ember", "Grayscale"}, "Waterfall")); append_hover(result); return result; } if (case_id == "afterglow") { result.push_back(number("frequency_origin", "频率起点", "Afterglow::set_frequency_range", 88e6, -1e10, 1e10, 1000, "Afterglow")); result.push_back(number("frequency_target", "频率终点", "Afterglow::set_frequency_range", 108e6, -1e10, 1e10, 1000, "Afterglow")); result.push_back(number("power_origin", "功率起点", "Afterglow::set_power_range", -120, -240, 120, 1, "Afterglow")); result.push_back(number("power_target", "功率终点", "Afterglow::set_power_range", -20, -240, 120, 1, "Afterglow")); result.push_back(number("frequency_point_size", "频率格点", "Afterglow::set_frequency_point_size", 192, 16, 1024, 8, "Afterglow", true)); result.push_back(number("power_point_size", "功率格点", "Afterglow::set_power_point_size", 96, 8, 512, 8, "Afterglow", true)); result.push_back(flag("interpolate_power", "功率 Bin 插值", "Afterglow::set_interpolate", true, "Afterglow")); result.push_back(number("attenuation_rate", "衰减率", "Afterglow::set_attenuation_rate", 0.18, 0.01, 1, 0.01, "Afterglow")); result.push_back(select("color_map", "颜色映射(重建 Builder)", "Afterglow::Builder::set_color_map / Color_Map::set_colors", "Spectrum", {"Spectrum", "Ember", "Grayscale"}, "Afterglow")); return result; } if (case_id == "sweep_spectrum") { result.push_back(number("frequency_origin", "扫频起点", "Sweep_Spectrum::set_frequency_range", 0, -1e10, 1e10, 1, "Sweep Spectrum")); result.push_back(number("frequency_target", "扫频终点", "Sweep_Spectrum::set_frequency_range", 300, -1e10, 1e10, 1, "Sweep Spectrum")); result.push_back(number("bins_per_block", "每块 Bin", "Sweep_Spectrum::set_bins_per_block", 64, 2, 1024, 1, "Sweep Spectrum", true)); result.push_back(number("block_count", "块数量", "Sweep_Spectrum::set_block_count", 8, 1, 128, 1, "Sweep Spectrum", true)); result.push_back(color("sweep_pen", "扫频曲线", "Sweep_Spectrum::set_pen", "#ffd166", "Sweep Spectrum")); result.push_back(color("current_frequency_pen", "当前频率游标", "Sweep_Spectrum::set_cur_frequency_pen", "#ff5d73", "Sweep Spectrum")); result.push_back(flag("visible_range_only", "仅绘制可见范围", "Sweep_Spectrum::set_visible_range_only", true, "Sweep Spectrum")); result.push_back(select("line_interpolation", "线插值模式", "Sweep_Spectrum::set_interpolation_mode", "Linear_Value", {"Nearest_Sample", "Linear_Value", "Linear_Power_Domain", "Step_Left", "Step_Right", "Cubic_Value"}, "Sweep Spectrum")); return result; } if (case_id == "frequency_trace") { append_time_axis(result); result.push_back(color("trace_pen", "轨迹颜色(重建 Builder)", "Frequency_Trace::Builder::set_pen", "#35e6b2", "Frequency Trace")); result.push_back(number("trace_pen_width", "轨迹宽度(重建 Builder)", "Frequency_Trace::Builder::set_pen", 2, 0.25, 12, 0.25, "Frequency Trace")); result.push_back(number("trace_value_min", "数值下限", "Axis::set_coord_range", -1, -1e6, 1e6, 0.1, "Frequency Trace")); result.push_back(number("trace_value_max", "数值上限", "Axis::set_coord_range", 1, -1e6, 1e6, 0.1, "Frequency Trace")); return result; } if (case_id == "constellation") { result.push_back(number("i_origin", "I 起点", "Constellation_Diagram::set_i_range", -1.25, -1000, 1000, 0.05, "Constellation")); result.push_back(number("i_target", "I 终点", "Constellation_Diagram::set_i_range", 1.25, -1000, 1000, 0.05, "Constellation")); result.push_back(number("q_origin", "Q 起点", "Constellation_Diagram::set_q_range", -1.25, -1000, 1000, 0.05, "Constellation")); result.push_back(number("q_target", "Q 终点", "Constellation_Diagram::set_q_range", 1.25, -1000, 1000, 0.05, "Constellation")); result.push_back(color("point_color", "采样点颜色", "Constellation_Diagram::set_point_color", "#49e6c3", "Constellation")); result.push_back(color("anchor_color", "锚点颜色", "Constellation_Diagram::set_anchor_color", "#ffd166", "Constellation")); result.push_back(number("point_lifetime_ms", "采样点寿命", "Constellation_Diagram::set_point_lifetime_ms", 1800, 50, 20000, 50, "Constellation", true)); result.push_back(select("constellation_type", "星座模式(重建 Builder)", "Constellation_Diagram::Builder::set_type", "PSK8", {"PSK4", "PSK8", "PSK16"}, "Constellation")); result.push_back(number("phase_offset", "相位偏移(重建 Builder)", "Constellation_Diagram::Builder::set_phase_offset_radians", 0, -6.283185307, 6.283185307, 0.05, "Constellation")); return result; } return result; } 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}}; } std::vector actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) { std::vector result{ action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "会话"), action("rebuild_renderable", "移除并重建控件", "Plot_Core::remove_renderable / Builder::build", "会话"), action("toggle_view", "切换 View 生命周期", "Plot_Core::activate_view / deactivate_view", "会话"), action("axis_set_start", "通过 set_coord_start 应用起点", "Axis::set_coord_start", "Axis"), action("axis_set_length", "通过 set_coord_length 应用跨度", "Axis::set_coord_length", "Axis"), 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(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(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(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(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(action("mode_dequeue", "消费队首帧", "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", "Flow_Refresh_Strategy")); result.push_back(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::pixel_sample_count / 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("rebind_axes", "重新绑定频率轴和功率轴", "Spectrum::set_frequency_axis / set_power_axis", "Spectrum")); 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")); result.push_back(action("rebind_selection_axes", "重新绑定框选轴", "Selection_Rectangle_Overlay::set_horizontal_axis / set_vertical_axis", "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("rebind_axes", "读取并重新绑定轴", "Waterfall::frequency_axis / time_axis", "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("rebind_axes", "读取轴绑定", "Afterglow::frequency_axis / power_axis", "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("rebind_axes", "读取轴绑定", "Sweep_Spectrum::frequency_axis / power_axis", "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_axes", "读取时间轴与值轴", "Frequency_Trace::time_axis / value_axis", "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_axes", "读取 I/Q 轴", "Constellation_Diagram::i_axis / q_axis", "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(), [id](const Case_Model& item) { return item.id == id; }); return found == all.end() ? nullptr : &*found; } const Control_Definition* find_control(const std::vector& all, std::string_view id) { const auto found = std::find_if(all.begin(), all.end(), [id](const Control_Definition& item) { return item.model.id == id; }); return found == all.end() ? nullptr : &*found; } bool valid_color(std::string_view value) { if (value.size() != 7 || value.front() != '#') return false; return std::all_of(value.begin() + 1, value.end(), [](char character) { return std::isxdigit(static_cast(character)) != 0; }); } Json value_json(const Gallery_Value& value) { return std::visit([](const auto& item) { return Json(item); }, value); } std::optional parse_value(const Json& value, const Control_Definition& definition, std::string& error) { if (definition.model.input == "boolean") { if (!value.is_boolean()) { error = "必须是布尔值"; return std::nullopt; } return Gallery_Value(value.get()); } if (definition.model.input == "number") { if (!value.is_number()) { error = "必须是数字"; return std::nullopt; } const double number_value = value.get(); if (!std::isfinite(number_value) || number_value < definition.model.minimum || number_value > definition.model.maximum) { error = "数值超出后端允许范围"; return std::nullopt; } if (definition.model.integer && std::floor(number_value) != number_value) { error = "必须是整数"; return std::nullopt; } return Gallery_Value(number_value); } if (!value.is_string()) { error = "必须是字符串"; return std::nullopt; } std::string string_value = value.get(); if (definition.model.input == "color" && !valid_color(string_value)) { error = "颜色必须使用 #RRGGBB"; return std::nullopt; } if (definition.model.input == "select" && std::find(definition.options.begin(), definition.options.end(), string_value) == definition.options.end()) { error = "枚举值不在后端选项中"; return std::nullopt; } if (string_value.size() > 160) { error = "文本过长"; return std::nullopt; } return Gallery_Value(std::move(string_value)); } double numeric(const Gallery_State& state, std::string_view id) { const auto found = state.values.find(id); return found == state.values.end() ? 0.0 : std::get(found->second); } std::optional range_error(const Gallery_State& state) { constexpr std::pair pairs[] = { {"coord_origin", "coord_target"}, {"frequency_origin", "frequency_target"}, {"power_origin", "power_target"}, {"sweep_origin", "sweep_target"}, {"trace_value_min", "trace_value_max"}, {"i_origin", "i_target"}, {"q_origin", "q_target"} }; for (const auto& [origin, target] : pairs) { if (state.values.contains(origin) && state.values.contains(target) && numeric(state, origin) == numeric(state, target)) { return std::string(origin) + " 与 " + std::string(target) + " 不能相等"; } } return std::nullopt; } Json parse_request(std::string_view message) { return Json::parse(message.begin(), message.end()); } adminive::Table_View control_view(); adminive::Table_View action_view(); } // namespace renderive::web::gallery_detail namespace adminive { template <> struct Type_Descriptor { static auto get() { using T = renderive::web::gallery_detail::Case_Model; return object("renderive_gallery_case", ADMINIVE_FIELD_LABEL(T, id, "标识"), ADMINIVE_FIELD_LABEL(T, title, "标题"), ADMINIVE_FIELD_LABEL(T, component, "Core2 控件"), ADMINIVE_FIELD_LABEL(T, category, "分类"), ADMINIVE_FIELD_LABEL(T, description, "说明"), ADMINIVE_FIELD_LABEL(T, order, "顺序"), ADMINIVE_FIELD_LABEL(T, preferred_width, "建议宽度"), ADMINIVE_FIELD_LABEL(T, preferred_height, "建议高度")) .label("Renderive 控件用例"); } }; template <> struct Type_Descriptor { static auto get() { using T = renderive::web::gallery_detail::Control_Model; return object("renderive_gallery_control", ADMINIVE_FIELD_LABEL(T, id, "字段"), ADMINIVE_FIELD_LABEL(T, label, "控件"), ADMINIVE_FIELD_LABEL(T, api, "Core2 API"), ADMINIVE_FIELD_LABEL(T, description, "说明"), ADMINIVE_FIELD_LABEL(T, group, "分组"), ADMINIVE_FIELD_LABEL(T, input, "输入类型"), ADMINIVE_FIELD_LABEL(T, integer, "整数"), ADMINIVE_FIELD_LABEL(T, minimum, "最小值"), ADMINIVE_FIELD_LABEL(T, maximum, "最大值"), ADMINIVE_FIELD_LABEL(T, step, "步长")) .label("后端控制菜单字段"); } }; template <> struct Type_Descriptor { static auto get() { using T = renderive::web::gallery_detail::Action_Model; return object("renderive_gallery_action", ADMINIVE_FIELD_LABEL(T, id, "动作"), ADMINIVE_FIELD_LABEL(T, label, "名称"), ADMINIVE_FIELD_LABEL(T, api, "Core2 API"), ADMINIVE_FIELD_LABEL(T, description, "说明"), ADMINIVE_FIELD_LABEL(T, group, "分组"), ADMINIVE_FIELD_LABEL(T, argument_input, "参数类型"), ADMINIVE_FIELD_LABEL(T, argument_label, "参数名称"), ADMINIVE_FIELD_LABEL(T, argument_default, "参数默认值")) .label("后端动作菜单"); } }; } // namespace adminive namespace renderive::web::gallery_detail { adminive::Table_View control_view() { using T = Control_Model; return adminive::table_view( adminive::column<&T::label>("控件"), adminive::column<&T::group>("分组"), adminive::column<&T::api>("Core2 API"), adminive::column<&T::input>("输入类型")) .titled("后端控制菜单"); } adminive::Table_View action_view() { using T = Action_Model; return adminive::table_view( adminive::column<&T::label>("动作"), adminive::column<&T::group>("分组"), adminive::column<&T::api>("Core2 API")) .titled("保留 API 动作"); } } // namespace renderive::web::gallery_detail namespace renderive::web { namespace { using gallery_detail::Json; Json protocol_base() { return {{"category", "gallery"}, {"protocol", "renderive.control-gallery"}, {"protocol_version", 3}}; } Json case_contract(std::string_view case_id) { const auto* item = gallery_detail::find_case(case_id); return item ? adminive::to_frontend_json(*item) : Json::object(); } std::string_view frame_mode_id(Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: return "manual"; case Gallery_Frame_Mode::Low_Latency: return "low_latency"; case Gallery_Frame_Mode::Playback: return "playback"; } return "low_latency"; } std::optional parse_frame_mode(std::string_view value) { if (value == "manual") return Gallery_Frame_Mode::Manual; if (value == "low_latency") return Gallery_Frame_Mode::Low_Latency; if (value == "playback") return Gallery_Frame_Mode::Playback; return std::nullopt; } Json frame_mode_contract(Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: return {{"id", "manual"}, {"title", "手动刷新"}, {"strategy", "Manual_Refresh_Strategy"}, {"description", "显式准备、刷新和渲染;页面不会自动拉取像素。"}, {"automatic", false}, {"order", 10}}; case Gallery_Frame_Mode::Low_Latency: return {{"id", "low_latency"}, {"title", "低延迟"}, {"strategy", "Low_Latency_Strategy"}, {"description", "以最大 FPS 自动发布并消费最新帧,可观测丢帧与端到端延迟。"}, {"automatic", true}, {"order", 20}}; case Gallery_Frame_Mode::Playback: return {{"id", "playback"}, {"title", "回放队列"}, {"strategy", "Flow_Refresh_Strategy"}, {"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"}, {"automatic", true}, {"order", 30}}; } return Json::object(); } } // namespace std::string Gallery_Protocol::catalog_json() { Json result = protocol_base(); result["type"] = "catalog"; result["transport"] = {{"events", "websocket-text"}, {"pixels", "websocket-binary-rvp1"}, {"http_api", false}, {"socket_per_canvas", true}}; result["case_descriptor"] = adminive::to_descriptor_json(); result["frame_modes"] = Json::array(); constexpr Gallery_Frame_Mode frame_modes[]{Gallery_Frame_Mode::Manual, Gallery_Frame_Mode::Low_Latency, Gallery_Frame_Mode::Playback}; for (const auto mode : frame_modes) result["frame_modes"].push_back(frame_mode_contract(mode)); result["cases"] = Json::array(); std::size_t controls_total{}; std::size_t actions_total{}; for (const auto& item : gallery_detail::cases()) { Json entry = adminive::to_frontend_json(item); entry["control_count_by_mode"] = Json::object(); entry["action_count_by_mode"] = Json::object(); for (const auto mode : frame_modes) { const std::string key(frame_mode_id(mode)); const std::size_t mode_controls = control_count(item.id, mode); const std::size_t mode_actions = action_count(item.id, mode); entry["control_count_by_mode"][key] = mode_controls; entry["action_count_by_mode"][key] = mode_actions; controls_total += mode_controls; actions_total += mode_actions; } entry["control_count"] = control_count(item.id, Gallery_Frame_Mode::Low_Latency); entry["action_count"] = action_count(item.id, Gallery_Frame_Mode::Low_Latency); result["cases"].push_back(std::move(entry)); } result["coverage"] = {{"case_count", gallery_detail::cases().size()}, {"page_count", std::size(frame_modes)}, {"canvas_count", gallery_detail::cases().size() * std::size(frame_modes)}, {"manual_control_count", controls_total}, {"manual_action_count", actions_total}, {"frequency_modes", Json::array({"spectrum", "afterglow", "sweep_spectrum"})}, {"image_interpolation_modes", Json::array({"Nearest", "Bilinear", "Bicubic"})}}; return result.dump(); } bool Gallery_Protocol::is_case(std::string_view case_id) { return gallery_detail::find_case(case_id) != nullptr; } Gallery_State Gallery_Protocol::default_state(std::string_view case_id, Gallery_Frame_Mode frame_mode) { Gallery_State state; for (const auto& definition : gallery_detail::controls(case_id, frame_mode)) state.values.emplace(definition.model.id, definition.initial); return state; } Gallery_State Gallery_Protocol::default_state(std::string_view case_id) { return default_state(case_id, Gallery_Frame_Mode::Low_Latency); } std::string Gallery_Protocol::case_json(std::string_view case_id, const Gallery_State& state, std::string_view telemetry_json, std::string_view notice, Gallery_Frame_Mode frame_mode) { Json result = protocol_base(); result["type"] = "case_state"; result["case"] = case_contract(case_id); result["frame_mode"] = frame_mode_contract(frame_mode); result["controls"] = { {"descriptor", adminive::to_descriptor_json()}, {"view", adminive::to_view_json( gallery_detail::control_view())}, {"data", Json::array()} }; for (const auto& definition : gallery_detail::controls(case_id, frame_mode)) { Json item = adminive::to_frontend_json(definition.model); const auto found = state.values.find(definition.model.id); item["value"] = found == state.values.end() ? gallery_detail::value_json(definition.initial) : gallery_detail::value_json(found->second); item["options"] = definition.options; result["controls"]["data"].push_back(std::move(item)); } result["actions"] = { {"descriptor", adminive::to_descriptor_json()}, {"view", adminive::to_view_json( gallery_detail::action_view())}, {"data", Json::array()} }; for (const auto& definition : gallery_detail::actions(case_id, frame_mode)) result["actions"]["data"].push_back( adminive::to_frontend_json(definition.model)); try { result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); } catch (const std::exception&) { result["telemetry"] = Json::object(); } if (!notice.empty()) result["notice"] = notice; return result.dump(); } std::string Gallery_Protocol::case_json(std::string_view case_id, const Gallery_State& state, std::string_view telemetry_json, std::string_view notice) { return case_json(case_id, state, telemetry_json, notice, Gallery_Frame_Mode::Low_Latency); } std::string Gallery_Protocol::error_json(std::string_view message, std::string_view field) { Json result = protocol_base(); result["type"] = "error"; result["message"] = message; result["field_errors"] = Json::object(); if (!field.empty()) result["field_errors"][std::string(field)] = message; return result.dump(); } std::string Gallery_Protocol::observer_json(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view telemetry_json) { Json result = protocol_base(); result["type"] = "observer_state"; result["case_id"] = case_id; result["frame_mode"] = frame_mode_contract(frame_mode); try { result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); } catch (const std::exception&) { result["telemetry"] = Json::object(); } return result.dump(); } Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id, const Gallery_State& current, std::string_view message, Gallery_Frame_Mode frame_mode) { try { const Json request = gallery_detail::parse_request(message); if (!request.is_object() || request.value("category", "") != "event" || request.value("type", "") != "gallery_patch" || !request.contains("patch") || !request.at("patch").is_object()) { return {std::nullopt, error_json("gallery_patch 格式无效")}; } const auto definitions = gallery_detail::controls(case_id, frame_mode); Gallery_State candidate = current; for (const auto& [id, value] : request.at("patch").items()) { const auto* definition = gallery_detail::find_control(definitions, id); if (!definition) return {std::nullopt, error_json("该控件不属于当前画布", id)}; std::string validation_error; auto parsed = gallery_detail::parse_value(value, *definition, validation_error); if (!parsed) return {std::nullopt, error_json(validation_error, id)}; candidate.values[id] = std::move(*parsed); } if (const auto error = gallery_detail::range_error(candidate)) return {std::nullopt, error_json(*error)}; return {std::move(candidate), {}}; } catch (const std::exception& error) { return {std::nullopt, error_json(error.what())}; } } Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id, const Gallery_State& current, std::string_view message) { return apply_patch(case_id, current, message, Gallery_Frame_Mode::Low_Latency); } std::optional Gallery_Protocol::open_request(std::string_view message) { try { const Json request = gallery_detail::parse_request(message); if (!request.is_object() || request.value("category", "") != "event" || request.value("type", "") != "gallery_open" || !request.contains("case") || !request.at("case").is_string()) return std::nullopt; const std::string value = request.at("case").get(); if (!is_case(value) || !request.contains("frame_mode") || !request.at("frame_mode").is_string()) return std::nullopt; const auto mode = parse_frame_mode(request.at("frame_mode").get()); return mode ? std::optional(Gallery_Open_Request{value, *mode}) : std::nullopt; } catch (const std::exception&) { return std::nullopt; } } std::optional Gallery_Protocol::action_request( std::string_view message) { try { const Json request = gallery_detail::parse_request(message); if (!request.is_object() || request.value("category", "") != "event" || request.value("type", "") != "gallery_action" || !request.contains("action") || !request.at("action").is_string()) return std::nullopt; Gallery_Action_Request result{request.at("action").get()}; if (request.contains("argument")) { const Json& argument = request.at("argument"); if (argument.is_boolean()) result.argument = argument.get(); else if (argument.is_number()) { const double value = argument.get(); if (!std::isfinite(value)) return std::nullopt; result.argument = value; } else if (argument.is_string()) result.argument = argument.get(); else return std::nullopt; } return result; } catch (const std::exception&) { return std::nullopt; } } std::size_t Gallery_Protocol::control_count(std::string_view case_id, Gallery_Frame_Mode frame_mode) { return gallery_detail::controls(case_id, frame_mode).size(); } std::size_t Gallery_Protocol::control_count(std::string_view case_id) { return control_count(case_id, Gallery_Frame_Mode::Low_Latency); } std::size_t Gallery_Protocol::action_count(std::string_view case_id, Gallery_Frame_Mode frame_mode) { return gallery_detail::actions(case_id, frame_mode).size(); } std::size_t Gallery_Protocol::action_count(std::string_view case_id) { return action_count(case_id, Gallery_Frame_Mode::Low_Latency); } 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); return std::any_of(available.begin(), available.end(), [action_id](const gallery_detail::Action_Definition& item) { return item.model.id == action_id; }); } } // namespace renderive::web