995 lines
55 KiB
C++
995 lines
55 KiB
C++
#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(flag("frequency_limit_enabled", "启用频率上限",
|
||
"Plot_Core::set_max_render_fps / clear_max_render_fps", true,
|
||
"Low_Latency_Strategy", "关闭后用户频率不参与调度。"));
|
||
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,
|
||
"仅在启用频率上限时参与调度。"));
|
||
result.push_back(flag("consumer_feedback_enabled", "启用消费者反馈",
|
||
"Plot_Core::set_consumer_feedback / clear_consumer_feedback", true,
|
||
"Low_Latency_Strategy", "消费者反馈总开关;关闭后所有消费者来源都不参与调度。"));
|
||
result.push_back(flag("consumer_pixel_feedback_enabled", "启用像素响应反馈",
|
||
"Gallery_Plot_Session::set_client_metrics", false,
|
||
"Low_Latency_Strategy", "启用后按浏览器实际收到像素帧的 FPS 计算消费者周期。"));
|
||
result.push_back(flag("consumer_presentation_feedback_enabled", "启用浏览器呈现反馈",
|
||
"Gallery_Plot_Session::set_client_metrics", true,
|
||
"Low_Latency_Strategy", "启用后按浏览器 RAF 中位周期计算消费者周期。"));
|
||
result.push_back(flag("consumer_manual_feedback_enabled", "启用手动消费者反馈",
|
||
"Gallery_Plot_Session::apply_consumer_feedback", false,
|
||
"Low_Latency_Strategy", "启用后手动消费者 FPS 参与消费者限速。"));
|
||
result.push_back(number("consumer_manual_fps", "手动消费者 FPS",
|
||
"Gallery_Plot_Session::apply_consumer_feedback",
|
||
60, 0.01, 1'000'000'000, 1, "Low_Latency_Strategy", false,
|
||
"仅在启用手动消费者反馈时参与调度。"));
|
||
}
|
||
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
|