网页升级

This commit is contained in:
2026-08-11 06:21:51 +08:00
parent f50e9f7c1e
commit feb7c72e94
40 changed files with 6490 additions and 98 deletions
+979
View File
@@ -0,0 +1,979 @@
#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(number("pixel_stream_fps", "WebSocket 像素 FPS",
"WebSocket_Pixel_Stream::set_pull_frequency", 30, 1, 60, 1,
"Web Transport", false,
"只限制浏览器拉取完整 RGBA 像素图的频率,不限制 Kernel 后台渲染频率。"));
}
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