Files
Renderive/web_server/Gallery_Protocol.cpp
T
2026-08-11 09:41:06 +08:00

976 lines
54 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Gallery_Protocol.h"
#include "adminive/adminive.hpp"
#include "adminive/adapters/nlohmann_json.hpp"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <utility>
#include <vector>
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<std::string> options;
};
struct Action_Definition {
Action_Model model;
};
const std::vector<Case_Model>& cases() {
static const std::vector<Case_Model> 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<std::string> 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<Control_Definition>& 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<Control_Definition>& 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<Control_Definition>& 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<Control_Definition>& 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<Control_Definition> controls(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
std::vector<Control_Definition> 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<Action_Definition> actions(std::string_view case_id,
Gallery_Frame_Mode frame_mode) {
std::vector<Action_Definition> 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<Control_Definition>& 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<unsigned char>(character)) != 0;
});
}
Json value_json(const Gallery_Value& value) {
return std::visit([](const auto& item) { return Json(item); }, value);
}
std::optional<Gallery_Value> 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<bool>());
}
if (definition.model.input == "number") {
if (!value.is_number()) {
error = "必须是数字";
return std::nullopt;
}
const double number_value = value.get<double>();
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<std::string>();
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<double>(found->second);
}
std::optional<std::string> range_error(const Gallery_State& state) {
constexpr std::pair<std::string_view, std::string_view> 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<renderive::web::gallery_detail::Case_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Case_Model;
return object<T>("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<renderive::web::gallery_detail::Control_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Control_Model;
return object<T>("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<renderive::web::gallery_detail::Action_Model> {
static auto get() {
using T = renderive::web::gallery_detail::Action_Model;
return object<T>("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<T>(
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<T>(
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<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<Gallery_Frame_Mode> 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<Json, gallery_detail::Case_Model>();
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<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<Json, gallery_detail::Control_Model>()},
{"view", adminive::to_view_json<Json, gallery_detail::Control_Model>(
gallery_detail::control_view())},
{"data", Json::array()}
};
for (const auto& definition : gallery_detail::controls(case_id, frame_mode)) {
Json item = adminive::to_frontend_json<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<Json, gallery_detail::Action_Model>()},
{"view", adminive::to_view_json<Json, gallery_detail::Action_Model>(
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<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_Open_Request> 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<std::string>();
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<std::string>());
return mode ? std::optional<Gallery_Open_Request>(Gallery_Open_Request{value, *mode})
: std::nullopt;
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<Gallery_Action_Request> 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<std::string>()};
if (request.contains("argument")) {
const Json& argument = request.at("argument");
if (argument.is_boolean())
result.argument = argument.get<bool>();
else if (argument.is_number()) {
const double value = argument.get<double>();
if (!std::isfinite(value))
return std::nullopt;
result.argument = value;
} else if (argument.is_string())
result.argument = argument.get<std::string>();
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