From 75795e43bf86a56344ac437b8b316c4c2eff9e3b Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Wed, 12 Aug 2026 00:59:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=87=86=E5=A4=87=E6=94=B9adminive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render_2D/plottable/Plottable.h | 6 + web_server/app/Gallery_Controls.h | 74 ++ web_server/app/Gallery_Plot_Session.cpp | 758 ++++++------------ web_server/app/Gallery_Properties.h | 251 ------ web_server/app/Gallery_Properties_Adminive.h | 423 ---------- web_server/app/Gallery_Protocol.cpp | 221 ++--- web_server/app/Gallery_Protocol.h | 41 +- web_server/app/Gallery_Renderables.h | 129 +++ web_server/app/Gallery_Renderables_Adminive.h | 488 +++++++++++ web_server/app/Gallery_Session_Control.cpp | 47 ++ web_server/app/Gallery_Session_Control.h | 57 ++ .../app/Gallery_Session_Control_Adminive.h | 49 ++ web_server/tests/Web_Bridge_Tests.cpp | 382 ++------- webapp_gallery/app.js | 57 +- 14 files changed, 1285 insertions(+), 1698 deletions(-) create mode 100644 web_server/app/Gallery_Controls.h delete mode 100644 web_server/app/Gallery_Properties.h delete mode 100644 web_server/app/Gallery_Properties_Adminive.h create mode 100644 web_server/app/Gallery_Renderables.h create mode 100644 web_server/app/Gallery_Renderables_Adminive.h create mode 100644 web_server/app/Gallery_Session_Control.cpp create mode 100644 web_server/app/Gallery_Session_Control.h create mode 100644 web_server/app/Gallery_Session_Control_Adminive.h diff --git a/render_2D/plottable/Plottable.h b/render_2D/plottable/Plottable.h index 4e189a6..57e0847 100644 --- a/render_2D/plottable/Plottable.h +++ b/render_2D/plottable/Plottable.h @@ -71,6 +71,12 @@ protected: Properties properties() const { return Base::read([](const Properties& value) { return value; }); } + void replace_properties(Properties value) { + Base::update([&value](Properties& current) { + current = std::move(value); + }); + this->changed(); + } const Properties& render_properties(const Render_State_View& state) const noexcept { return state.get(static_cast(*this)); } diff --git a/web_server/app/Gallery_Controls.h b/web_server/app/Gallery_Controls.h new file mode 100644 index 0000000..becabf0 --- /dev/null +++ b/web_server/app/Gallery_Controls.h @@ -0,0 +1,74 @@ +#pragma once + +#include "Gallery_Renderables_Adminive.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace renderive::web { + +class Gallery_Controls final { +public: + using Json = nlohmann::json; + + template + void add(std::string id, std::string title, Object& object) { + using Model = adminive::Object_Model_Type; + auto* target = &object; + entries_.push_back({ + std::move(id), std::move(title), + [target] { + return Json{ + {"descriptor", adminive::to_descriptor_json()}, + {"view", adminive::to_view_json( + adminive::describe_edit_view())}, + {"data", adminive::to_frontend_json(*target)} + }; + }, + [target](const Json& patch) { + return adminive::apply_frontend_patch(*target, patch); + } + }); + } + + [[nodiscard]] Json resources() const { + Json result = Json::array(); + for (const auto& entry : entries_) { + Json resource = entry.resource(); + resource["target"] = entry.id; + resource["view"]["title"] = entry.title; + result.push_back(std::move(resource)); + } + return result; + } + + [[nodiscard]] adminive::Update_Result apply(std::string_view target, + const Json& patch) const { + for (const auto& entry : entries_) { + if (entry.id == target) + return entry.apply(patch); + } + adminive::Update_Result result; + result.success = false; + result.message = "unknown gallery control target: " + std::string(target); + return result; + } + +private: + struct Entry { + std::string id; + std::string title; + std::function resource; + std::function apply; + }; + + std::vector entries_; +}; + +} // namespace renderive::web diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index 856d101..2dac2e6 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -1,5 +1,7 @@ #include "Gallery_Plot_Session.h" +#include "Gallery_Controls.h" #include "Gallery_Protocol.h" +#include "Gallery_Session_Control_Adminive.h" #include "Pixel_Frame.h" #include "Web_Performance_Log.h" #include "render_2D/export.h" @@ -36,27 +38,12 @@ void update_axis_state(const std::shared_ptr& axis, Update&& update) { } } -double number_value(const Gallery_State& state, std::string_view id) { - return std::get(gallery_property_value(state, id)); -} std::uint64_t milliseconds_to_ns(double milliseconds) { return milliseconds > 0.0 ? static_cast(std::llround(milliseconds * 1'000'000.0)) : 0; } std::uint64_t frequency_to_ns(double frequency_hz) { return frequency_hz > 0.0 ? static_cast(std::llround(1'000'000'000.0 / frequency_hz)) : 0; } -int integer_value(const Gallery_State& state, std::string_view id) { - return static_cast(std::lround(number_value(state, id))); -} -bool bool_value(const Gallery_State& state, std::string_view id) { - return std::get(gallery_property_value(state, id)); -} -std::string string_value(const Gallery_State& state, std::string_view id) { - return std::get(gallery_property_value(state, id)); -} -bool has_value(const Gallery_State& state, std::string_view id) { - return gallery_has_property(state, id); -} int hex_digit(char value) { if (value >= '0' && value <= '9') return value - '0'; @@ -71,45 +58,6 @@ Color color_from_hex(std::string_view value, std::uint8_t alpha = 255) { }; return {channel(1), channel(3), channel(5), alpha}; } -Color blend(Color first, Color second, double amount) { - const auto channel = [amount](std::uint8_t a, std::uint8_t b) { - return static_cast(std::clamp( - std::lround(a + (b - a) * amount), 0L, 255L)); - }; - return { - channel(first.r, second.r), channel(first.g, second.g), - channel(first.b, second.b), channel(first.a, second.a) - }; -} -Color_Map gallery_color_map(std::string_view palette) { - std::array stops{ - Color{5, 10, 25, 255}, Color{20, 52, 112, 255}, Color{39, 196, 181, 255}, - Color{242, 201, 76, 255}, Color{255, 76, 106, 255} - }; - if (palette == "Ember") { - stops = { - Color{10, 5, 8, 255}, Color{62, 18, 30, 255}, Color{153, 47, 39, 255}, - Color{244, 132, 48, 255}, Color{255, 238, 166, 255} - }; - } - else if (palette == "Grayscale") { - stops = { - Color{0, 0, 0, 255}, Color{52, 52, 52, 255}, Color{112, 112, 112, 255}, - Color{188, 188, 188, 255}, Color{255, 255, 255, 255} - }; - } - std::vector colors; - colors.reserve(256); - for (int index = 0; index < 256; ++index) { - const double position = index / 255.0 * (stops.size() - 1); - const auto first = static_cast(std::floor(position)); - const auto second = std::min(first + 1, stops.size() - 1); - colors.push_back(premultiply(blend(stops[first], stops[second], position - first))); - } - Color_Map result; - result.set_colors(std::move(colors)); - return result; -} Time_Of_Day current_time_of_day() { constexpr std::int64_t day_ms = 24LL * 60LL * 60LL * 1000LL; const auto now = std::chrono::duration_cast( @@ -121,33 +69,6 @@ double gaussian(double x, double center, double width) { const double normalized = (x - center) / width; return std::exp(-0.5 * normalized * normalized); } -Line_Interpolation_Mode line_mode(std::string_view value) { - if (value == "Nearest_Sample") - return Line_Interpolation_Mode::Nearest_Sample; - if (value == "Linear_Power_Domain") - return Line_Interpolation_Mode::Linear_Power_Domain; - if (value == "Step_Left") - return Line_Interpolation_Mode::Step_Left; - if (value == "Step_Right") - return Line_Interpolation_Mode::Step_Right; - if (value == "Cubic_Value") - return Line_Interpolation_Mode::Cubic_Value; - return Line_Interpolation_Mode::Linear_Value; -} -Image_Interpolation_Mode image_mode(std::string_view value) { - if (value == "Bilinear") - return Image_Interpolation_Mode::Bilinear; - if (value == "Bicubic") - return Image_Interpolation_Mode::Bicubic; - return Image_Interpolation_Mode::Nearest; -} -Constellation_Diagram_Type constellation_mode(std::string_view value) { - if (value == "PSK4") - return Constellation_Diagram_Type::Psk4; - if (value == "PSK16") - return Constellation_Diagram_Type::Psk16; - return Constellation_Diagram_Type::Psk8; -} std::optional action_number(const Gallery_Action_Request& request) { if (!request.argument || !std::holds_alternative(*request.argument)) return std::nullopt; @@ -182,9 +103,9 @@ struct Gallery_Pixel_Copy { }; class Gallery_Scene final { public: - Gallery_Scene(std::uint64_t session_id, std::string case_id, Gallery_State state, + Gallery_Scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode frame_mode, bool automatic_low_latency) - : session_id_(session_id), case_id_(std::move(case_id)), state_(std::move(state)), + : session_id_(session_id), case_id_(std::move(case_id)), frame_mode_(frame_mode), automatic_low_latency_(automatic_low_latency), plot_(core_frame_mode(frame_mode)), performance_started_(std::chrono::steady_clock::now()) { plot_.init(); @@ -197,7 +118,13 @@ public: attach_performance_overlay(plot_); build_axes(); build_primary(); - apply_state(); + apply_layout(); + session_control_ = std::make_unique( + plot_, *primary_, feedback_policy_); + register_controls(); + set_performance_plot_name(plot_, case_id_ + "/" + std::string(frame_mode_name(frame_mode_))); + if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) + plot_.set_max_render_fps(30.0); plot_.activate_view(); update_model(); (void)plot_.render_frame(true); @@ -210,8 +137,8 @@ public: [[nodiscard]] const std::string& case_id() const noexcept { return case_id_; } - [[nodiscard]] const Gallery_State& state() const noexcept { - return state_; + [[nodiscard]] nlohmann::json controls() const { + return controls_.resources(); } [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept { return frame_mode_; @@ -252,32 +179,19 @@ public: client_last_pixel_change_age_ms_ = std::isfinite(last_pixel_change_age_ms) ? std::max(0.0, last_pixel_change_age_ms) : 0.0; apply_consumer_feedback(); } - [[nodiscard]] bool requires_rebuild(const Gallery_State& candidate) const { - const auto changed = [this, &candidate](std::string_view id) { - return gallery_has_property(state_, id) && gallery_has_property(candidate, id) && - gallery_property_value(state_, id) != gallery_property_value(candidate, id); - }; - if (case_id_ == "frequency_trace") - return changed("trace_pen") || changed("trace_pen_width"); - if (case_id_ == "constellation") - return changed("constellation_type") || changed("phase_offset"); - if (case_id_ == "waterfall" || case_id_ == "afterglow") - return changed("color_map"); - return false; - } - void set_state(Gallery_State state) { - const bool render_schedule_changed = frame_mode_ == Gallery_Frame_Mode::Low_Latency && - (bool_value(state_, "frequency_limit_enabled") != bool_value(state, "frequency_limit_enabled") || - number_value(state_, "max_render_fps") != number_value(state, "max_render_fps") || - bool_value(state_, "consumer_feedback_enabled") != bool_value(state, "consumer_feedback_enabled") || - bool_value(state_, "consumer_pixel_feedback_enabled") != bool_value(state, "consumer_pixel_feedback_enabled") || - bool_value(state_, "consumer_presentation_feedback_enabled") != bool_value(state, "consumer_presentation_feedback_enabled") || - bool_value(state_, "consumer_manual_feedback_enabled") != bool_value(state, "consumer_manual_feedback_enabled") || - number_value(state_, "consumer_manual_fps") != number_value(state, "consumer_manual_fps")); - state_ = std::move(state); - apply_state(); - if (render_schedule_changed) + [[nodiscard]] adminive::Update_Result apply_patch(std::string_view target, + const nlohmann::json& patch) { + auto result = controls_.apply(target, patch); + if (!result.success) + return result; + if (target == "scene") { + apply_consumer_feedback(); render_history_.clear(); + } + apply_layout(); + update_model(); + plot_.notify_model_dirty(); + return result; } void resize(Size size) { const Size previous = plot_.viewport_size(); @@ -295,10 +209,6 @@ public: plot_.deactivate_view(); plot_.dispatch_event(event); } - void remove_primary() { - if (primary_) - plot_.remove_renderable(primary_); - } bool render_latest_frame() { if (!plot_.view_active()) return false; @@ -391,25 +301,6 @@ public: plot_.activate_view(); return plot_.view_active() ? "Plot view 已激活" : "Plot view 已停用;可再次执行恢复"; } - if (request.id == "axis_set_start") { - if (numeric_domain_axis_) { - const Range range = numeric_domain_axis_->get<&Axis_Properties::coordinates>(); - const double origin = number_value(state_, "coord_origin"); - numeric_domain_axis_->set<&Axis_Properties::coordinates>( - Range{origin, origin + range.length()}); - } - return "轴起点已写入 Axis_Properties 状态"; - } - if (request.id == "axis_set_length") { - if (numeric_domain_axis_) { - const Range range = numeric_domain_axis_->get<&Axis_Properties::coordinates>(); - const double length = number_value(state_, "coord_target") - - number_value(state_, "coord_origin"); - numeric_domain_axis_->set<&Axis_Properties::coordinates>( - Range{range.origin, range.origin + length}); - } - return "轴跨度已写入 Axis_Properties 状态"; - } if (request.id == "axis_probe") return "坐标映射、刻度步长、次刻度数与标签结果已刷新到遥测区"; if (request.id == "append_time" && time_axis_) { @@ -608,24 +499,24 @@ public: { "consumer_feedback_master_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && - bool_value(state_, "consumer_feedback_enabled") + feedback_policy_.enabled }, { "consumer_pixel_feedback_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && - bool_value(state_, "consumer_pixel_feedback_enabled") + feedback_policy_.pixel }, { "consumer_presentation_feedback_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && - bool_value(state_, "consumer_presentation_feedback_enabled") + feedback_policy_.presentation }, { "consumer_manual_feedback_enabled", frame_mode_ == Gallery_Frame_Mode::Low_Latency && - bool_value(state_, "consumer_manual_feedback_enabled") + feedback_policy_.manual }, - {"consumer_manual_fps", frame_mode_ == Gallery_Frame_Mode::Low_Latency ? number_value(state_, "consumer_manual_fps") : 0.0}, + {"consumer_manual_fps", frame_mode_ == Gallery_Frame_Mode::Low_Latency ? feedback_policy_.manual_fps : 0.0}, {"consumer_feedback_source", consumer_feedback_source_}, {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, @@ -682,11 +573,11 @@ public: {"configured_frequency_hz", observer.frequency_hz}, {"frequency_limit_enabled", observer.frequency_limit_enabled}, {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, - {"consumer_feedback_master_enabled", bool_value(state_, "consumer_feedback_enabled")}, - {"consumer_pixel_feedback_enabled", bool_value(state_, "consumer_pixel_feedback_enabled")}, - {"consumer_presentation_feedback_enabled", bool_value(state_, "consumer_presentation_feedback_enabled")}, - {"consumer_manual_feedback_enabled", bool_value(state_, "consumer_manual_feedback_enabled")}, - {"consumer_manual_fps", number_value(state_, "consumer_manual_fps")}, + {"consumer_feedback_master_enabled", feedback_policy_.enabled}, + {"consumer_pixel_feedback_enabled", feedback_policy_.pixel}, + {"consumer_presentation_feedback_enabled", feedback_policy_.presentation}, + {"consumer_manual_feedback_enabled", feedback_policy_.manual}, + {"consumer_manual_fps", feedback_policy_.manual_fps}, {"consumer_feedback_source", consumer_feedback_source_}, {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, @@ -720,8 +611,8 @@ public: if (const auto snapshot = overlay->display_snapshot()) telemetry["performance_overlay_lines"] = snapshot->lines.size(); } - if (numeric_domain_axis_) { - const Axis_Transform transform = numeric_domain_axis_->transform(); + if (const auto axis = horizontal_axis()) { + const Axis_Transform transform = axis->transform(); const Range range = transform.coordinate_range; const double center = range.center(); telemetry["axis"] = { @@ -735,12 +626,11 @@ public: }, {"pixel_samples", static_cast(transform.pixel_length) + (transform.pixel_length > 0.0 ? 1 : 0)}, - {"tick_step", numeric_domain_axis_->tick_step(range)}, + {"tick_step", axis->tick_step(range)}, { - "sub_tick_count", numeric_domain_axis_->sub_tick_count( - numeric_domain_axis_->tick_step(range)) + "sub_tick_count", axis->sub_tick_count(axis->tick_step(range)) }, - {"center_label", numeric_domain_axis_->tick_label(center)} + {"center_label", axis->tick_label(center)} }; } if (spectrum_) { @@ -793,8 +683,8 @@ public: }; } if (constellation_) { - const std::string& constellation_type = string_value(state_, "constellation_type"); - const int anchor_count = constellation_type == "PSK4" ? 4 : constellation_type == "PSK16" ? 16 : 8; + const int anchor_count = static_cast( + constellation_->get<&Constellation_Diagram::Properties::type>()); telemetry["constellation"] = { {"point_count", constellation_->point_count()}, {"anchor_count", anchor_count}, @@ -807,8 +697,8 @@ public: } if (case_id_ == "axis_lab") { telemetry["axis_lab"] = { - {"domain_axis_pixel_samples", numeric_domain_axis_ - ? static_cast(numeric_domain_axis_->transform().pixel_length) + 1 + {"domain_axis_pixel_samples", horizontal_axis() + ? static_cast(horizontal_axis()->transform().pixel_length) + 1 : 0}, {"time_axis_point_count", time_axis_ ? time_axis_->time_point_count() : 0} }; @@ -844,8 +734,8 @@ public: {"unit", "points"} }; else if (constellation_) { - const std::string& constellation_type = string_value(state_, "constellation_type"); - const std::size_t anchors = constellation_type == "PSK4" ? 4U : constellation_type == "PSK16" ? 16U : 8U; + const std::size_t anchors = static_cast( + constellation_->get<&Constellation_Diagram::Properties::type>()); telemetry["data_shape"] = { {"input_points", constellation_->point_count()}, {"rendered_elements", constellation_->point_count() + anchors}, @@ -855,8 +745,8 @@ public: else telemetry["data_shape"] = { {"input_points", 0}, - {"rendered_elements", numeric_domain_axis_ - ? static_cast(numeric_domain_axis_->transform().pixel_length) + 1 + {"rendered_elements", horizontal_axis() + ? static_cast(horizontal_axis()->transform().pixel_length) + 1 : 0}, {"unit", "axis samples"} }; @@ -918,9 +808,9 @@ private: return; consumer_pixel_interval_ns_ = frequency_to_ns(client_transport_fps_); consumer_presentation_interval_ns_ = milliseconds_to_ns(client_display_interval_ms_); - consumer_manual_interval_ns_ = frequency_to_ns(number_value(state_, "consumer_manual_fps")); + consumer_manual_interval_ns_ = frequency_to_ns(feedback_policy_.manual_fps); consumer_feedback_source_ = "none"; - if (!bool_value(state_, "consumer_feedback_enabled")) { + if (!feedback_policy_.enabled) { plot_.clear_consumer_feedback(); consumer_feedback_source_ = "disabled"; return; @@ -941,11 +831,11 @@ private: consumer_feedback_source_ += source; } }; - select_source(bool_value(state_, "consumer_pixel_feedback_enabled"), + select_source(feedback_policy_.pixel, consumer_pixel_interval_ns_, "pixel"); - select_source(bool_value(state_, "consumer_presentation_feedback_enabled"), + select_source(feedback_policy_.presentation, consumer_presentation_interval_ns_, "presentation"); - select_source(bool_value(state_, "consumer_manual_feedback_enabled"), + select_source(feedback_policy_.manual, consumer_manual_interval_ns_, "manual"); if (interval_ns == 0) { plot_.clear_consumer_feedback(); @@ -1076,20 +966,21 @@ private: } void build_axes() { constexpr Color axis_color{118, 145, 184, 255}; - const Range coordinate_range{ - number_value(state_, "coord_origin"), - number_value(state_, "coord_target") - }; + Range coordinate_range{88'000'000.0, 108'000'000.0}; + if (case_id_ == "sweep_spectrum") + coordinate_range = {0.0, 300.0}; + else if (case_id_ == "constellation") + coordinate_range = {-1.25, 1.25}; if (case_id_ == "frequency_trace") { - time_axis_ = Time_Axis::Builder(axes_node_, Orientation::Horizontal) - .set_visible_time_point_count(integer_value(state_, "time_visible_count")) - .set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")) - .set_time_format(string_value(state_, "time_format")) + time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Horizontal) + .set_visible_time_point_count(80) + .set_tick_label_spacing_px(28) + .set_time_format("mm:ss.zzz") .set_color(axis_color) .build(); } else if (case_id_ == "sweep_spectrum" || case_id_ == "constellation") { - numeric_domain_axis_ = Axis::Builder(axes_node_, Orientation::Horizontal) + numeric_domain_axis_ = Gallery_Axis::Builder(axes_node_, Orientation::Horizontal) .set_coord_range(coordinate_range) .set_use_wheel(true) .set_use_drag(true) @@ -1097,7 +988,7 @@ private: .build(); } else { - numeric_domain_axis_ = Frequency_Axis::Builder(axes_node_, Orientation::Horizontal) + frequency_domain_axis_ = Gallery_Frequency_Axis::Builder(axes_node_, Orientation::Horizontal) .set_coord_range(coordinate_range) .set_use_wheel(true) .set_use_drag(true) @@ -1105,141 +996,153 @@ private: .build(); } if (case_id_ == "waterfall") { - time_axis_ = Time_Axis::Builder(axes_node_, Orientation::Vertical) - .set_visible_time_point_count(integer_value(state_, "time_visible_count")) - .set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")) - .set_time_format(string_value(state_, "time_format")) + time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Vertical) + .set_visible_time_point_count(80) + .set_tick_label_spacing_px(28) + .set_time_format("mm:ss.zzz") .set_color(axis_color) .build(); } else { Range value_range{-120.0, -20.0}; if (case_id_ == "frequency_trace") - value_range = { - number_value(state_, "trace_value_min"), - number_value(state_, "trace_value_max") - }; + value_range = {-1.0, 1.0}; else if (case_id_ == "constellation") - value_range = { - number_value(state_, "q_origin"), - number_value(state_, "q_target") - }; - value_axis_ = Axis::Builder(axes_node_, Orientation::Vertical) + value_range = {-1.25, 1.25}; + value_axis_ = Gallery_Axis::Builder(axes_node_, Orientation::Vertical) .set_coord_range(value_range) .set_label_precision(case_id_ == "constellation" ? 2 : 0) .set_color(axis_color) .build(); } if (case_id_ == "axis_lab") { - time_axis_ = Time_Axis::Builder(axes_node_, Orientation::Horizontal) - .set_visible_time_point_count(integer_value(state_, "time_visible_count")) - .set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")) - .set_time_format(string_value(state_, "time_format")) + time_axis_ = Gallery_Time_Axis::Builder(axes_node_, Orientation::Horizontal) + .set_visible_time_point_count(80) + .set_tick_label_spacing_px(28) + .set_time_format("mm:ss.zzz") .set_color({69, 221, 190, 255}) .build(); } + const auto style = [this](const std::shared_ptr& axis) { + if (!axis) + return; + update_axis_state(axis, [this](Axis_Base_Properties& value) { + value.tick_length = 8; + value.sub_tick_length = 4; + value.unit_text = case_id_ == "sweep_spectrum" ? "MHz" + : case_id_ == "constellation" ? "I / Q" + : case_id_ == "frequency_trace" ? "value" : "Hz"; + value.unit_text_font = {12.0, 500, false}; + value.unit_text_pen = {Color{220, 233, 255, 255}}; + value.unit_text_background_brush = { + Color{7, 17, 31, 255}, Brush_Style::Solid}; + }); + }; + style(horizontal_axis()); + style(vertical_axis()); + if (case_id_ == "axis_lab") + style(time_axis_); } void build_primary() { if (case_id_ == "axis_lab") { - primary_ = numeric_domain_axis_; + primary_ = std::static_pointer_cast(frequency_domain_axis_); return; } if (case_id_ == "spectrum" || case_id_ == "selection_overlay") { - spectrum_ = Spectrum::Builder{} - .set<&Spectrum::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }) - .set<&Spectrum::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")) - .set<&Spectrum::Properties::center_frequency>(number_value(state_, "center_frequency")) - .set<&Spectrum::Properties::sweep_frequency_range>(Range{ - number_value(state_, "sweep_origin"), - number_value(state_, "sweep_target") - }) - .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), value_axis_); + Spectrum_Properties properties; + properties.frequency_range = {88'000'000.0, 108'000'000.0}; + properties.frequency_point_size = 512; + properties.center_frequency = 98'000'000.0; + properties.sweep_frequency_range = {94'000'000.0, 102'000'000.0}; + properties.max_hold_visible = true; + properties.max_marker_visible = true; + properties.sweep_region_visible = true; + properties.max_brush = {color_from_hex("#332810", 76), Brush_Style::Solid}; + properties.current_brush = {color_from_hex("#0e3a35", 76), Brush_Style::Solid}; + properties.min_brush = {color_from_hex("#19213a", 76), Brush_Style::Solid}; + properties.max_pen = {color_from_hex("#ffd166"), 1.2}; + properties.current_pen = {color_from_hex("#35e6b2"), 2.0, + Line_Style::Solid, Line_Cap::Round, Line_Join::Round}; + properties.min_pen = {color_from_hex("#7aa2ff"), 1.2}; + properties.selected_marker_pen = {color_from_hex("#ff4d8d"), 2.0}; + properties.marker_pen = {color_from_hex("#ff7a59"), 1.2}; + properties.middle_frequency_pen = {color_from_hex("#6bc8ff"), 1.0, Line_Style::Dash}; + properties.sweep_region_brush = {color_from_hex("#293257", 80), Brush_Style::Solid}; + spectrum_ = Gallery_Spectrum::Builder{properties}.build( + data_node_, frequency_domain_axis_, value_axis_); primary_ = spectrum_; if (case_id_ == "selection_overlay") { - selection_ = Selection_Rectangle_Overlay::Builder{}.build(overlay_node_, numeric_domain_axis_, value_axis_); + Selection_Rectangle_Overlay_Properties overlay; + overlay.label_font = {12.0, 500, false}; + overlay.selection_brush = {color_from_hex("#173b66", 70), Brush_Style::Solid}; + overlay.selection_border_pen = {color_from_hex("#7dd3fc"), 1.0, Line_Style::Dash}; + selection_ = Gallery_Selection_Rectangle_Overlay::Builder{overlay}.build( + overlay_node_, frequency_domain_axis_, value_axis_); primary_ = selection_; } return; } if (case_id_ == "waterfall") { - waterfall_ = Waterfall::Builder{} - .set<&Waterfall::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }) - .set<&Waterfall::Properties::power_range>(Range{ - number_value(state_, "power_origin"), - number_value(state_, "power_target") - }) - .set<&Waterfall::Properties::frequency_bin_count>(integer_value(state_, "frequency_bin_count")) - .set<&Waterfall::Properties::color_map>(gallery_color_map(string_value(state_, "color_map"))) - .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), time_axis_); + Waterfall_Properties properties; + properties.frequency_range = {88'000'000.0, 108'000'000.0}; + properties.power_range = {-120.0, -20.0}; + properties.frequency_bin_count = 256; + waterfall_ = Gallery_Waterfall::Builder{properties}.build( + data_node_, frequency_domain_axis_, time_axis_); primary_ = waterfall_; return; } if (case_id_ == "afterglow") { - afterglow_ = Afterglow::Builder{} - .set<&Afterglow::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }) - .set<&Afterglow::Properties::power_range>(Range{ - number_value(state_, "power_origin"), - number_value(state_, "power_target") - }) - .set<&Afterglow::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")) - .set<&Afterglow::Properties::power_point_size>(integer_value(state_, "power_point_size")) - .set<&Afterglow::Properties::color_map>(gallery_color_map(string_value(state_, "color_map"))) - .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), value_axis_); + Afterglow_Properties properties; + properties.frequency_range = {88'000'000.0, 108'000'000.0}; + properties.power_range = {-120.0, -20.0}; + properties.frequency_point_size = 192; + properties.power_point_size = 96; + properties.attenuation_rate = 0.18; + afterglow_ = Gallery_Afterglow::Builder{properties}.build( + data_node_, frequency_domain_axis_, value_axis_); primary_ = afterglow_; return; } if (case_id_ == "sweep_spectrum") { - sweep_ = Sweep_Spectrum::Builder{} - .set<&Sweep_Spectrum::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }) - .set<&Sweep_Spectrum::Properties::bins_per_block>(integer_value(state_, "bins_per_block")) - .set<&Sweep_Spectrum::Properties::block_count>(integer_value(state_, "block_count")) - .build(data_node_, numeric_domain_axis_, value_axis_); + Sweep_Spectrum_Properties properties; + properties.frequency_range = {0.0, 300.0}; + properties.bins_per_block = 64; + properties.block_count = 8; + properties.pen = {color_from_hex("#ffd166"), 1.8}; + properties.current_frequency_pen = {color_from_hex("#ff5d73"), 2.0}; + sweep_ = Gallery_Sweep_Spectrum::Builder{properties}.build( + data_node_, numeric_domain_axis_, value_axis_); primary_ = sweep_; return; } if (case_id_ == "frequency_trace") { - trace_ = Frequency_Trace::Builder{} - .set<&Frequency_Trace::Properties::pen>(Pen{ - color_from_hex(string_value(state_, "trace_pen")), - number_value(state_, "trace_pen_width"), Line_Style::Solid, - Line_Cap::Round, Line_Join::Round - }) - .build(data_node_, time_axis_, value_axis_); + Frequency_Trace_Properties properties; + properties.pen = {color_from_hex("#35e6b2"), 2.0, Line_Style::Solid, + Line_Cap::Round, Line_Join::Round}; + trace_ = Gallery_Frequency_Trace::Builder{properties}.build( + data_node_, time_axis_, value_axis_); primary_ = trace_; return; } if (case_id_ == "constellation") { - constellation_ = Constellation_Diagram::Builder{} - .set<&Constellation_Diagram::Properties::i_range>(Range{ - number_value(state_, "i_origin"), - number_value(state_, "i_target") - }) - .set<&Constellation_Diagram::Properties::q_range>(Range{ - number_value(state_, "q_origin"), - number_value(state_, "q_target") - }) - .set<&Constellation_Diagram::Properties::type>(constellation_mode(string_value(state_, "constellation_type"))) - .set<&Constellation_Diagram::Properties::phase_offset_radians>(number_value(state_, "phase_offset")) - .build(data_node_, numeric_domain_axis_, value_axis_); + Constellation_Diagram_Properties properties; + properties.i_range = {-1.25, 1.25}; + properties.q_range = {-1.25, 1.25}; + properties.point_color = color_from_hex("#49e6c3"); + properties.anchor_color = color_from_hex("#ffd166"); + properties.point_lifetime_ms = 1800; + constellation_ = Gallery_Constellation_Diagram::Builder{properties}.build( + data_node_, numeric_domain_axis_, value_axis_); primary_ = constellation_; } } std::shared_ptr horizontal_axis() const { - return numeric_domain_axis_ - ? std::static_pointer_cast(numeric_domain_axis_) - : std::static_pointer_cast(time_axis_); + if (frequency_domain_axis_) + return frequency_domain_axis_; + if (numeric_domain_axis_) + return numeric_domain_axis_; + return time_axis_; } std::shared_ptr vertical_axis() const { if (case_id_ == "waterfall") @@ -1254,23 +1157,18 @@ private: const int bottom = 46; const int width = std::max(1, viewport.width - left - right); const int height = std::max(1, viewport.height - top - bottom); - const int x_offset = integer_value(state_, "axis_x_offset"); - const int y_offset = integer_value(state_, "axis_y_offset"); - const double length_rate = number_value(state_, "axis_length_percent") / 100.0; if (const auto horizontal = horizontal_axis()) { - update_axis_state(horizontal, [&](Axis_Base_Properties& axis) { - axis.x = left + x_offset; - axis.y = top + height + y_offset; - axis.pixel_length = static_cast( - std::max(1, static_cast(std::lround(width * length_rate)))); + update_axis_state(horizontal, [=](Axis_Base_Properties& axis) { + axis.x = left; + axis.y = top + height; + axis.pixel_length = static_cast(width); }); } if (const auto vertical = vertical_axis()) { - update_axis_state(vertical, [&](Axis_Base_Properties& axis) { - axis.x = left + x_offset; - axis.y = top + y_offset; - axis.pixel_length = static_cast( - std::max(1, static_cast(std::lround(height * length_rate)))); + update_axis_state(vertical, [=](Axis_Base_Properties& axis) { + axis.x = left; + axis.y = top; + axis.pixel_length = static_cast(height); }); } if (case_id_ == "axis_lab" && time_axis_) { @@ -1281,206 +1179,30 @@ private: }); } } - void apply_axis_style(const std::shared_ptr& axis) { - if (!axis) - return; - update_axis_state(axis, [&](Axis_Base_Properties& value) { - value.tick_length = integer_value(state_, "tick_length"); - value.sub_tick_length = integer_value(state_, "sub_tick_length"); - value.color = color_from_hex(string_value(state_, "axis_color")); - value.locale = {string_value(state_, "decimal_separator").front()}; - value.unit_text = string_value(state_, "unit_text"); - value.unit_text_font = { - number_value(state_, "unit_font_size"), - integer_value(state_, "unit_font_weight"), - bool_value(state_, "unit_font_italic") - }; - value.unit_text_pen = {color_from_hex(string_value(state_, "unit_pen_color"))}; - value.unit_text_background_brush = { - color_from_hex(string_value(state_, "unit_background_color")), - Brush_Style::Solid - }; - value.label_rotation_degrees = integer_value(state_, "label_rotation"); - }); - } - void apply_axes() { - const bool horizontal = string_value(state_, "axis_orientation") == "Horizontal"; - if (const auto main = horizontal_axis()) - update_axis_state(main, [horizontal](Axis_Base_Properties& axis) { - axis.orientation = horizontal ? Orientation::Horizontal : Orientation::Vertical; - }); - if (const auto other = vertical_axis()) - update_axis_state(other, [horizontal](Axis_Base_Properties& axis) { - axis.orientation = horizontal ? Orientation::Vertical : Orientation::Horizontal; - }); - apply_axis_style(horizontal_axis()); - apply_axis_style(vertical_axis()); - if (case_id_ == "axis_lab") - apply_axis_style(time_axis_); - if (numeric_domain_axis_) { - numeric_domain_axis_->update([&](Axis_Properties& axis) { - axis.coordinates = { - number_value(state_, "coord_origin"), - number_value(state_, "coord_target") - }; - axis.precision = std::clamp(integer_value(state_, "label_precision"), 0, 12); - axis.wheel = bool_value(state_, "use_wheel"); - axis.drag = bool_value(state_, "use_drag"); - }); - } - if (time_axis_) { - time_axis_->update([&](auto& axis) { - axis.visible_count = std::max(2, integer_value(state_, "time_visible_count")); - axis.tick_label_spacing_px = std::max(0, integer_value(state_, "time_label_spacing")); - axis.format = string_value(state_, "time_format"); - axis.unit_text_font = {number_value(state_, "time_font_size"), 500, false}; - axis.newest_at_start = bool_value(state_, "time_newest_at_start"); - }); - } - apply_layout(); - } - void apply_state() { - plot_.set_background_color(color_from_hex(string_value(state_, "background_color"))); - if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { - if (bool_value(state_, "frequency_limit_enabled")) - plot_.set_max_render_fps(number_value(state_, "max_render_fps")); - else - plot_.clear_max_render_fps(); - apply_consumer_feedback(); - } - set_performance_plot_name(plot_, case_id_ + "/" + std::string(frame_mode_name(frame_mode_))); - set_performance_overlay_enabled(plot_, bool_value(state_, "performance_overlay")); - if (primary_) { - primary_->set_visible(bool_value(state_, "renderable_visible")); - primary_->set_cache_mode(string_value(state_, "cache_mode") == "Direct" - ? Renderable_Cache_Mode::Direct - : Renderable_Cache_Mode::Local_Pixel); - primary_->set_object_name(string_value(state_, "object_name")); - } - apply_axes(); - apply_specific_state(); - plot_.notify_model_dirty(); - } - void apply_specific_state() { - if (spectrum_) { - spectrum_->set<&Spectrum::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")); - spectrum_->set<&Spectrum::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }); - spectrum_->set<&Spectrum::Properties::center_frequency>(number_value(state_, "center_frequency")); - spectrum_->set<&Spectrum::Properties::sweep_frequency_range>(Range{ - number_value(state_, "sweep_origin"), - number_value(state_, "sweep_target") - }); - spectrum_->set<&Spectrum::Properties::max_hold_visible>(bool_value(state_, "max_hold_visible")); - spectrum_->set<&Spectrum::Properties::min_hold_visible>(bool_value(state_, "min_hold_visible")); - spectrum_->set<&Spectrum::Properties::max_marker_visible>(bool_value(state_, "max_marker_visible")); - spectrum_->set<&Spectrum::Properties::use_min_marker>(bool_value(state_, "use_min_marker")); - spectrum_->set<&Spectrum::Properties::sweep_region_visible>(bool_value(state_, "sweep_region_visible")); - spectrum_->set<&Spectrum::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); - spectrum_->set<&Spectrum::Properties::interpolation_mode>(line_mode(string_value(state_, "line_interpolation"))); - spectrum_->set<&Spectrum::Properties::max_brush>(Brush{color_from_hex(string_value(state_, "max_brush"), 76), Brush_Style::Solid}); - spectrum_->set<&Spectrum::Properties::current_brush>(Brush{color_from_hex(string_value(state_, "current_brush"), 76), Brush_Style::Solid}); - spectrum_->set<&Spectrum::Properties::min_brush>(Brush{color_from_hex(string_value(state_, "min_brush"), 76), Brush_Style::Solid}); - spectrum_->set<&Spectrum::Properties::max_pen>(Pen{color_from_hex(string_value(state_, "max_pen")), 1.2}); - spectrum_->set<&Spectrum::Properties::current_pen>(Pen{ - color_from_hex(string_value(state_, "current_pen")), 2.0, - Line_Style::Solid, Line_Cap::Round, Line_Join::Round - }); - spectrum_->set<&Spectrum::Properties::min_pen>(Pen{color_from_hex(string_value(state_, "min_pen")), 1.2}); - spectrum_->set<&Spectrum::Properties::selected_marker_pen>(Pen{color_from_hex(string_value(state_, "selected_marker_pen")), 2.0}); - spectrum_->set<&Spectrum::Properties::marker_pen>(Pen{color_from_hex(string_value(state_, "marker_pen")), 1.2}); - spectrum_->set<&Spectrum::Properties::middle_frequency_pen>(Pen{ - color_from_hex(string_value(state_, "middle_frequency_pen")), - 1.0, Line_Style::Dash - }); - spectrum_->set<&Spectrum::Properties::sweep_region_brush>(Brush{ - color_from_hex(string_value(state_, "sweep_region_brush"), 80), - Brush_Style::Solid - }); - spectrum_->set<&Spectrum::Properties::tooltip_enabled>(bool_value(state_, "hover_enabled")); - spectrum_->set<&Spectrum::Properties::tooltip_font>(Font{number_value(state_, "hover_font_size"), 500, false}); - spectrum_->set<&Spectrum::Properties::tooltip_text_pen>(Pen{color_from_hex(string_value(state_, "hover_text_color"))}); - spectrum_->set<&Spectrum::Properties::tooltip_background_brush>(Brush{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); - } - if (selection_) { - selection_->set<&Selection_Rectangle_Overlay::Properties::label_font>(Font{number_value(state_, "selection_font_size"), 500, false}); - selection_->set<&Selection_Rectangle_Overlay::Properties::label_pen>(Pen{color_from_hex(string_value(state_, "selection_label_pen"))}); - selection_->set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{ - color_from_hex(string_value(state_, "selection_brush"), 70), - Brush_Style::Solid - }); - selection_->set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{ - color_from_hex(string_value(state_, "selection_border")), - 1.0, Line_Style::Dash - }); - } - if (waterfall_) { - waterfall_->set<&Waterfall::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }); - waterfall_->set<&Waterfall::Properties::power_range>(Range{ - number_value(state_, "power_origin"), - number_value(state_, "power_target") - }); - waterfall_->set<&Waterfall::Properties::frequency_bin_count>(integer_value(state_, "frequency_bin_count")); - waterfall_->set<&Waterfall::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); - waterfall_->set<&Waterfall::Properties::interpolation_mode>(image_mode(string_value(state_, "image_interpolation"))); - waterfall_->set<&Waterfall::Properties::tooltip_enabled>(bool_value(state_, "hover_enabled")); - waterfall_->set<&Waterfall::Properties::tooltip_font>(Font{number_value(state_, "hover_font_size"), 500, false}); - waterfall_->set<&Waterfall::Properties::tooltip_text_pen>(Pen{color_from_hex(string_value(state_, "hover_text_color"))}); - waterfall_->set<&Waterfall::Properties::tooltip_background_brush>(Brush{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); - } - if (afterglow_) { - afterglow_->set<&Afterglow::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }); - afterglow_->set<&Afterglow::Properties::power_range>(Range{ - number_value(state_, "power_origin"), - number_value(state_, "power_target") - }); - afterglow_->set<&Afterglow::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")); - afterglow_->set<&Afterglow::Properties::power_point_size>(integer_value(state_, "power_point_size")); - afterglow_->set<&Afterglow::Properties::interpolate>(bool_value(state_, "interpolate_power")); - afterglow_->set<&Afterglow::Properties::attenuation_rate>(number_value(state_, "attenuation_rate")); - } - if (sweep_) { - sweep_->set<&Sweep_Spectrum::Properties::frequency_range>(Range{ - number_value(state_, "frequency_origin"), - number_value(state_, "frequency_target") - }); - sweep_->set<&Sweep_Spectrum::Properties::bins_per_block>(integer_value(state_, "bins_per_block")); - sweep_->set<&Sweep_Spectrum::Properties::block_count>(integer_value(state_, "block_count")); - sweep_->set<&Sweep_Spectrum::Properties::pen>(Pen{color_from_hex(string_value(state_, "sweep_pen")), 1.8}); - sweep_->set<&Sweep_Spectrum::Properties::current_frequency_pen>(Pen{color_from_hex(string_value(state_, "current_frequency_pen")), 2.0}); - sweep_->set<&Sweep_Spectrum::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); - sweep_->set<&Sweep_Spectrum::Properties::interpolation_mode>(line_mode(string_value(state_, "line_interpolation"))); - } - if (trace_ && value_axis_) - value_axis_->set<&Axis_Properties::coordinates>(Range{ - number_value(state_, "trace_value_min"), - number_value(state_, "trace_value_max") - }); - if (constellation_) { - constellation_->set<&Constellation_Diagram::Properties::i_range>(Range{number_value(state_, "i_origin"), number_value(state_, "i_target")}); - constellation_->set<&Constellation_Diagram::Properties::q_range>(Range{number_value(state_, "q_origin"), number_value(state_, "q_target")}); - constellation_->set<&Constellation_Diagram::Properties::point_color>(color_from_hex(string_value(state_, "point_color"))); - constellation_->set<&Constellation_Diagram::Properties::anchor_color>(color_from_hex(string_value(state_, "anchor_color"))); - constellation_->set<&Constellation_Diagram::Properties::point_lifetime_ms>(integer_value(state_, "point_lifetime_ms")); - if (numeric_domain_axis_) - numeric_domain_axis_->set<&Axis_Properties::coordinates>(Range{ - number_value(state_, "i_origin"), - number_value(state_, "i_target") - }); - if (value_axis_) - value_axis_->set<&Axis_Properties::coordinates>(Range{ - number_value(state_, "q_origin"), - number_value(state_, "q_target") - }); - } + void register_controls() { + controls_.add("scene", "Scene", *session_control_); + if (frequency_domain_axis_) + controls_.add("frequency_axis", "Frequency axis", *frequency_domain_axis_); + if (numeric_domain_axis_) + controls_.add("domain_axis", "Domain axis", *numeric_domain_axis_); + if (value_axis_) + controls_.add("value_axis", "Value axis", *value_axis_); + if (time_axis_) + controls_.add("time_axis", "Time axis", *time_axis_); + if (spectrum_) + controls_.add("spectrum", "Spectrum", *spectrum_); + if (waterfall_) + controls_.add("waterfall", "Waterfall", *waterfall_); + if (afterglow_) + controls_.add("afterglow", "Afterglow", *afterglow_); + if (sweep_) + controls_.add("sweep_spectrum", "Sweep spectrum", *sweep_); + if (trace_) + controls_.add("frequency_trace", "Frequency trace", *trace_); + if (selection_) + controls_.add("selection_overlay", "Selection overlay", *selection_); + if (constellation_) + controls_.add("constellation", "Constellation", *constellation_); } std::vector spectrum_values(int count, Range power_range = {-120.0, -20.0}) const { count = std::max(count, 2); @@ -1521,16 +1243,18 @@ private: sweep_->append_block(block); } void push_trace_sample() { - const double lower = number_value(state_, "trace_value_min"); - const double upper = number_value(state_, "trace_value_max"); + const Range range = value_axis_->get<&Axis_Properties::coordinates>(); + const double lower = range.origin; + const double upper = range.target; const double center = (lower + upper) * 0.5; const double amplitude = std::abs(upper - lower) * 0.42; trace_->append_sample(current_time_of_day(), center + amplitude * std::sin(frame_index_ * 0.12)); } void push_constellation_points() { - const std::string& mode = string_value(state_, "constellation_type"); - const int count = mode == "PSK4" ? 4 : mode == "PSK16" ? 16 : 8; - const double phase = number_value(state_, "phase_offset"); + const int count = static_cast( + constellation_->get<&Constellation_Diagram::Properties::type>()); + const double phase = constellation_->get< + &Constellation_Diagram::Properties::phase_offset_radians>(); for (int index = 0; index < 24; ++index) { const int anchor = static_cast((frame_index_ * 7 + index * 5) % count); const double angle = phase + 2.0 * std::numbers::pi * anchor / count; @@ -1564,27 +1288,30 @@ private: } std::uint64_t session_id_{}; std::string case_id_; - Gallery_State state_; Gallery_Frame_Mode frame_mode_ = Gallery_Frame_Mode::Low_Latency; bool automatic_low_latency_{}; Plot_Core plot_; + Gallery_Feedback_Policy feedback_policy_; + std::unique_ptr session_control_; + Gallery_Controls controls_; std::chrono::steady_clock::time_point performance_started_; std::chrono::steady_clock::time_point last_performance_log_; std::shared_ptr root_; std::shared_ptr axes_node_; std::shared_ptr data_node_; std::shared_ptr overlay_node_; - std::shared_ptr numeric_domain_axis_; - std::shared_ptr value_axis_; - std::shared_ptr time_axis_; + std::shared_ptr numeric_domain_axis_; + std::shared_ptr frequency_domain_axis_; + std::shared_ptr value_axis_; + std::shared_ptr time_axis_; std::shared_ptr primary_; - std::shared_ptr spectrum_; - std::shared_ptr waterfall_; - std::shared_ptr trace_; - std::shared_ptr afterglow_; - std::shared_ptr sweep_; - std::shared_ptr selection_; - std::shared_ptr constellation_; + std::shared_ptr spectrum_; + std::shared_ptr waterfall_; + std::shared_ptr trace_; + std::shared_ptr afterglow_; + std::shared_ptr sweep_; + std::shared_ptr selection_; + std::shared_ptr constellation_; std::uint64_t frame_index_{}; std::uint64_t render_attempt_count_{}; std::uint64_t successful_render_count_{}; @@ -1774,15 +1501,13 @@ struct Gallery_Plot_Session::Impl { Gallery_Protocol::error_json("未知的 gallery case 或 frame_mode", "case") }; scene = std::make_unique( - session_id, open->case_id, - Gallery_Protocol::default_state(open->case_id, open->frame_mode), - open->frame_mode, automatic_low_latency); + session_id, open->case_id, open->frame_mode, automatic_low_latency); ++scene_generation; if (scene->frame_mode() == Gallery_Frame_Mode::Low_Latency) ensure_render_worker(); return Web_Response{ Web_Response_Type::Json, - Gallery_Protocol::case_json(scene->case_id(), scene->state(), + Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(), scene->telemetry_json(), "render_2D/Kernel 独立画布已创建", scene->frame_mode()) @@ -1805,25 +1530,18 @@ struct Gallery_Plot_Session::Impl { }; } if (request.kind == Gallery_Request_Kind::Patch) { - auto patch = Gallery_Protocol::apply_patch(scene->case_id(), scene->state(), - request.message, scene->frame_mode()); - if (!patch.candidate) - return Web_Response{Web_Response_Type::Json, std::move(patch.response_json)}; - if (scene->requires_rebuild(*patch.candidate)) { - const std::string id = scene->case_id(); - const auto mode = scene->frame_mode(); - scene->remove_primary(); - scene = std::make_unique(session_id, id, - std::move(*patch.candidate), mode, - automatic_low_latency); - ++scene_generation; - } - else { - scene->set_state(std::move(*patch.candidate)); - } + const auto patch = Gallery_Protocol::control_patch_request(request.message); + if (!patch) + return Web_Response{Web_Response_Type::Json, + Gallery_Protocol::error_json("gallery_patch format is invalid")}; + const auto result = scene->apply_patch( + patch->target, nlohmann::json::parse(patch->patch_json)); + if (!result.success) + return Web_Response{Web_Response_Type::Json, + Gallery_Protocol::error_json(result.message)}; return Web_Response{ Web_Response_Type::Json, - Gallery_Protocol::case_json(scene->case_id(), scene->state(), + Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(), scene->telemetry_json(), "控件 API 已由后端应用", scene->frame_mode()) @@ -1846,31 +1564,15 @@ struct Gallery_Plot_Session::Impl { const std::string id = scene->case_id(); const auto mode = scene->frame_mode(); scene = std::make_unique( - session_id, id, Gallery_Protocol::default_state(id, mode), mode, - automatic_low_latency); + session_id, id, mode, automatic_low_latency); ++scene_generation; return Web_Response{ Web_Response_Type::Json, - Gallery_Protocol::case_json(id, scene->state(), + Gallery_Protocol::case_json_from_controls(id, scene->controls().dump(), scene->telemetry_json(), "本图已恢复后端默认值", mode) }; } - if (action->id == "rebuild_renderable") { - const std::string id = scene->case_id(); - const auto mode = scene->frame_mode(); - Gallery_State state = scene->state(); - scene->remove_primary(); - scene = std::make_unique(session_id, id, std::move(state), mode, - automatic_low_latency); - ++scene_generation; - return Web_Response{ - Web_Response_Type::Json, - Gallery_Protocol::case_json(id, scene->state(), - scene->telemetry_json(), - "Renderable 已移除并按属性重建", mode) - }; - } bool recognized{}; const std::string notice = scene->action(*action, recognized); if (!recognized) @@ -1882,7 +1584,7 @@ struct Gallery_Plot_Session::Impl { scene->record_action_notice_if_empty(notice); return Web_Response{ Web_Response_Type::Json, - Gallery_Protocol::case_json(scene->case_id(), scene->state(), + Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(), scene->telemetry_json(), notice, scene->frame_mode()) }; diff --git a/web_server/app/Gallery_Properties.h b/web_server/app/Gallery_Properties.h deleted file mode 100644 index 2664d54..0000000 --- a/web_server/app/Gallery_Properties.h +++ /dev/null @@ -1,251 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -namespace renderive::web { -template -class Gallery_Number final { -public: - using value_type = T; - static constexpr T minimum = Minimum; - static constexpr T maximum = Maximum; - static constexpr T step = Step; - constexpr explicit Gallery_Number(T value) : value_(checked(value)) {} - constexpr Gallery_Number& operator=(T value) { - value_ = checked(value); - return *this; - } - constexpr operator T() const noexcept { - return value_; - } - constexpr T value() const noexcept { - return value_; - } -private: - static constexpr T checked(T value) { - if(value < Minimum || value > Maximum) - throw std::out_of_range("value is outside the declared range"); - return value; - } - T value_; -}; -class Gallery_Text final { -public: - explicit Gallery_Text(std::string value) : value_(checked(std::move(value))) {} - Gallery_Text& operator=(std::string value) { - value_ = checked(std::move(value)); - return *this; - } - const std::string& value() const noexcept { - return value_; - } -private: - static std::string checked(std::string value) { - if(value.size() > 160) - throw std::out_of_range("text is longer than 160 bytes"); - return value; - } - std::string value_; -}; -class Gallery_Color final { -public: - explicit Gallery_Color(std::string value) : value_(checked(std::move(value))) {} - Gallery_Color& operator=(std::string value) { - value_ = checked(std::move(value)); - return *this; - } - const std::string& value() const noexcept { - return value_; - } -private: - static std::string checked(std::string value) { - if(value.size() != 7 || value.front() != '#') - throw std::invalid_argument("color must use #RRGGBB"); - for(const char character : std::string_view(value).substr(1)) { - if(!std::isxdigit(static_cast(character))) - throw std::invalid_argument("color must use #RRGGBB"); - } - return value; - } - std::string value_; -}; -enum class Gallery_Cache_Mode { Direct, Local_Pixel }; -enum class Gallery_Axis_Orientation { Horizontal, Vertical }; -enum class Gallery_Decimal_Separator { Dot, Comma }; -enum class Gallery_Line_Interpolation { Nearest_Sample, Linear_Value, Linear_Power_Domain, Step_Left, Step_Right, Cubic_Value }; -enum class Gallery_Image_Interpolation { Nearest, Bilinear, Bicubic }; -enum class Gallery_Color_Map { Spectrum, Ember, Grayscale }; -enum class Gallery_Constellation_Type { PSK4, PSK8, PSK16 }; -using Gallery_Frequency = Gallery_Number; -using Gallery_Positive_Frequency = Gallery_Number; -using Gallery_Axis_Offset = Gallery_Number; -using Gallery_Axis_Length = Gallery_Number; -using Gallery_Tick_Length = Gallery_Number; -using Gallery_Sub_Tick_Length = Gallery_Number; -using Gallery_Font_Size = Gallery_Number; -using Gallery_Font_Weight = Gallery_Number; -using Gallery_Label_Rotation = Gallery_Number; -using Gallery_Label_Precision = Gallery_Number; -struct Gallery_Common_Properties { - Gallery_Color background_color{"#07111f"}; - bool performance_overlay{}; - bool renderable_visible{true}; - Gallery_Cache_Mode cache_mode{Gallery_Cache_Mode::Local_Pixel}; - Gallery_Text object_name{"Gallery_Renderable"}; -}; -struct Gallery_Low_Latency_Properties { - bool frequency_limit_enabled{true}; - Gallery_Positive_Frequency max_render_fps{30}; - bool consumer_feedback_enabled{true}; - bool consumer_pixel_feedback_enabled{}; - bool consumer_presentation_feedback_enabled{true}; - bool consumer_manual_feedback_enabled{}; - Gallery_Positive_Frequency consumer_manual_fps{60}; -}; -struct Gallery_Axis_Properties { - Gallery_Axis_Orientation axis_orientation{Gallery_Axis_Orientation::Horizontal}; - Gallery_Axis_Offset axis_x_offset{0}; - Gallery_Axis_Offset axis_y_offset{0}; - Gallery_Axis_Length axis_length_percent{100}; - Gallery_Tick_Length tick_length{8}; - Gallery_Sub_Tick_Length sub_tick_length{4}; - Gallery_Color axis_color{"#7691b8"}; - Gallery_Decimal_Separator decimal_separator{Gallery_Decimal_Separator::Dot}; - Gallery_Text unit_text{"Hz"}; - Gallery_Font_Size unit_font_size{12}; - Gallery_Font_Weight unit_font_weight{500}; - bool unit_font_italic{}; - Gallery_Color unit_pen_color{"#dce9ff"}; - Gallery_Color unit_background_color{"#07111f"}; - Gallery_Label_Rotation label_rotation{0}; - Gallery_Frequency coord_origin{88'000'000}; - Gallery_Frequency coord_target{108'000'000}; - Gallery_Label_Precision label_precision{2}; - bool use_wheel{true}; - bool use_drag{true}; -}; -struct Gallery_Time_Axis_Properties { - Gallery_Number time_visible_count{80}; - Gallery_Number time_label_spacing{28}; - Gallery_Text time_format{"mm:ss.zzz"}; - Gallery_Number time_font_size{11}; - bool time_newest_at_start{}; -}; -struct Gallery_Hover_Properties { - bool hover_enabled{true}; - Gallery_Number hover_font_size{12}; - Gallery_Color hover_text_color{"#ffffff"}; - Gallery_Color hover_background{"#111827"}; -}; -struct Gallery_Axis_Lab_Properties : Gallery_Common_Properties, Gallery_Axis_Properties, Gallery_Time_Axis_Properties {}; -struct Gallery_Spectrum_Properties : Gallery_Common_Properties, Gallery_Axis_Properties, Gallery_Hover_Properties { - Gallery_Number frequency_point_size{512}; - Gallery_Frequency frequency_origin{88'000'000}; - Gallery_Frequency frequency_target{108'000'000}; - Gallery_Frequency center_frequency{98'000'000}; - Gallery_Frequency sweep_origin{94'000'000}; - Gallery_Frequency sweep_target{102'000'000}; - bool max_hold_visible{true}; - bool min_hold_visible{}; - bool max_marker_visible{true}; - bool use_min_marker{}; - bool sweep_region_visible{true}; - bool visible_range_only{true}; - Gallery_Line_Interpolation line_interpolation{Gallery_Line_Interpolation::Linear_Value}; - Gallery_Color max_brush{"#332810"}; - Gallery_Color current_brush{"#0e3a35"}; - Gallery_Color min_brush{"#19213a"}; - Gallery_Color max_pen{"#ffd166"}; - Gallery_Color current_pen{"#35e6b2"}; - Gallery_Color min_pen{"#7aa2ff"}; - Gallery_Color selected_marker_pen{"#ff4d8d"}; - Gallery_Color marker_pen{"#ff7a59"}; - Gallery_Color middle_frequency_pen{"#6bc8ff"}; - Gallery_Color sweep_region_brush{"#293257"}; -}; -struct Gallery_Selection_Overlay_Properties : Gallery_Spectrum_Properties { - Gallery_Number selection_font_size{12}; - Gallery_Color selection_label_pen{"#ffffff"}; - Gallery_Color selection_brush{"#173b66"}; - Gallery_Color selection_border{"#7dd3fc"}; -}; -struct Gallery_Waterfall_Properties : Gallery_Common_Properties, Gallery_Axis_Properties, Gallery_Time_Axis_Properties, Gallery_Hover_Properties { - Gallery_Frequency frequency_origin{88'000'000}; - Gallery_Frequency frequency_target{108'000'000}; - Gallery_Number power_origin{-120}; - Gallery_Number power_target{-20}; - Gallery_Number frequency_bin_count{256}; - bool visible_range_only{true}; - Gallery_Image_Interpolation image_interpolation{Gallery_Image_Interpolation::Nearest}; - Gallery_Color_Map color_map{Gallery_Color_Map::Spectrum}; -}; -struct Gallery_Afterglow_Properties : Gallery_Common_Properties, Gallery_Axis_Properties { - Gallery_Frequency frequency_origin{88'000'000}; - Gallery_Frequency frequency_target{108'000'000}; - Gallery_Number power_origin{-120}; - Gallery_Number power_target{-20}; - Gallery_Number frequency_point_size{192}; - Gallery_Number power_point_size{96}; - bool interpolate_power{true}; - Gallery_Number attenuation_rate{0.18}; - Gallery_Color_Map color_map{Gallery_Color_Map::Spectrum}; -}; -struct Gallery_Sweep_Spectrum_Properties : Gallery_Common_Properties, Gallery_Axis_Properties { - Gallery_Sweep_Spectrum_Properties() { - coord_origin = 0; - coord_target = 300; - unit_text = "MHz"; - } - Gallery_Frequency frequency_origin{0}; - Gallery_Frequency frequency_target{300}; - Gallery_Number bins_per_block{64}; - Gallery_Number block_count{8}; - Gallery_Color sweep_pen{"#ffd166"}; - Gallery_Color current_frequency_pen{"#ff5d73"}; - bool visible_range_only{true}; - Gallery_Line_Interpolation line_interpolation{Gallery_Line_Interpolation::Linear_Value}; -}; -struct Gallery_Frequency_Trace_Properties : Gallery_Common_Properties, Gallery_Axis_Properties, Gallery_Time_Axis_Properties { - Gallery_Frequency_Trace_Properties() { - coord_origin = -1; - coord_target = 1; - unit_text = "value"; - } - Gallery_Color trace_pen{"#35e6b2"}; - Gallery_Number trace_pen_width{2}; - Gallery_Number trace_value_min{-1}; - Gallery_Number trace_value_max{1}; -}; -struct Gallery_Constellation_Properties : Gallery_Common_Properties, Gallery_Axis_Properties { - Gallery_Constellation_Properties() { - coord_origin = -1.25; - coord_target = 1.25; - unit_text = "I / Q"; - } - Gallery_Number i_origin{-1.25}; - Gallery_Number i_target{1.25}; - Gallery_Number q_origin{-1.25}; - Gallery_Number q_target{1.25}; - Gallery_Color point_color{"#49e6c3"}; - Gallery_Color anchor_color{"#ffd166"}; - Gallery_Number point_lifetime_ms{1800}; - Gallery_Constellation_Type constellation_type{Gallery_Constellation_Type::PSK8}; - Gallery_Number phase_offset{0}; -}; -template -struct Gallery_Low_Latency_Case_Properties : Properties, Gallery_Low_Latency_Properties {}; -using Gallery_Property_Model = std::variant, - Gallery_Low_Latency_Case_Properties, - Gallery_Low_Latency_Case_Properties, - Gallery_Low_Latency_Case_Properties, - Gallery_Low_Latency_Case_Properties, - Gallery_Low_Latency_Case_Properties, - Gallery_Low_Latency_Case_Properties, - Gallery_Low_Latency_Case_Properties>; -} diff --git a/web_server/app/Gallery_Properties_Adminive.h b/web_server/app/Gallery_Properties_Adminive.h deleted file mode 100644 index 3946e5f..0000000 --- a/web_server/app/Gallery_Properties_Adminive.h +++ /dev/null @@ -1,423 +0,0 @@ -#pragma once -#include "Gallery_Properties.h" -#include "adminive/adminive.hpp" -#include "adminive/adapters/nlohmann_json.hpp" -#include -#include -#include -#include -#include -#include -#include -namespace renderive::web::gallery_adminive { -template -struct Enum_Metadata; -template <> -struct Enum_Metadata { - static constexpr std::array values{ - std::pair{Gallery_Cache_Mode::Direct, std::string_view{"Direct"}}, - std::pair{Gallery_Cache_Mode::Local_Pixel, std::string_view{"Local_Pixel"}} - }; -}; -template <> -struct Enum_Metadata { - static constexpr std::array values{ - std::pair{Gallery_Axis_Orientation::Horizontal, std::string_view{"Horizontal"}}, - std::pair{Gallery_Axis_Orientation::Vertical, std::string_view{"Vertical"}} - }; -}; -template <> -struct Enum_Metadata { - static constexpr std::array values{ - std::pair{Gallery_Decimal_Separator::Dot, std::string_view{"."}}, - std::pair{Gallery_Decimal_Separator::Comma, std::string_view{","}} - }; -}; -template <> -struct Enum_Metadata { - static constexpr std::array values{ - std::pair{Gallery_Line_Interpolation::Nearest_Sample, std::string_view{"Nearest_Sample"}}, - std::pair{Gallery_Line_Interpolation::Linear_Value, std::string_view{"Linear_Value"}}, - std::pair{Gallery_Line_Interpolation::Linear_Power_Domain, std::string_view{"Linear_Power_Domain"}}, - std::pair{Gallery_Line_Interpolation::Step_Left, std::string_view{"Step_Left"}}, - std::pair{Gallery_Line_Interpolation::Step_Right, std::string_view{"Step_Right"}}, - std::pair{Gallery_Line_Interpolation::Cubic_Value, std::string_view{"Cubic_Value"}} - }; -}; -template <> -struct Enum_Metadata { - static constexpr std::array values{ - std::pair{Gallery_Image_Interpolation::Nearest, std::string_view{"Nearest"}}, - std::pair{Gallery_Image_Interpolation::Bilinear, std::string_view{"Bilinear"}}, - std::pair{Gallery_Image_Interpolation::Bicubic, std::string_view{"Bicubic"}} - }; -}; -template <> -struct Enum_Metadata { - static constexpr std::array values{ - std::pair{Gallery_Color_Map::Spectrum, std::string_view{"Spectrum"}}, - std::pair{Gallery_Color_Map::Ember, std::string_view{"Ember"}}, - std::pair{Gallery_Color_Map::Grayscale, std::string_view{"Grayscale"}} - }; -}; -template <> -struct Enum_Metadata { - static constexpr std::array values{ - std::pair{Gallery_Constellation_Type::PSK4, std::string_view{"PSK4"}}, - std::pair{Gallery_Constellation_Type::PSK8, std::string_view{"PSK8"}}, - std::pair{Gallery_Constellation_Type::PSK16, std::string_view{"PSK16"}} - }; -}; -template -struct Enum_Adapter { - static std::vector values() { - std::vector result; - result.reserve(Enum_Metadata::values.size()); - for(const auto& [value, name] : Enum_Metadata::values) - result.push_back(value); - return result; - } - static std::string_view name(Enum value) { - for(const auto& [candidate, name] : Enum_Metadata::values) { - if(candidate == value) - return name; - } - return {}; - } - static std::optional cast(std::string_view name) { - for(const auto& [value, candidate] : Enum_Metadata::values) { - if(candidate == name) - return value; - } - return std::nullopt; - } -}; -template -auto boolean_property(std::string name, std::string label, std::string api) { - return adminive::field(std::move(name), std::move(label)).editable().boolean_input().description(std::move(api)); -} -template -auto number_property(std::string name, std::string label, std::string api) { - return adminive::field(std::move(name), std::move(label)).editable().number_input().description(std::move(api)); -} -template -auto text_property(std::string name, std::string label, std::string api) { - return adminive::field(std::move(name), std::move(label)).editable().text_input().description(std::move(api)); -} -template -auto color_property(std::string name, std::string label, std::string api) { - return adminive::field(std::move(name), std::move(label)).editable().color_input().description(std::move(api)); -} -template -auto select_property(std::string name, std::string label, std::string api) { - return adminive::field(std::move(name), std::move(label)).editable().select_input().description(std::move(api)); -} -inline auto common_fields() { - using T = Gallery_Common_Properties; - return std::tuple{ - color_property<&T::background_color>("background_color", "背景颜色", "Plot_Core::set_background_color"), - boolean_property<&T::performance_overlay>("performance_overlay", "性能叠层", "set_performance_overlay_enabled"), - boolean_property<&T::renderable_visible>("renderable_visible", "控件可见", "Renderable::set_visible"), - select_property<&T::cache_mode>("cache_mode", "缓存模式", "Renderable::set_cache_mode"), - text_property<&T::object_name>("object_name", "对象名称", "Renderable::set_object_name") - }; -} -inline auto low_latency_fields() { - using T = Gallery_Low_Latency_Properties; - return std::tuple{ - boolean_property<&T::frequency_limit_enabled>("frequency_limit_enabled", "启用频率上限", "Plot_Core::set_max_render_fps / clear_max_render_fps"), - number_property<&T::max_render_fps>("max_render_fps", "最大渲染 FPS", "Plot_Core::set_max_render_fps"), - boolean_property<&T::consumer_feedback_enabled>("consumer_feedback_enabled", "启用消费者反馈", "Plot_Core::set_consumer_feedback / clear_consumer_feedback"), - boolean_property<&T::consumer_pixel_feedback_enabled>("consumer_pixel_feedback_enabled", "启用像素响应反馈", "Gallery_Plot_Session::set_client_metrics"), - boolean_property<&T::consumer_presentation_feedback_enabled>("consumer_presentation_feedback_enabled", "启用浏览器呈现反馈", "Gallery_Plot_Session::set_client_metrics"), - boolean_property<&T::consumer_manual_feedback_enabled>("consumer_manual_feedback_enabled", "启用手动消费者反馈", "Gallery_Plot_Session::apply_consumer_feedback"), - number_property<&T::consumer_manual_fps>("consumer_manual_fps", "手动消费者 FPS", "Gallery_Plot_Session::apply_consumer_feedback") - }; -} -inline auto axis_fields() { - using T = Gallery_Axis_Properties; - return std::tuple{ - select_property<&T::axis_orientation>("axis_orientation", "主轴方向", "Axis::set<&Axis_Base_Properties::orientation>"), - number_property<&T::axis_x_offset>("axis_x_offset", "X 偏移", "Axis::set<&Axis_Base_Properties::x>"), - number_property<&T::axis_y_offset>("axis_y_offset", "Y 偏移", "Axis::set<&Axis_Base_Properties::y>"), - number_property<&T::axis_length_percent>("axis_length_percent", "轴长百分比", "Axis::set<&Axis_Base_Properties::pixel_length>"), - number_property<&T::tick_length>("tick_length", "主刻度长度", "Axis::set<&Axis_Base_Properties::tick_length>"), - number_property<&T::sub_tick_length>("sub_tick_length", "次刻度长度", "Axis::set<&Axis_Base_Properties::sub_tick_length>"), - color_property<&T::axis_color>("axis_color", "轴颜色", "Axis::set<&Axis_Base_Properties::color>"), - select_property<&T::decimal_separator>("decimal_separator", "小数点", "Axis::set<&Axis_Base_Properties::locale>"), - text_property<&T::unit_text>("unit_text", "单位文本", "Axis::set<&Axis_Base_Properties::unit_text>"), - number_property<&T::unit_font_size>("unit_font_size", "单位字号", "Axis::set<&Axis_Base_Properties::unit_text_font>"), - number_property<&T::unit_font_weight>("unit_font_weight", "单位字重", "Axis::set<&Axis_Base_Properties::unit_text_font>"), - boolean_property<&T::unit_font_italic>("unit_font_italic", "单位斜体", "Axis::set<&Axis_Base_Properties::unit_text_font>"), - color_property<&T::unit_pen_color>("unit_pen_color", "单位文字颜色", "Axis::set<&Axis_Base_Properties::unit_text_pen>"), - color_property<&T::unit_background_color>("unit_background_color", "单位背景颜色", "Axis::set<&Axis_Base_Properties::unit_text_background_brush>"), - number_property<&T::label_rotation>("label_rotation", "标签旋转角度", "Axis::set<&Axis_Base_Properties::label_rotation_degrees>"), - number_property<&T::coord_origin>("coord_origin", "坐标起点", "Axis::set<&Axis_Properties::coordinates>"), - number_property<&T::coord_target>("coord_target", "坐标终点", "Axis::set<&Axis_Properties::coordinates>"), - number_property<&T::label_precision>("label_precision", "标签精度", "Axis::set<&Axis_Properties::precision>"), - boolean_property<&T::use_wheel>("use_wheel", "启用滚轮缩放", "Axis::set<&Axis_Properties::wheel>"), - boolean_property<&T::use_drag>("use_drag", "启用拖拽平移", "Axis::set<&Axis_Properties::drag>") - }; -} -inline auto time_axis_fields() { - using T = Gallery_Time_Axis_Properties; - return std::tuple{ - number_property<&T::time_visible_count>("time_visible_count", "可见时间点", "Time_Axis::set<&Time_Axis_Properties::visible_count>"), - number_property<&T::time_label_spacing>("time_label_spacing", "时间标签间距", "Time_Axis::set<&Time_Axis_Properties::tick_label_spacing_px>"), - text_property<&T::time_format>("time_format", "时间格式", "Time_Axis::set<&Time_Axis_Properties::format>"), - number_property<&T::time_font_size>("time_font_size", "时间字体", "Time_Axis::set<&Axis_Base_Properties::unit_text_font>"), - boolean_property<&T::time_newest_at_start>("time_newest_at_start", "最新数据位于轴起点", "Time_Axis::set<&Time_Axis_Properties::newest_at_start>") - }; -} -inline auto hover_fields() { - using T = Gallery_Hover_Properties; - return std::tuple{ - boolean_property<&T::hover_enabled>("hover_enabled", "悬浮信息", "Plottable_State::set<&Properties::tooltip_enabled>"), - number_property<&T::hover_font_size>("hover_font_size", "悬浮字体", "Plottable_State::set<&Properties::tooltip_font>"), - color_property<&T::hover_text_color>("hover_text_color", "悬浮文字", "Plottable_State::set<&Properties::tooltip_text_pen>"), - color_property<&T::hover_background>("hover_background", "悬浮背景", "Plottable_State::set<&Properties::tooltip_background_brush>") - }; -} -inline auto spectrum_fields() { - using T = Gallery_Spectrum_Properties; - return std::tuple{ - number_property<&T::frequency_point_size>("frequency_point_size", "频率点数", "Spectrum::set<&Properties::frequency_point_size>"), - number_property<&T::frequency_origin>("frequency_origin", "数据频率起点", "Spectrum::set<&Properties::frequency_range>"), - number_property<&T::frequency_target>("frequency_target", "数据频率终点", "Spectrum::set<&Properties::frequency_range>"), - number_property<&T::center_frequency>("center_frequency", "中心频率", "Spectrum::set<&Properties::center_frequency>"), - number_property<&T::sweep_origin>("sweep_origin", "扫频区起点", "Spectrum::set<&Properties::sweep_frequency_range>"), - number_property<&T::sweep_target>("sweep_target", "扫频区终点", "Spectrum::set<&Properties::sweep_frequency_range>"), - boolean_property<&T::max_hold_visible>("max_hold_visible", "最大保持线", "Spectrum::set<&Properties::max_hold_visible>"), - boolean_property<&T::min_hold_visible>("min_hold_visible", "最小保持线", "Spectrum::set<&Properties::min_hold_visible>"), - boolean_property<&T::max_marker_visible>("max_marker_visible", "峰值 Marker", "Spectrum::set<&Properties::max_marker_visible>"), - boolean_property<&T::use_min_marker>("use_min_marker", "最小值 Marker", "Spectrum::set<&Properties::use_min_marker>"), - boolean_property<&T::sweep_region_visible>("sweep_region_visible", "扫频区域", "Spectrum::set<&Properties::sweep_region_visible>"), - boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Spectrum::set<&Properties::visible_range_only>"), - select_property<&T::line_interpolation>("line_interpolation", "线插值模式", "Spectrum::set<&Properties::interpolation_mode>"), - color_property<&T::max_brush>("max_brush", "最大保持填充", "Spectrum::set<&Properties::max_brush>"), - color_property<&T::current_brush>("current_brush", "当前曲线填充", "Spectrum::set<&Properties::current_brush>"), - color_property<&T::min_brush>("min_brush", "最小保持填充", "Spectrum::set<&Properties::min_brush>"), - color_property<&T::max_pen>("max_pen", "最大保持线颜色", "Spectrum::set<&Properties::max_pen>"), - color_property<&T::current_pen>("current_pen", "当前曲线颜色", "Spectrum::set<&Properties::current_pen>"), - color_property<&T::min_pen>("min_pen", "最小保持线颜色", "Spectrum::set<&Properties::min_pen>"), - color_property<&T::selected_marker_pen>("selected_marker_pen", "选中 Marker", "Spectrum::set<&Properties::selected_marker_pen>"), - color_property<&T::marker_pen>("marker_pen", "Marker 颜色", "Spectrum::set<&Properties::marker_pen>"), - color_property<&T::middle_frequency_pen>("middle_frequency_pen", "中心频率线", "Spectrum::set<&Properties::middle_frequency_pen>"), - color_property<&T::sweep_region_brush>("sweep_region_brush", "扫频区填充", "Spectrum::set<&Properties::sweep_region_brush>") - }; -} -template -struct Property_Fields; -template <> -struct Property_Fields { - static auto get() { - return std::tuple_cat(common_fields(), axis_fields(), time_axis_fields()); - } -}; -template <> -struct Property_Fields { - static auto get() { - return std::tuple_cat(common_fields(), axis_fields(), spectrum_fields(), hover_fields()); - } -}; -template <> -struct Property_Fields { - static auto get() { - using T = Gallery_Selection_Overlay_Properties; - return std::tuple_cat(Property_Fields::get(), std::tuple{ - number_property<&T::selection_font_size>("selection_font_size", "框选标签字体", "Selection_Rectangle_Overlay::set<&Properties::label_font>"), - color_property<&T::selection_label_pen>("selection_label_pen", "框选标签颜色", "Selection_Rectangle_Overlay::set<&Properties::label_pen>"), - color_property<&T::selection_brush>("selection_brush", "框选填充", "Selection_Rectangle_Overlay::set<&Properties::selection_brush>"), - color_property<&T::selection_border>("selection_border", "框选边框", "Selection_Rectangle_Overlay::set<&Properties::selection_border_pen>") - }); - } -}; -template <> -struct Property_Fields { - static auto get() { - using T = Gallery_Waterfall_Properties; - return std::tuple_cat(common_fields(), axis_fields(), time_axis_fields(), std::tuple{ - number_property<&T::frequency_origin>("frequency_origin", "频率起点", "Waterfall::set<&Properties::frequency_range>"), - number_property<&T::frequency_target>("frequency_target", "频率终点", "Waterfall::set<&Properties::frequency_range>"), - number_property<&T::power_origin>("power_origin", "功率起点", "Waterfall::set<&Properties::power_range>"), - number_property<&T::power_target>("power_target", "功率终点", "Waterfall::set<&Properties::power_range>"), - number_property<&T::frequency_bin_count>("frequency_bin_count", "频率 Bin", "Waterfall::set<&Properties::frequency_bin_count>"), - boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Waterfall::set<&Properties::visible_range_only>"), - select_property<&T::image_interpolation>("image_interpolation", "图像插值(三模式)", "Waterfall::set<&Properties::interpolation_mode>"), - select_property<&T::color_map>("color_map", "颜色映射", "Waterfall::set<&Properties::color_map>") - }, hover_fields()); - } -}; -template <> -struct Property_Fields { - static auto get() { - using T = Gallery_Afterglow_Properties; - return std::tuple_cat(common_fields(), axis_fields(), std::tuple{ - number_property<&T::frequency_origin>("frequency_origin", "频率起点", "Afterglow::set<&Properties::frequency_range>"), - number_property<&T::frequency_target>("frequency_target", "频率终点", "Afterglow::set<&Properties::frequency_range>"), - number_property<&T::power_origin>("power_origin", "功率起点", "Afterglow::set<&Properties::power_range>"), - number_property<&T::power_target>("power_target", "功率终点", "Afterglow::set<&Properties::power_range>"), - number_property<&T::frequency_point_size>("frequency_point_size", "频率格点", "Afterglow::set<&Properties::frequency_point_size>"), - number_property<&T::power_point_size>("power_point_size", "功率格点", "Afterglow::set<&Properties::power_point_size>"), - boolean_property<&T::interpolate_power>("interpolate_power", "功率 Bin 插值", "Afterglow::set<&Properties::interpolate>"), - number_property<&T::attenuation_rate>("attenuation_rate", "衰减率", "Afterglow::set<&Properties::attenuation_rate>"), - select_property<&T::color_map>("color_map", "颜色映射", "Afterglow::set<&Properties::color_map>") - }); - } -}; -template <> -struct Property_Fields { - static auto get() { - using T = Gallery_Sweep_Spectrum_Properties; - return std::tuple_cat(common_fields(), axis_fields(), std::tuple{ - number_property<&T::frequency_origin>("frequency_origin", "扫频起点", "Sweep_Spectrum::set<&Properties::frequency_range>"), - number_property<&T::frequency_target>("frequency_target", "扫频终点", "Sweep_Spectrum::set<&Properties::frequency_range>"), - number_property<&T::bins_per_block>("bins_per_block", "每块 Bin", "Sweep_Spectrum::set<&Properties::bins_per_block>"), - number_property<&T::block_count>("block_count", "块数量", "Sweep_Spectrum::set<&Properties::block_count>"), - color_property<&T::sweep_pen>("sweep_pen", "扫频曲线", "Sweep_Spectrum::set<&Properties::pen>"), - color_property<&T::current_frequency_pen>("current_frequency_pen", "当前频率游标", "Sweep_Spectrum::set<&Properties::current_frequency_pen>"), - boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Sweep_Spectrum::set<&Properties::visible_range_only>"), - select_property<&T::line_interpolation>("line_interpolation", "线插值模式", "Sweep_Spectrum::set<&Properties::interpolation_mode>") - }); - } -}; -template <> -struct Property_Fields { - static auto get() { - using T = Gallery_Frequency_Trace_Properties; - return std::tuple_cat(common_fields(), axis_fields(), time_axis_fields(), std::tuple{ - color_property<&T::trace_pen>("trace_pen", "轨迹颜色", "Frequency_Trace::set<&Properties::pen>"), - number_property<&T::trace_pen_width>("trace_pen_width", "轨迹宽度", "Frequency_Trace::set<&Properties::pen>"), - number_property<&T::trace_value_min>("trace_value_min", "数值下限", "Axis::set<&Axis_Properties::coordinates>"), - number_property<&T::trace_value_max>("trace_value_max", "数值上限", "Axis::set<&Axis_Properties::coordinates>") - }); - } -}; -template <> -struct Property_Fields { - static auto get() { - using T = Gallery_Constellation_Properties; - return std::tuple_cat(common_fields(), axis_fields(), std::tuple{ - number_property<&T::i_origin>("i_origin", "I 起点", "Constellation_Diagram::set<&Properties::i_range>"), - number_property<&T::i_target>("i_target", "I 终点", "Constellation_Diagram::set<&Properties::i_range>"), - number_property<&T::q_origin>("q_origin", "Q 起点", "Constellation_Diagram::set<&Properties::q_range>"), - number_property<&T::q_target>("q_target", "Q 终点", "Constellation_Diagram::set<&Properties::q_range>"), - color_property<&T::point_color>("point_color", "采样点颜色", "Constellation_Diagram::set<&Properties::point_color>"), - color_property<&T::anchor_color>("anchor_color", "锚点颜色", "Constellation_Diagram::set<&Properties::anchor_color>"), - number_property<&T::point_lifetime_ms>("point_lifetime_ms", "采样点寿命", "Constellation_Diagram::set<&Properties::point_lifetime_ms>"), - select_property<&T::constellation_type>("constellation_type", "星座模式", "Constellation_Diagram::set<&Properties::type>"), - number_property<&T::phase_offset>("phase_offset", "相位偏移", "Constellation_Diagram::set<&Properties::phase_offset_radians>") - }); - } -}; -template -struct Property_Fields> { - static auto get() { - return std::tuple_cat(Property_Fields::get(), low_latency_fields()); - } -}; -struct Property_Validator { - template - void operator()(const T& value, adminive::Validation_Context& context) const { - if constexpr(requires { value.coord_origin; value.coord_target; }) - validate_pair(context, "coord_origin", "coord_target", value.coord_origin, value.coord_target); - if constexpr(requires { value.frequency_origin; value.frequency_target; }) - validate_pair(context, "frequency_origin", "frequency_target", value.frequency_origin, value.frequency_target); - if constexpr(requires { value.power_origin; value.power_target; }) - validate_pair(context, "power_origin", "power_target", value.power_origin, value.power_target); - if constexpr(requires { value.sweep_origin; value.sweep_target; }) - validate_pair(context, "sweep_origin", "sweep_target", value.sweep_origin, value.sweep_target); - if constexpr(requires { value.trace_value_min; value.trace_value_max; }) - validate_pair(context, "trace_value_min", "trace_value_max", value.trace_value_min, value.trace_value_max); - if constexpr(requires { value.i_origin; value.i_target; }) - validate_pair(context, "i_origin", "i_target", value.i_origin, value.i_target); - if constexpr(requires { value.q_origin; value.q_target; }) - validate_pair(context, "q_origin", "q_target", value.q_origin, value.q_target); - } -private: - template - static void validate_pair(adminive::Validation_Context& context, std::string_view origin, std::string_view target, const Origin& origin_value, const Target& target_value) { - if(static_cast(origin_value) == static_cast(target_value)) { - context.error(std::string(origin), std::string(origin) + " 与 " + std::string(target) + " 不能相等"); - context.error(std::string(target), std::string(origin) + " 与 " + std::string(target) + " 不能相等"); - } - } -}; -template -auto property_descriptor() { - auto fields = Property_Fields::get(); - return std::apply([](auto... field) { - return adminive::object("renderive_gallery_properties", "Renderive 控件属性", std::move(field)...).validator(Property_Validator{}); - }, std::move(fields)); -} -} -namespace adminive { -template -struct Value_Adapter, Json> { - using Storage = renderive::web::Gallery_Number; - using value_type = T; - static constexpr T minimum = Minimum; - static constexpr T maximum = Maximum; - static constexpr T multiple_of = Step; - static T read(const Storage& value) noexcept { - return value.value(); - } - static void write(Storage& target, T value) { - target = value; - } -}; -template -struct Value_Adapter { - using value_type = std::string; - static const std::string& read(const renderive::web::Gallery_Text& value) noexcept { - return value.value(); - } - static void write(renderive::web::Gallery_Text& target, std::string value) { - target = std::move(value); - } -}; -template -struct Value_Adapter { - using value_type = std::string; - static const std::string& read(const renderive::web::Gallery_Color& value) noexcept { - return value.value(); - } - static void write(renderive::web::Gallery_Color& target, std::string value) { - target = std::move(value); - } -}; -#define RENDERIVE_GALLERY_ENUM_ADAPTER(Type) \ -template <> \ -struct Enum_Adapter : renderive::web::gallery_adminive::Enum_Adapter {} -RENDERIVE_GALLERY_ENUM_ADAPTER(Gallery_Cache_Mode); -RENDERIVE_GALLERY_ENUM_ADAPTER(Gallery_Axis_Orientation); -RENDERIVE_GALLERY_ENUM_ADAPTER(Gallery_Decimal_Separator); -RENDERIVE_GALLERY_ENUM_ADAPTER(Gallery_Line_Interpolation); -RENDERIVE_GALLERY_ENUM_ADAPTER(Gallery_Image_Interpolation); -RENDERIVE_GALLERY_ENUM_ADAPTER(Gallery_Color_Map); -RENDERIVE_GALLERY_ENUM_ADAPTER(Gallery_Constellation_Type); -#undef RENDERIVE_GALLERY_ENUM_ADAPTER -#define RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Type) \ -template <> \ -struct Type_Descriptor { \ - static auto get() { \ - return renderive::web::gallery_adminive::property_descriptor(); \ - } \ -} -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Axis_Lab_Properties); -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Spectrum_Properties); -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Selection_Overlay_Properties); -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Waterfall_Properties); -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Afterglow_Properties); -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Sweep_Spectrum_Properties); -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Frequency_Trace_Properties); -RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR(Gallery_Constellation_Properties); -#undef RENDERIVE_GALLERY_PROPERTY_DESCRIPTOR -template -struct Type_Descriptor> { - static auto get() { - using T = renderive::web::Gallery_Low_Latency_Case_Properties; - return renderive::web::gallery_adminive::property_descriptor(); - } -}; -} diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index d5dbaf8..03965cf 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -1,5 +1,6 @@ #include "Gallery_Protocol.h" -#include "Gallery_Properties_Adminive.h" +#include "Gallery_Renderables_Adminive.h" +#include "Gallery_Session_Control_Adminive.h" #include "adminive/adminive.hpp" #include "adminive/adapters/nlohmann_json.hpp" @@ -88,10 +89,7 @@ std::vector actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) { std::vector result{ action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "会话"), - action("rebuild_renderable", "移除并重建控件", "Plot_Core::remove_renderable / Builder::build", "会话"), action("toggle_view", "切换 View 生命周期", "Plot_Core::activate_view / deactivate_view", "会话"), - action("axis_set_start", "写入坐标范围起点", "Axis::set<&Axis_Properties::coordinates>", "Axis"), - action("axis_set_length", "写入坐标范围跨度", "Axis::set<&Axis_Properties::coordinates>", "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) { @@ -178,67 +176,6 @@ const Case_Model* find_case(std::string_view id) { Json parse_request(std::string_view message) { return Json::parse(message.begin(), message.end()); } -template -Gallery_Property_Model make_property_model(Gallery_Frame_Mode frame_mode) { - if(frame_mode == Gallery_Frame_Mode::Low_Latency) { - using T = Gallery_Low_Latency_Case_Properties; - return Gallery_Property_Model{std::in_place_type}; - } - return Gallery_Property_Model{std::in_place_type}; -} -Gallery_Property_Model make_property_model(std::string_view case_id, Gallery_Frame_Mode frame_mode) { - if(case_id == "spectrum") - return make_property_model(frame_mode); - if(case_id == "afterglow") - return make_property_model(frame_mode); - if(case_id == "sweep_spectrum") - return make_property_model(frame_mode); - if(case_id == "waterfall") - return make_property_model(frame_mode); - if(case_id == "frequency_trace") - return make_property_model(frame_mode); - if(case_id == "selection_overlay") - return make_property_model(frame_mode); - if(case_id == "constellation") - return make_property_model(frame_mode); - return make_property_model(frame_mode); -} -Gallery_Value gallery_value(const Json& value) { - if(value.is_boolean()) - return value.get(); - if(value.is_number()) - return value.get(); - return value.get(); -} -template -std::optional property_value(const T& value, std::string_view id) { - std::optional result; - const auto descriptor = adminive::describe(); - std::apply([&](const auto&... field) { - ([&] { - if(!result && field.name() == id) - result = gallery_value(adminive::encode_json_value(field.get(value))); - }(), ...); - }, descriptor.fields()); - return result; -} -template -std::size_t property_count(const T&) { - return std::tuple_size_v>; -} -template -Json property_resource(const T& value) { - return { - {"descriptor", adminive::to_descriptor_json()}, - {"view", adminive::to_view_json(adminive::describe_edit_view().titled("控件属性").submit("应用"))}, - {"data", adminive::to_frontend_json(value)} - }; -} -template -adminive::Update_Result apply_property_patch(T& value, const Json& patch) { - return adminive::apply_frontend_patch(value, patch); -} - adminive::Table_View action_view(); } // namespace renderive::web::gallery_detail @@ -525,27 +462,6 @@ Json frame_mode_contract(Gallery_Frame_Mode mode) { } // namespace -Gallery_Value gallery_property_value(const Gallery_State& state, std::string_view id) { - const auto result = std::visit([id](const auto& properties) { - return gallery_detail::property_value(properties, id); - }, state.properties); - if(!result) - throw std::out_of_range("gallery property does not exist: " + std::string(id)); - return *result; -} - -bool gallery_has_property(const Gallery_State& state, std::string_view id) { - return std::visit([id](const auto& properties) { - return gallery_detail::property_value(properties, id).has_value(); - }, state.properties); -} - -std::size_t gallery_property_count(const Gallery_State& state) { - return std::visit([](const auto& properties) { - return gallery_detail::property_count(properties); - }, state.properties); -} - std::string Gallery_Protocol::catalog_json() { Json result = protocol_base(); result["type"] = "catalog"; @@ -600,27 +516,23 @@ 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) { - return {gallery_detail::make_property_model(case_id, frame_mode)}; -} - -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) { +std::string Gallery_Protocol::case_json_from_controls( + std::string_view case_id, + std::string_view controls_json, + 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"] = std::visit([](const auto& properties) { - return gallery_detail::property_resource(properties); - }, state.properties); + try { + result["controls"] = { + {"resources", Json::parse(controls_json.begin(), controls_json.end())} + }; + } catch (const std::exception&) { + result["controls"] = {{"resources", Json::array()}}; + } result["actions"] = { {"descriptor", adminive::to_descriptor_json()}, {"view", adminive::to_view_json( @@ -640,14 +552,6 @@ std::string Gallery_Protocol::case_json(std::string_view case_id, 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(); @@ -674,40 +578,6 @@ std::string Gallery_Protocol::observer_json(std::string_view case_id, 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 格式无效")}; - } - Gallery_State candidate = current; - const auto update = std::visit([&request](auto& properties) { - return gallery_detail::apply_property_patch(properties, request.at("patch")); - }, candidate.properties); - if(!update.success) { - Json response = protocol_base(); - response["type"] = "error"; - response["message"] = update.message; - response["field_errors"] = update.field_errors; - return {std::nullopt, response.dump()}; - } - return {std::move(candidate), {}}; - } catch (const std::exception& error) { - return {std::nullopt, error_json(error.what())}; - } -} - -Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id, - const Gallery_State& current, - std::string_view message) { - return apply_patch(case_id, current, message, Gallery_Frame_Mode::Low_Latency); -} - std::optional Gallery_Protocol::open_request(std::string_view message) { try { const Json request = gallery_detail::parse_request(message); @@ -756,9 +626,68 @@ std::optional Gallery_Protocol::action_request( } } +std::optional Gallery_Protocol::control_patch_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_patch" || + !request.contains("target") || !request.at("target").is_string() || + !request.contains("patch") || !request.at("patch").is_object()) + return std::nullopt; + return Gallery_Control_Patch_Request{ + request.at("target").get(), request.at("patch").dump()}; + } 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_property_count(default_state(case_id, frame_mode)); + const auto count = [](const auto& self, const Json& fields) -> std::size_t { + std::size_t result{}; + for (const auto& field : fields) { + if (field.value("editable", false) && !field.contains("children")) + ++result; + if (field.contains("children")) + result += self(self, field.at("children")); + } + return result; + }; + const auto type_count = [&count]() { + return count(count, adminive::to_descriptor_json().at("fields")); + }; + std::size_t result = type_count.template operator()(); + if (case_id == "axis_lab") + return result + type_count.template operator()() + + type_count.template operator()(); + if (case_id == "spectrum" || case_id == "selection_overlay") { + result += type_count.template operator()() + + type_count.template operator()() + + type_count.template operator()(); + if (case_id == "selection_overlay") + result += type_count.template operator()(); + return result; + } + if (case_id == "waterfall") + return result + type_count.template operator()() + + type_count.template operator()() + + type_count.template operator()(); + if (case_id == "afterglow") + return result + type_count.template operator()() + + type_count.template operator()() + + type_count.template operator()(); + if (case_id == "sweep_spectrum") + return result + 2 * type_count.template operator()() + + type_count.template operator()(); + if (case_id == "frequency_trace") + return result + type_count.template operator()() + + type_count.template operator()() + + type_count.template operator()(); + if (case_id == "constellation") + return result + 2 * type_count.template operator()() + + type_count.template operator()(); + return result; } std::size_t Gallery_Protocol::control_count(std::string_view case_id) { diff --git a/web_server/app/Gallery_Protocol.h b/web_server/app/Gallery_Protocol.h index 08a5c36..621135d 100644 --- a/web_server/app/Gallery_Protocol.h +++ b/web_server/app/Gallery_Protocol.h @@ -1,5 +1,4 @@ #pragma once -#include "Gallery_Properties.h" #include #include #include @@ -16,49 +15,33 @@ struct Gallery_Open_Request { Gallery_Frame_Mode frame_mode = Gallery_Frame_Mode::Low_Latency; }; using Gallery_Value = std::variant; -struct Gallery_State { - Gallery_Property_Model properties; -}; -[[nodiscard]] Gallery_Value gallery_property_value(const Gallery_State& state, std::string_view id); -[[nodiscard]] bool gallery_has_property(const Gallery_State& state, std::string_view id); -[[nodiscard]] std::size_t gallery_property_count(const Gallery_State& state); -struct Gallery_Patch_Result { - std::optional candidate; - std::string response_json; -}; struct Gallery_Action_Request { std::string id; std::optional argument; }; +struct Gallery_Control_Patch_Request { + std::string target; + std::string patch_json; +}; class Gallery_Protocol final { public: [[nodiscard]] static std::string catalog_json(); [[nodiscard]] static bool is_case(std::string_view case_id); - [[nodiscard]] static Gallery_State default_state(std::string_view case_id); - [[nodiscard]] static Gallery_State default_state(std::string_view case_id, Gallery_Frame_Mode frame_mode); - [[nodiscard]] static std::string case_json(std::string_view case_id, - const Gallery_State& state, - std::string_view telemetry_json = "{}", - std::string_view notice = {}); - [[nodiscard]] static std::string 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); [[nodiscard]] static std::string error_json(std::string_view message, std::string_view field = {}); [[nodiscard]] static std::string observer_json(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view telemetry_json); - [[nodiscard]] static Gallery_Patch_Result apply_patch(std::string_view case_id, - const Gallery_State& current, - std::string_view message); - [[nodiscard]] static Gallery_Patch_Result apply_patch(std::string_view case_id, - const Gallery_State& current, - std::string_view message, - Gallery_Frame_Mode frame_mode); + [[nodiscard]] static std::string case_json_from_controls( + std::string_view case_id, + std::string_view controls_json, + std::string_view telemetry_json, + std::string_view notice, + Gallery_Frame_Mode frame_mode); [[nodiscard]] static std::optional open_request(std::string_view message); [[nodiscard]] static std::optional action_request(std::string_view message); + [[nodiscard]] static std::optional control_patch_request( + std::string_view message); [[nodiscard]] static std::size_t control_count(std::string_view case_id); [[nodiscard]] static std::size_t control_count(std::string_view case_id, Gallery_Frame_Mode frame_mode); [[nodiscard]] static std::size_t action_count(std::string_view case_id); diff --git a/web_server/app/Gallery_Renderables.h b/web_server/app/Gallery_Renderables.h new file mode 100644 index 0000000..ad2a60b --- /dev/null +++ b/web_server/app/Gallery_Renderables.h @@ -0,0 +1,129 @@ +#pragma once + +#include "render_2D/axis/Axis.h" +#include "render_2D/axis/Frequency_Axis.h" +#include "render_2D/axis/Time_Axis.h" +#include "render_2D/plottable/Afterglow.h" +#include "render_2D/plottable/Constellation_Diagram.h" +#include "render_2D/plottable/Frequency_Trace.h" +#include "render_2D/plottable/Selection_Rectangle_Overlay.h" +#include "render_2D/plottable/Spectrum.h" +#include "render_2D/plottable/Sweep_Spectrum.h" +#include "render_2D/plottable/Waterfall.h" + +#include + +namespace adminive { +template +struct Object_Adapter; +} + +namespace renderive::web { + +template +class Gallery_Plottable : public Base { +public: + using Properties = typename Base::Properties; + using Base::Base; + +private: + [[nodiscard]] Properties adminive_snapshot() const { + return this->properties(); + } + + void adminive_commit(Properties properties) { + this->replace_properties(std::move(properties)); + } + + template + friend struct ::adminive::Object_Adapter; +}; + +class Gallery_Spectrum final : public Gallery_Plottable { +public: + using Gallery_Plottable::Gallery_Plottable; + using Builder = Renderable_Builder; +}; + +class Gallery_Waterfall final : public Gallery_Plottable { +public: + using Gallery_Plottable::Gallery_Plottable; + using Builder = Renderable_Builder; +}; + +class Gallery_Afterglow final : public Gallery_Plottable { +public: + using Gallery_Plottable::Gallery_Plottable; + using Builder = Renderable_Builder; +}; + +class Gallery_Sweep_Spectrum final : public Gallery_Plottable { +public: + using Gallery_Plottable::Gallery_Plottable; + using Builder = Renderable_Builder; +}; + +class Gallery_Frequency_Trace final : public Gallery_Plottable { +public: + using Gallery_Plottable::Gallery_Plottable; + using Builder = Renderable_Builder; +}; + +class Gallery_Selection_Rectangle_Overlay final + : public Gallery_Plottable { +public: + using Gallery_Plottable::Gallery_Plottable; + using Builder = Renderable_Builder; +}; + +class Gallery_Constellation_Diagram final + : public Gallery_Plottable { +public: + using Gallery_Plottable::Gallery_Plottable; + using Builder = Renderable_Builder; +}; + +template +class Gallery_Axis_Base : public Base { +public: + using Properties = Properties_Type; + using Base::Base; + +private: + [[nodiscard]] Properties adminive_snapshot() const { + return this->read([](const auto& state) { + return Properties(static_cast(state)); + }); + } + + void adminive_commit(Properties properties) { + this->update([&properties](auto& state) { + static_cast(state) = std::move(properties); + }); + } + + template + friend struct ::adminive::Object_Adapter; +}; + +class Gallery_Axis final : public Gallery_Axis_Base { +public: + using Gallery_Axis_Base::Gallery_Axis_Base; + using Builder = detail::Axis_Renderable_Builder; +}; + +class Gallery_Frequency_Axis final + : public Gallery_Axis_Base { +public: + using Gallery_Axis_Base::Gallery_Axis_Base; + using Builder = detail::Axis_Renderable_Builder; +}; + +class Gallery_Time_Axis final + : public Gallery_Axis_Base { +public: + using Gallery_Axis_Base::Gallery_Axis_Base; + using Builder = detail::Axis_Renderable_Builder; +}; + +} // namespace renderive::web diff --git a/web_server/app/Gallery_Renderables_Adminive.h b/web_server/app/Gallery_Renderables_Adminive.h new file mode 100644 index 0000000..ab5f8d1 --- /dev/null +++ b/web_server/app/Gallery_Renderables_Adminive.h @@ -0,0 +1,488 @@ +#pragma once + +#include "Gallery_Renderables.h" + +#include "adminive/adminive.hpp" +#include "adminive/adapters/nlohmann_json.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::web::gallery_adminive { + +enum class Number_Locale_Choice { Point, Comma }; +enum class Color_Map_Choice { Spectrum, Ember, Grayscale }; + +template +struct Enum_Metadata; + +#define RENDERIVE_ENUM_METADATA(Type, ...) \ + template <> struct Enum_Metadata { static constexpr auto values = std::array{__VA_ARGS__}; } + +RENDERIVE_ENUM_METADATA(Orientation, + std::pair{Orientation::Horizontal, std::string_view{"Horizontal"}}, + std::pair{Orientation::Vertical, std::string_view{"Vertical"}}); +RENDERIVE_ENUM_METADATA(Renderable_Cache_Mode, + std::pair{Renderable_Cache_Mode::Direct, std::string_view{"Direct"}}, + std::pair{Renderable_Cache_Mode::Local_Pixel, std::string_view{"Local_Pixel"}}); +RENDERIVE_ENUM_METADATA(Line_Interpolation_Mode, + std::pair{Line_Interpolation_Mode::Nearest_Sample, std::string_view{"Nearest_Sample"}}, + std::pair{Line_Interpolation_Mode::Linear_Value, std::string_view{"Linear_Value"}}, + std::pair{Line_Interpolation_Mode::Linear_Power_Domain, std::string_view{"Linear_Power_Domain"}}, + std::pair{Line_Interpolation_Mode::Step_Left, std::string_view{"Step_Left"}}, + std::pair{Line_Interpolation_Mode::Step_Right, std::string_view{"Step_Right"}}, + std::pair{Line_Interpolation_Mode::Cubic_Value, std::string_view{"Cubic_Value"}}); +RENDERIVE_ENUM_METADATA(Image_Interpolation_Mode, + std::pair{Image_Interpolation_Mode::Nearest, std::string_view{"Nearest"}}, + std::pair{Image_Interpolation_Mode::Bilinear, std::string_view{"Bilinear"}}, + std::pair{Image_Interpolation_Mode::Bicubic, std::string_view{"Bicubic"}}); +RENDERIVE_ENUM_METADATA(Constellation_Diagram_Type, + std::pair{Constellation_Diagram_Type::Psk4, std::string_view{"PSK4"}}, + std::pair{Constellation_Diagram_Type::Psk8, std::string_view{"PSK8"}}, + std::pair{Constellation_Diagram_Type::Psk16, std::string_view{"PSK16"}}); +RENDERIVE_ENUM_METADATA(Line_Style, + std::pair{Line_Style::None, std::string_view{"None"}}, + std::pair{Line_Style::Solid, std::string_view{"Solid"}}, + std::pair{Line_Style::Dash, std::string_view{"Dash"}}, + std::pair{Line_Style::Dot, std::string_view{"Dot"}}); +RENDERIVE_ENUM_METADATA(Line_Cap, + std::pair{Line_Cap::Butt, std::string_view{"Butt"}}, + std::pair{Line_Cap::Square, std::string_view{"Square"}}, + std::pair{Line_Cap::Round, std::string_view{"Round"}}); +RENDERIVE_ENUM_METADATA(Line_Join, + std::pair{Line_Join::Miter, std::string_view{"Miter"}}, + std::pair{Line_Join::Bevel, std::string_view{"Bevel"}}, + std::pair{Line_Join::Round, std::string_view{"Round"}}); +RENDERIVE_ENUM_METADATA(Brush_Style, + std::pair{Brush_Style::None, std::string_view{"None"}}, + std::pair{Brush_Style::Solid, std::string_view{"Solid"}}); +RENDERIVE_ENUM_METADATA(Number_Locale_Choice, + std::pair{Number_Locale_Choice::Point, std::string_view{"."}}, + std::pair{Number_Locale_Choice::Comma, std::string_view{","}}); +RENDERIVE_ENUM_METADATA(Color_Map_Choice, + std::pair{Color_Map_Choice::Spectrum, std::string_view{"Spectrum"}}, + std::pair{Color_Map_Choice::Ember, std::string_view{"Ember"}}, + std::pair{Color_Map_Choice::Grayscale, std::string_view{"Grayscale"}}); + +#undef RENDERIVE_ENUM_METADATA + +template +struct Actual_Enum_Adapter { + static std::vector values() { + std::vector result; + result.reserve(Enum_Metadata::values.size()); + for (const auto& [value, name] : Enum_Metadata::values) + result.push_back(value); + return result; + } + + static std::string_view name(Enum value) { + for (const auto& [candidate, name] : Enum_Metadata::values) { + if (candidate == value) + return name; + } + return {}; + } + + static std::optional cast(std::string_view name) { + for (const auto& [value, candidate] : Enum_Metadata::values) { + if (candidate == name) + return value; + } + return std::nullopt; + } +}; + +inline Color blend(Color first, Color second, double amount) { + const auto channel = [amount](std::uint8_t a, std::uint8_t b) { + return static_cast(std::clamp( + std::lround(a + (b - a) * amount), 0L, 255L)); + }; + return {channel(first.r, second.r), channel(first.g, second.g), + channel(first.b, second.b), channel(first.a, second.a)}; +} + +inline Color_Map color_map(std::string_view palette) { + std::array stops{ + Color{5, 10, 25, 255}, Color{20, 52, 112, 255}, Color{39, 196, 181, 255}, + Color{242, 201, 76, 255}, Color{255, 76, 106, 255} + }; + if (palette == "Ember") { + stops = {Color{10, 5, 8, 255}, Color{62, 18, 30, 255}, Color{153, 47, 39, 255}, + Color{244, 132, 48, 255}, Color{255, 238, 166, 255}}; + } else if (palette == "Grayscale") { + stops = {Color{0, 0, 0, 255}, Color{52, 52, 52, 255}, Color{112, 112, 112, 255}, + Color{188, 188, 188, 255}, Color{255, 255, 255, 255}}; + } + std::vector colors; + colors.reserve(256); + for (int index = 0; index < 256; ++index) { + const double position = index / 255.0 * (stops.size() - 1); + const auto first = static_cast(std::floor(position)); + const auto second = std::min(first + 1, stops.size() - 1); + colors.push_back(premultiply(blend(stops[first], stops[second], position - first))); + } + return Color_Map(std::move(colors)); +} + +inline std::string color_map_name(const Color_Map& value) { + for (const std::string_view name : {std::string_view{"Spectrum"}, + std::string_view{"Ember"}, + std::string_view{"Grayscale"}}) { + if (value.colors() == color_map(name).colors()) + return std::string(name); + } + return "Spectrum"; +} + +template +auto editable(std::string name, std::string label, adminive::Field_Control control) { + return adminive::field(std::move(name), std::move(label)) + .editable().control(control); +} + +template +auto number(std::string name, std::string label) { + return editable(std::move(name), std::move(label), adminive::Field_Control::number); +} + +template +auto boolean(std::string name, std::string label) { + return editable(std::move(name), std::move(label), adminive::Field_Control::boolean); +} + +template +auto select(std::string name, std::string label) { + return editable(std::move(name), std::move(label), adminive::Field_Control::select); +} + +template +auto color(std::string name, std::string label) { + return editable(std::move(name), std::move(label), adminive::Field_Control::color); +} + +} // namespace renderive::web::gallery_adminive + +namespace adminive { + +template +struct Renderive_Validator_Metadata {}; + +template +struct Renderive_Validator_Metadata<::Range_Validator> { + static constexpr T minimum = Minimum; + static constexpr T maximum = Maximum; +}; + +template +struct Value_Adapter<::Validated_Value, Json> + : Renderive_Validator_Metadata { + using Storage = ::Validated_Value; + using value_type = T; + static const T& read(const Storage& value) noexcept { return value.get(); } + static void write(Storage& target, T value) { target = std::move(value); } +}; + +#define RENDERIVE_ENUM_ADAPTER(Type) \ + template <> struct Enum_Adapter \ + : renderive::web::gallery_adminive::Actual_Enum_Adapter {} + +RENDERIVE_ENUM_ADAPTER(Orientation); +RENDERIVE_ENUM_ADAPTER(Renderable_Cache_Mode); +RENDERIVE_ENUM_ADAPTER(Line_Interpolation_Mode); +RENDERIVE_ENUM_ADAPTER(Image_Interpolation_Mode); +RENDERIVE_ENUM_ADAPTER(Constellation_Diagram_Type); +RENDERIVE_ENUM_ADAPTER(Line_Style); +RENDERIVE_ENUM_ADAPTER(Line_Cap); +RENDERIVE_ENUM_ADAPTER(Line_Join); +RENDERIVE_ENUM_ADAPTER(Brush_Style); +#undef RENDERIVE_ENUM_ADAPTER + +template <> +struct Enum_Adapter + : renderive::web::gallery_adminive::Actual_Enum_Adapter< + renderive::web::gallery_adminive::Number_Locale_Choice> {}; + +template <> +struct Enum_Adapter + : renderive::web::gallery_adminive::Actual_Enum_Adapter< + renderive::web::gallery_adminive::Color_Map_Choice> {}; + +template +struct Value_Adapter { + using value_type = std::string; + static std::string read(renderive::Color value) { + char result[8]{}; + std::snprintf(result, sizeof(result), "#%02x%02x%02x", value.r, value.g, value.b); + return result; + } + static void write(renderive::Color& target, const std::string& value) { + if (value.size() != 7 || value.front() != '#' || + !std::all_of(value.begin() + 1, value.end(), [](unsigned char character) { + return std::isxdigit(character) != 0; + })) + throw std::invalid_argument("color must use #RRGGBB"); + const auto channel = [&value](std::size_t offset) { + return static_cast(std::stoul(value.substr(offset, 2), nullptr, 16)); + }; + target.r = channel(1); + target.g = channel(3); + target.b = channel(5); + } +}; + +template +struct Value_Adapter { + using Choice = renderive::web::gallery_adminive::Number_Locale_Choice; + using value_type = Choice; + static Choice read(renderive::Number_Locale value) { + return value.decimal_point == ',' ? Choice::Comma : Choice::Point; + } + static void write(renderive::Number_Locale& target, Choice value) { + target.decimal_point = value == Choice::Comma ? ',' : '.'; + } +}; + +template +struct Value_Adapter, Json> { + using Storage = renderive::Clamped_Property; + using value_type = T; + static constexpr T minimum = Minimum; + static constexpr T maximum = Maximum; + static T read(const Storage& value) noexcept { return value.get(); } + static void write(Storage& target, T value) { target = value; } +}; + +template +struct Value_Adapter { + using value_type = double; + static constexpr double minimum = 0.0; + static constexpr double maximum = 1.0; + static constexpr double multiple_of = 0.01; + static double read(const renderive::Unit_Interval& value) noexcept { return value.get(); } + static void write(renderive::Unit_Interval& target, double value) { target = value; } +}; + +template +struct Value_Adapter { + using Choice = renderive::web::gallery_adminive::Color_Map_Choice; + using value_type = Choice; + static Choice read(const renderive::Color_Map& value) { + const auto name = renderive::web::gallery_adminive::color_map_name(value); + if (name == "Ember") + return Choice::Ember; + if (name == "Grayscale") + return Choice::Grayscale; + return Choice::Spectrum; + } + static void write(renderive::Color_Map& target, Choice value) { + const std::string_view name = value == Choice::Ember ? "Ember" : + value == Choice::Grayscale ? "Grayscale" : "Spectrum"; + target = renderive::web::gallery_adminive::color_map(name); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::Range; + using namespace renderive::web::gallery_adminive; + return object("range", "Range", + number<&T::origin>("origin", "Origin"), + number<&T::target>("target", "Target")); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::Pen; + using namespace renderive::web::gallery_adminive; + return object("pen", "Pen", + color<&T::color>("color", "Color"), + number<&T::width>("width", "Width"), + select<&T::style>("style", "Style"), + select<&T::cap>("cap", "Cap"), + select<&T::join>("join", "Join")); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::Brush; + using namespace renderive::web::gallery_adminive; + return object("brush", "Brush", + color<&T::color>("color", "Color"), + select<&T::style>("style", "Style")); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::Font; + using namespace renderive::web::gallery_adminive; + return object("font", "Font", + number<&T::size>("size", "Size"), + number<&T::weight>("weight", "Weight"), + boolean<&T::italic>("italic", "Italic")); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::Axis_Properties; + using B = renderive::Axis_Base_Properties; + using namespace renderive::web::gallery_adminive; + return object("axis_properties", "Axis", + select<&B::orientation>("orientation", "Orientation"), + number<&B::tick_length>("tick_length", "Tick length"), + number<&B::sub_tick_length>("sub_tick_length", "Sub tick length"), + color<&B::color>("color", "Color"), + select<&B::locale>("locale", "Decimal separator"), + editable<&B::unit_text>("unit_text", "Unit text", Field_Control::text), + editable<&B::unit_text_font>("unit_text_font", "Unit font", Field_Control::automatic), + editable<&B::unit_text_pen>("unit_text_pen", "Unit pen", Field_Control::automatic), + editable<&B::unit_text_background_brush>("unit_text_background_brush", "Unit background", Field_Control::automatic), + number<&B::label_rotation_degrees>("label_rotation_degrees", "Label rotation"), + editable<&T::coordinates>("coordinates", "Coordinates", Field_Control::automatic), + number<&T::precision>("precision", "Precision"), + boolean<&T::wheel>("wheel", "Wheel zoom"), + boolean<&T::drag>("drag", "Drag pan")); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::Time_Axis_Properties; + using B = renderive::Axis_Base_Properties; + using namespace renderive::web::gallery_adminive; + return object("time_axis_properties", "Time axis", + select<&B::orientation>("orientation", "Orientation"), + number<&B::tick_length>("tick_length", "Tick length"), + number<&B::sub_tick_length>("sub_tick_length", "Sub tick length"), + color<&B::color>("color", "Color"), + editable<&B::unit_text>("unit_text", "Unit text", Field_Control::text), + editable<&B::unit_text_font>("unit_text_font", "Unit font", Field_Control::automatic), + number<&T::visible_count>("visible_count", "Visible points"), + number<&T::tick_label_spacing_px>("tick_label_spacing_px", "Label spacing"), + editable<&T::format>("format", "Time format", Field_Control::text), + boolean<&T::newest_at_start>("newest_at_start", "Newest at start")); + } +}; + +#define RENDERIVE_PROPERTY_DESCRIPTOR(Type, Label, ...) \ + template <> struct Type_Descriptor { \ + static auto get() { using T = renderive::Type; using namespace renderive::web::gallery_adminive; \ + return object(#Type, Label, __VA_ARGS__); } } + +RENDERIVE_PROPERTY_DESCRIPTOR(Spectrum_Properties, "Spectrum", + number<&T::frequency_point_size>("frequency_point_size", "Frequency points"), + editable<&T::frequency_range>("frequency_range", "Frequency range", Field_Control::automatic), + number<&T::center_frequency>("center_frequency", "Center frequency"), + editable<&T::sweep_frequency_range>("sweep_frequency_range", "Sweep range", Field_Control::automatic), + boolean<&T::max_hold_visible>("max_hold_visible", "Max hold"), + boolean<&T::min_hold_visible>("min_hold_visible", "Min hold"), + boolean<&T::max_marker_visible>("max_marker_visible", "Max marker"), + boolean<&T::use_min_marker>("use_min_marker", "Use min marker"), + boolean<&T::sweep_region_visible>("sweep_region_visible", "Sweep region"), + boolean<&T::visible_range_only>("visible_range_only", "Visible range only"), + select<&T::interpolation_mode>("interpolation_mode", "Interpolation"), + editable<&T::max_brush>("max_brush", "Max brush", Field_Control::automatic), + editable<&T::current_brush>("current_brush", "Current brush", Field_Control::automatic), + editable<&T::min_brush>("min_brush", "Min brush", Field_Control::automatic), + editable<&T::max_pen>("max_pen", "Max pen", Field_Control::automatic), + editable<&T::current_pen>("current_pen", "Current pen", Field_Control::automatic), + editable<&T::min_pen>("min_pen", "Min pen", Field_Control::automatic), + editable<&T::selected_marker_pen>("selected_marker_pen", "Selected marker pen", Field_Control::automatic), + editable<&T::marker_pen>("marker_pen", "Marker pen", Field_Control::automatic), + editable<&T::middle_frequency_pen>("middle_frequency_pen", "Middle frequency pen", Field_Control::automatic), + editable<&T::sweep_region_brush>("sweep_region_brush", "Sweep region brush", Field_Control::automatic), + boolean<&T::tooltip_enabled>("tooltip_enabled", "Tooltip"), + editable<&T::tooltip_font>("tooltip_font", "Tooltip font", Field_Control::automatic), + editable<&T::tooltip_text_pen>("tooltip_text_pen", "Tooltip text pen", Field_Control::automatic), + editable<&T::tooltip_background_brush>("tooltip_background_brush", "Tooltip background", Field_Control::automatic)); + +RENDERIVE_PROPERTY_DESCRIPTOR(Waterfall_Properties, "Waterfall", + editable<&T::frequency_range>("frequency_range", "Frequency range", Field_Control::automatic), + editable<&T::power_range>("power_range", "Power range", Field_Control::automatic), + number<&T::frequency_bin_count>("frequency_bin_count", "Frequency bins"), + boolean<&T::visible_range_only>("visible_range_only", "Visible range only"), + select<&T::interpolation_mode>("interpolation_mode", "Interpolation"), + select<&T::color_map>("color_map", "Color map"), + boolean<&T::tooltip_enabled>("tooltip_enabled", "Tooltip"), + editable<&T::tooltip_font>("tooltip_font", "Tooltip font", Field_Control::automatic), + editable<&T::tooltip_text_pen>("tooltip_text_pen", "Tooltip text pen", Field_Control::automatic), + editable<&T::tooltip_background_brush>("tooltip_background_brush", "Tooltip background", Field_Control::automatic)); + +RENDERIVE_PROPERTY_DESCRIPTOR(Afterglow_Properties, "Afterglow", + editable<&T::frequency_range>("frequency_range", "Frequency range", Field_Control::automatic), + editable<&T::power_range>("power_range", "Power range", Field_Control::automatic), + number<&T::frequency_point_size>("frequency_point_size", "Frequency points"), + number<&T::power_point_size>("power_point_size", "Power points"), + boolean<&T::interpolate>("interpolate", "Interpolate power"), + number<&T::attenuation_rate>("attenuation_rate", "Attenuation"), + select<&T::color_map>("color_map", "Color map")); + +RENDERIVE_PROPERTY_DESCRIPTOR(Sweep_Spectrum_Properties, "Sweep spectrum", + editable<&T::frequency_range>("frequency_range", "Frequency range", Field_Control::automatic), + number<&T::bins_per_block>("bins_per_block", "Bins per block"), + number<&T::block_count>("block_count", "Block count"), + editable<&T::pen>("pen", "Sweep pen", Field_Control::automatic), + editable<&T::current_frequency_pen>("current_frequency_pen", "Current frequency pen", Field_Control::automatic), + boolean<&T::visible_range_only>("visible_range_only", "Visible range only"), + select<&T::interpolation_mode>("interpolation_mode", "Interpolation")); + +RENDERIVE_PROPERTY_DESCRIPTOR(Frequency_Trace_Properties, "Frequency trace", + editable<&T::pen>("pen", "Trace pen", Field_Control::automatic)); + +RENDERIVE_PROPERTY_DESCRIPTOR(Selection_Rectangle_Overlay_Properties, "Selection overlay", + editable<&T::label_font>("label_font", "Label font", Field_Control::automatic), + editable<&T::label_pen>("label_pen", "Label pen", Field_Control::automatic), + editable<&T::selection_brush>("selection_brush", "Selection brush", Field_Control::automatic), + editable<&T::selection_border_pen>("selection_border_pen", "Selection border", Field_Control::automatic)); + +RENDERIVE_PROPERTY_DESCRIPTOR(Constellation_Diagram_Properties, "Constellation diagram", + editable<&T::i_range>("i_range", "I range", Field_Control::automatic), + editable<&T::q_range>("q_range", "Q range", Field_Control::automatic), + color<&T::point_color>("point_color", "Point color"), + color<&T::anchor_color>("anchor_color", "Anchor color"), + number<&T::point_lifetime_ms>("point_lifetime_ms", "Point lifetime"), + select<&T::type>("type", "Constellation"), + number<&T::phase_offset_radians>("phase_offset_radians", "Phase offset")); + +#undef RENDERIVE_PROPERTY_DESCRIPTOR + +#define RENDERIVE_OBJECT_ADAPTER(GalleryType, PropertiesType) \ + template <> struct Object_Adapter { \ + using object_type = renderive::web::GalleryType; \ + using model_type = renderive::PropertiesType; \ + static model_type snapshot(const object_type& value) { return value.adminive_snapshot(); } \ + static void commit(object_type& target, model_type value) { target.adminive_commit(std::move(value)); } \ + } + +RENDERIVE_OBJECT_ADAPTER(Gallery_Spectrum, Spectrum_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Waterfall, Waterfall_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Afterglow, Afterglow_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Sweep_Spectrum, Sweep_Spectrum_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Frequency_Trace, Frequency_Trace_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Selection_Rectangle_Overlay, Selection_Rectangle_Overlay_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Constellation_Diagram, Constellation_Diagram_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Axis, Axis_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Frequency_Axis, Axis_Properties); +RENDERIVE_OBJECT_ADAPTER(Gallery_Time_Axis, Time_Axis_Properties); + +#undef RENDERIVE_OBJECT_ADAPTER + +} // namespace adminive diff --git a/web_server/app/Gallery_Session_Control.cpp b/web_server/app/Gallery_Session_Control.cpp new file mode 100644 index 0000000..b62ce0c --- /dev/null +++ b/web_server/app/Gallery_Session_Control.cpp @@ -0,0 +1,47 @@ +#include "Gallery_Session_Control.h" + +namespace renderive::web { + +Gallery_Session_Control_Model Gallery_Session_Control::adminive_snapshot() const { + Gallery_Session_Control_Model result; + result.background_color = plot_.background_color(); + if (const auto overlay = plot_.performance_overlay()) + result.performance_overlay = overlay->enabled(); + result.renderable_visible = renderable_.is_visible(); + result.cache_mode = renderable_.get_cache_mode(); + result.object_name = renderable_.object_name(); + result.max_render_fps = plot_.max_render_fps(); + result.frequency_limit_enabled = result.max_render_fps > 0.0; + if (!result.frequency_limit_enabled) + result.max_render_fps = 30.0; + result.feedback = feedback_; + return result; +} + +void Gallery_Session_Control::adminive_commit(Gallery_Session_Control_Model model) { + const auto current = adminive_snapshot(); + if (model.frequency_limit_enabled != current.frequency_limit_enabled) { + if (model.frequency_limit_enabled) + plot_.set_max_render_fps(model.max_render_fps); + else + plot_.clear_max_render_fps(); + } else if (model.frequency_limit_enabled && + model.max_render_fps != current.max_render_fps) { + plot_.set_max_render_fps(model.max_render_fps); + } + if (model.background_color != current.background_color) + plot_.set_background_color(model.background_color); + if (model.performance_overlay != current.performance_overlay) + set_performance_overlay_enabled(plot_, model.performance_overlay); + if (model.renderable_visible != current.renderable_visible) + renderable_.set_visible(model.renderable_visible); + if (model.cache_mode != current.cache_mode) + renderable_.set_cache_mode(model.cache_mode); + if (model.object_name != current.object_name) + renderable_.set_object_name(std::move(model.object_name)); + if (model.feedback != current.feedback) + feedback_ = model.feedback; + plot_.notify_model_dirty(); +} + +} // namespace renderive::web diff --git a/web_server/app/Gallery_Session_Control.h b/web_server/app/Gallery_Session_Control.h new file mode 100644 index 0000000..d73e6e0 --- /dev/null +++ b/web_server/app/Gallery_Session_Control.h @@ -0,0 +1,57 @@ +#pragma once + +#include "render_2D/plot/Plot_Core.h" +#include "render_2D/plottable/Performance_Overlay.h" +#include "render_2D/renderable/Renderable.h" + +#include +#include + +namespace adminive { +template +struct Object_Adapter; +} + +namespace renderive::web { + +struct Gallery_Feedback_Policy { + bool enabled{true}; + bool pixel{}; + bool presentation{true}; + bool manual{}; + double manual_fps{60.0}; + + friend bool operator==(const Gallery_Feedback_Policy&, + const Gallery_Feedback_Policy&) = default; +}; + +struct Gallery_Session_Control_Model { + Color background_color{7, 17, 31, 255}; + bool performance_overlay{}; + bool renderable_visible{true}; + Renderable_Cache_Mode cache_mode{Renderable_Cache_Mode::Local_Pixel}; + std::string object_name{"Gallery_Renderable"}; + bool frequency_limit_enabled{true}; + double max_render_fps{30.0}; + Gallery_Feedback_Policy feedback; +}; + +class Gallery_Session_Control final { +public: + Gallery_Session_Control(Plot_Core& plot, Renderable& renderable, + Gallery_Feedback_Policy& feedback) noexcept + : plot_(plot), renderable_(renderable), feedback_(feedback) {} + +private: + [[nodiscard]] Gallery_Session_Control_Model adminive_snapshot() const; + void adminive_commit(Gallery_Session_Control_Model model); + + Plot_Core& plot_; + Renderable& renderable_; + Gallery_Feedback_Policy& feedback_; + + template + friend struct ::adminive::Object_Adapter; +}; + +} // namespace renderive::web diff --git a/web_server/app/Gallery_Session_Control_Adminive.h b/web_server/app/Gallery_Session_Control_Adminive.h new file mode 100644 index 0000000..5048711 --- /dev/null +++ b/web_server/app/Gallery_Session_Control_Adminive.h @@ -0,0 +1,49 @@ +#pragma once + +#include "Gallery_Session_Control.h" +#include "Gallery_Renderables_Adminive.h" + +namespace adminive { + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::web::Gallery_Feedback_Policy; + using namespace renderive::web::gallery_adminive; + return object("feedback_policy", "Consumer feedback", + boolean<&T::enabled>("enabled", "Enabled"), + boolean<&T::pixel>("pixel", "Pixel feedback"), + boolean<&T::presentation>("presentation", "Presentation feedback"), + boolean<&T::manual>("manual", "Manual feedback"), + number<&T::manual_fps>("manual_fps", "Manual FPS")); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::web::Gallery_Session_Control_Model; + using namespace renderive::web::gallery_adminive; + return object("gallery_session", "Scene", + color<&T::background_color>("background_color", "Background"), + boolean<&T::performance_overlay>("performance_overlay", "Performance overlay"), + boolean<&T::renderable_visible>("renderable_visible", "Renderable visible"), + select<&T::cache_mode>("cache_mode", "Cache mode"), + editable<&T::object_name>("object_name", "Object name", Field_Control::text), + boolean<&T::frequency_limit_enabled>("frequency_limit_enabled", "Frequency limit"), + number<&T::max_render_fps>("max_render_fps", "Maximum render FPS"), + editable<&T::feedback>("feedback", "Consumer feedback", Field_Control::automatic)); + } +}; + +template <> +struct Object_Adapter { + using object_type = renderive::web::Gallery_Session_Control; + using model_type = renderive::web::Gallery_Session_Control_Model; + static model_type snapshot(const object_type& value) { return value.adminive_snapshot(); } + static void commit(object_type& target, model_type value) { + target.adminive_commit(std::move(value)); + } +}; + +} // namespace adminive diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 66557ff..a1317d7 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -69,9 +69,10 @@ nlohmann::json invoke_action(Gallery_Plot_Session& session, std::string_view act Gallery_Request_Kind::Action, request.dump()))); } -nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patch) { +nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patch, + std::string_view target = "scene") { const nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"}, - {"patch", std::move(patch)}}; + {"target", target}, {"patch", std::move(patch)}}; return response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, request.dump()))); } @@ -236,9 +237,9 @@ TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) { EXPECT_TRUE(mode.contains("observer_visible")); EXPECT_TRUE(mode.contains("accent")); } - const auto state = Gallery_Protocol::default_state("spectrum", Gallery_Frame_Mode::Manual); - const auto contract = parse_json(Gallery_Protocol::case_json( - "spectrum", state, "{}", {}, Gallery_Frame_Mode::Manual)); + Gallery_Plot_Session session; + const auto contract = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); const auto action = std::find_if(contract.at("actions").at("data").begin(), contract.at("actions").at("data").end(), [](const auto& item) { return item.at("id") == "mode_render"; }); @@ -246,201 +247,47 @@ TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) { EXPECT_TRUE(action->at("request_frame")); } -TEST(RenderiveWebGallery, BackendMenuMapsRetainedControlApis) { - constexpr std::array cases{ - "axis_lab", "spectrum", "afterglow", "sweep_spectrum", - "waterfall", "frequency_trace", "selection_overlay", "constellation" - }; - std::string api_text; - constexpr Gallery_Frame_Mode modes[]{Gallery_Frame_Mode::Manual, - Gallery_Frame_Mode::Low_Latency, - Gallery_Frame_Mode::Playback}; - for (const auto case_id : cases) { - for (const auto mode : modes) { - const auto contract = parse_json(Gallery_Protocol::case_json( - case_id, Gallery_Protocol::default_state(case_id, mode), "{}", {}, mode)); - for (const auto& control : contract.at("controls").at("descriptor").at("fields")) - api_text += control.at("presentation").at("description").get() + '\n'; - for (const auto& action : contract.at("actions").at("data")) - api_text += action.at("api").get() + '\n'; - } - } - constexpr std::array required{ - "Plot_Core::set_background_color", "Plot_Core::set_max_render_fps", - "Plot_Core::activate_view", "Plot_Core::remove_renderable", - "Renderable::set_visible", "Renderable::set_cache_mode", "Renderable::set_object_name", - "Axis::set<&Axis_Base_Properties::x>", "Axis::set<&Axis_Base_Properties::y>", "Axis::set<&Axis_Base_Properties::orientation>", - "Axis::set<&Axis_Base_Properties::pixel_length>", "Axis::set<&Axis_Base_Properties::locale>", "Axis::set<&Axis_Base_Properties::unit_text_font>", - "Axis::set<&Axis_Properties::coordinates>", "Axis::set<&Axis_Properties::precision>", "Axis::set<&Axis_Properties::wheel>", - "Axis::set<&Axis_Properties::drag>", "Time_Axis::set<&Time_Axis_Properties::visible_count>", "Time_Axis::set<&Time_Axis_Properties::format>", - "Time_Axis::append_time", "Spectrum::set<&Properties::frequency_point_size>", - "Spectrum::set<&Properties::interpolation_mode>", "Spectrum::set<&Properties::current_pen>", "Spectrum::set<&Properties::max_brush>", - "Spectrum::update_samples", "Spectrum::power_at", "Spectrum::add_custom_marker", - "Spectrum::set_marker_frequency", "Waterfall::set<&Properties::frequency_range>", - "Waterfall::set<&Properties::interpolation_mode>", "Waterfall::append_row", "Waterfall::set<&Properties::color_map>", - "Afterglow::set<&Properties::attenuation_rate>", "Afterglow::append_spectrum", - "Sweep_Spectrum::set<&Properties::bins_per_block>", "Sweep_Spectrum::append_block", - "Frequency_Trace::set<&Properties::pen>", "Selection_Rectangle_Overlay::set<&Properties::selection_brush>", - "Selection_Rectangle_Overlay::clear_selected_regions", - "Constellation_Diagram::set<&Properties::type>", "Constellation_Diagram::fit_square_to_axes" - }; - for (const auto api : required) - EXPECT_NE(api_text.find(api), std::string::npos) << api; - EXPECT_NE(api_text.find("Plot_Core::refresh_manual_frame"), std::string::npos); - EXPECT_NE(api_text.find("Plot_Core::prepare_frame"), std::string::npos); - EXPECT_NE(api_text.find("Plot_Core::render_prepared_frame"), std::string::npos); - EXPECT_NE(api_text.find("Flow_Refresh_Strategy"), std::string::npos); -} - -TEST(RenderiveWebGallery, BackendControlSchemaValidatesEveryPatch) { - const Gallery_State current = Gallery_Protocol::default_state("spectrum"); - const auto accepted = Gallery_Protocol::apply_patch( - "spectrum", current, - R"({"category":"event","type":"gallery_patch","patch":{"current_pen":"#ff8844","frequency_point_size":1024,"line_interpolation":"Cubic_Value","max_render_fps":1000}})"); - ASSERT_TRUE(accepted.candidate.has_value()); - EXPECT_EQ(std::get(gallery_property_value(*accepted.candidate, "current_pen")), "#ff8844"); - EXPECT_DOUBLE_EQ(std::get(gallery_property_value(*accepted.candidate, "frequency_point_size")), 1024); - EXPECT_DOUBLE_EQ(std::get(gallery_property_value(*accepted.candidate, "max_render_fps")), 1000); - - const auto excessive_fps = Gallery_Protocol::apply_patch( - "spectrum", current, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000001}})"); - EXPECT_FALSE(excessive_fps.candidate.has_value()); - EXPECT_TRUE(parse_json(excessive_fps.response_json).at("field_errors").contains("max_render_fps")); - - const auto invalid_color = Gallery_Protocol::apply_patch( - "spectrum", current, - R"({"category":"event","type":"gallery_patch","patch":{"current_pen":"red"}})"); - EXPECT_FALSE(invalid_color.candidate.has_value()); - EXPECT_TRUE(parse_json(invalid_color.response_json).at("field_errors").contains("current_pen")); - - const auto foreign_control = Gallery_Protocol::apply_patch( - "spectrum", current, - R"({"category":"event","type":"gallery_patch","patch":{"image_interpolation":"Bicubic"}})"); - EXPECT_FALSE(foreign_control.candidate.has_value()); - EXPECT_TRUE(parse_json(foreign_control.response_json).at("field_errors").contains("image_interpolation")); -} - -TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract) { - constexpr std::array cases{ - "axis_lab", "spectrum", "afterglow", "sweep_spectrum", - "waterfall", "frequency_trace", "selection_overlay", "constellation" - }; - struct Mode { - std::string_view id; - Gallery_Frame_Mode value; - }; - constexpr std::array modes{ - Mode{"manual", Gallery_Frame_Mode::Manual}, - Mode{"low_latency", Gallery_Frame_Mode::Low_Latency}, - Mode{"playback", Gallery_Frame_Mode::Playback} +TEST(RenderiveWebGallery, AdminiveResourcesPatchTheRealRenderableState) { + Gallery_Plot_Session session; + auto response = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); + ASSERT_EQ(response.at("type"), "case_state"); + const auto find_resource = [](const nlohmann::json& value, std::string_view target) + -> const nlohmann::json& { + const auto& resources = value.at("controls").at("resources"); + const auto found = std::find_if(resources.begin(), resources.end(), + [target](const auto& resource) { + return resource.at("target").template get() == target; + }); + if (found == resources.end()) + throw std::out_of_range("missing gallery resource"); + return *found; }; - std::size_t validated_control_instances{}; - for (const auto case_id : cases) { - for (const auto mode : modes) { - const auto defaults = Gallery_Protocol::default_state(case_id, mode.value); - const auto contract = parse_json(Gallery_Protocol::case_json( - case_id, defaults, "{}", {}, mode.value)); - const auto& resource = contract.at("controls"); - const auto& controls = resource.at("descriptor").at("fields"); - const auto& data = resource.at("data"); - EXPECT_EQ(controls.size(), gallery_property_count(defaults)) << case_id << '/' << mode.id; - std::set ids; - for (const auto& control : controls) { - ++validated_control_instances; - const std::string id = control.at("name"); - const auto& presentation = control.at("presentation"); - SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode.id) + "/" + id); - EXPECT_TRUE(ids.insert(id).second); - EXPECT_TRUE(control.at("editable").get()); - EXPECT_FALSE(presentation.at("description").get().empty()); + const auto& spectrum = find_resource(response, "spectrum"); + EXPECT_EQ(spectrum.at("descriptor").at("name"), "Spectrum_Properties"); + EXPECT_TRUE(spectrum.at("data").at("frequency_range").is_object()); + EXPECT_EQ(spectrum.at("data").at("frequency_point_size"), 512); - const std::string input = presentation.at("control"); - const auto& current_value = data.at(id); - const auto apply = [&](const nlohmann::json& value) { - nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"}, - {"patch", {{id, value}}}}; - return Gallery_Protocol::apply_patch(case_id, defaults, request.dump(), - mode.value); - }; - const auto expect_rejected = [&](const nlohmann::json& value) { - const auto result = apply(value); - EXPECT_FALSE(result.candidate.has_value()); - EXPECT_TRUE(parse_json(result.response_json).at("field_errors").contains(id)); - }; + response = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Patch, + R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"frequency_point_size":1024,"frequency_range":{"origin":90000000},"current_pen":{"color":"#ff8844"},"interpolation_mode":"Cubic_Value"}})"))); + ASSERT_EQ(response.at("type"), "case_state"); + const auto& updated = find_resource(response, "spectrum").at("data"); + EXPECT_EQ(updated.at("frequency_point_size"), 1024); + EXPECT_EQ(updated.at("frequency_range").at("origin"), 90'000'000); + EXPECT_EQ(updated.at("current_pen").at("color"), "#ff8844"); + EXPECT_EQ(updated.at("interpolation_mode"), "Cubic_Value"); - if (input == "boolean") { - const bool alternative = !current_value.get(); - const auto accepted = apply(alternative); - ASSERT_TRUE(accepted.candidate.has_value()); - EXPECT_EQ(std::get(gallery_property_value(*accepted.candidate, id)), alternative); - expect_rejected(1); - } else if (input == "number") { - const double minimum = control.at("minimum"); - const double maximum = control.at("maximum"); - const double current = current_value; - const double step = std::max(control.at("multiple_of").get(), 1e-9); - double alternative = current + step; - if (alternative > maximum) - alternative = current - step; - if (alternative < minimum || alternative == current) - alternative = current == minimum ? maximum : minimum; - const bool integer = control.at("value_type") == "integer"; - if (integer) - alternative = std::round(alternative); + const auto invalid = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Patch, + R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"current_pen":{"color":"not-a-color"}}})"))); + EXPECT_EQ(invalid.at("type"), "error"); - const nlohmann::json alternative_value = integer ? - nlohmann::json(static_cast(std::llround(alternative))) : - nlohmann::json(alternative); - const auto accepted = apply(alternative_value); - ASSERT_TRUE(accepted.candidate.has_value()); - EXPECT_DOUBLE_EQ(std::get(gallery_property_value(*accepted.candidate, id)), - alternative); - EXPECT_TRUE(apply(integer ? nlohmann::json(static_cast(minimum)) : nlohmann::json(minimum)).candidate.has_value()); - EXPECT_TRUE(apply(integer ? nlohmann::json(static_cast(maximum)) : nlohmann::json(maximum)).candidate.has_value()); - expect_rejected(minimum - std::max(1.0, std::abs(minimum) * 0.1 + 1.0)); - expect_rejected(maximum + std::max(1.0, std::abs(maximum) * 0.1 + 1.0)); - expect_rejected("not-a-number"); - if (integer && maximum - minimum >= 1.0) - expect_rejected(std::clamp(std::floor(current) + 0.5, - minimum + 0.5, maximum - 0.5)); - } else if (input == "select") { - const auto& options = presentation.at("options"); - ASSERT_FALSE(options.empty()); - const std::string current = current_value; - const auto alternative = std::find_if( - options.begin(), options.end(), [¤t](const auto& option) { - return option.at("value").template get() != current; - }); - ASSERT_NE(alternative, options.end()); - const std::string value = alternative->at("value").get(); - const auto accepted = apply(value); - ASSERT_TRUE(accepted.candidate.has_value()); - EXPECT_EQ(std::get(gallery_property_value(*accepted.candidate, id)), value); - expect_rejected("__invalid_gallery_option__"); - expect_rejected(7); - } else if (input == "color") { - const std::string value = current_value == "#123456" ? - "#654321" : "#123456"; - const auto accepted = apply(value); - ASSERT_TRUE(accepted.candidate.has_value()); - EXPECT_EQ(std::get(gallery_property_value(*accepted.candidate, id)), value); - expect_rejected("red"); - expect_rejected(7); - } else { - ASSERT_EQ(input, "text"); - std::string value = current_value.get() + "_qa"; - const auto accepted = apply(value); - ASSERT_TRUE(accepted.candidate.has_value()); - EXPECT_EQ(std::get(gallery_property_value(*accepted.candidate, id)), value); - expect_rejected(std::string(161, 'x')); - expect_rejected(7); - } - } - } - } - EXPECT_GT(validated_control_instances, 180U); + const auto foreign = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Patch, + R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"visible_range_only":false}})"))); + EXPECT_EQ(foreign.at("type"), "error"); } TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode) { @@ -484,73 +331,6 @@ TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode) EXPECT_GT(dispatched_action_instances, 150U); } -TEST(RenderiveWebGallery, EveryPublishedControlAppliesToTheLiveCore2Scene) { - constexpr std::array cases{ - "axis_lab", "spectrum", "afterglow", "sweep_spectrum", - "waterfall", "frequency_trace", "selection_overlay", "constellation" - }; - constexpr std::array modes{"manual", "low_latency", "playback"}; - std::size_t applied_control_instances{}; - for (const auto case_id : cases) { - for (const auto mode : modes) { - Gallery_Plot_Session session; - auto state = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Open, open_message(case_id, mode)))); - ASSERT_EQ(state.at("type"), "case_state"); - const auto published_controls = state.at("controls").at("descriptor").at("fields"); - for (const auto& published : published_controls) { - ++applied_control_instances; - const std::string id = published.at("name"); - SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode) + "/" + id); - const std::string input = published.at("presentation").at("control"); - const auto& current_value = state.at("controls").at("data").at(id); - nlohmann::json alternative; - if (input == "boolean") { - alternative = !current_value.get(); - } else if (input == "number") { - const double minimum = published.at("minimum"); - const double maximum = published.at("maximum"); - const double current = current_value; - const double step = std::max(published.at("multiple_of").get(), 1e-9); - double value = current + step; - if (value > maximum) - value = current - step; - if (value < minimum || value == current) - value = current == minimum ? maximum : minimum; - const bool integer = published.at("value_type") == "integer"; - if (integer) - value = std::round(value); - alternative = integer ? nlohmann::json(static_cast(std::llround(value))) : nlohmann::json(value); - } else if (input == "select") { - const auto& options = published.at("presentation").at("options"); - const std::string current = current_value; - const auto selected = std::find_if( - options.begin(), options.end(), [¤t](const auto& option) { - return option.at("value").template get() != current; - }); - ASSERT_NE(selected, options.end()); - alternative = selected->at("value"); - } else if (input == "color") { - alternative = current_value == "#123456" ? "#654321" : "#123456"; - } else { - ASSERT_EQ(input, "text"); - alternative = current_value.get() + "_live"; - } - - nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"}, - {"patch", {{id, alternative}}}}; - state = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Patch, request.dump()))); - ASSERT_EQ(state.at("type"), "case_state") << state.dump(); - EXPECT_EQ(state.at("frame_mode").at("id").get(), mode); - EXPECT_EQ(state.at("controls").at("data").at(id), alternative); - EXPECT_TRUE(state.at("telemetry").contains("kernel_observer")); - } - } - } - EXPECT_GT(applied_control_instances, 180U); -} - TEST(RenderiveWebGallery, CanvasSpecificActionsProduceObservableStateChanges) { { Gallery_Plot_Session session; @@ -644,7 +424,9 @@ TEST(RenderiveWebGallery, CommonControlsAndViewLifecycleProduceObservableEffects "case_state"); const auto background_state = patch_controls(session, {{"background_color", "#123456"}}); - EXPECT_EQ(background_state.at("controls").at("data").at("background_color"), "#123456"); + ASSERT_EQ(background_state.at("type"), "case_state") << background_state.dump(); + EXPECT_EQ(background_state.at("controls").at("resources").at(0).at("data") + .at("background_color"), "#123456"); ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state"); const auto background_frame = session.handle(Frame_Request{}); ASSERT_TRUE(background_frame.has_value()); @@ -793,10 +575,14 @@ TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) { EXPECT_EQ(state.at("case").at("id").get(), std::string(case_id)) << case_id; EXPECT_EQ(state.at("frame_mode").at("id"), std::string(mode)) << case_id; - EXPECT_EQ(state.at("controls").at("descriptor").at("protocol"), - "adminive.resource") << case_id; - EXPECT_EQ(state.at("controls").at("view").at("protocol"), - "adminive.view") << case_id; + const auto& resources = state.at("controls").at("resources"); + ASSERT_FALSE(resources.empty()) << case_id; + for (const auto& resource : resources) { + EXPECT_EQ(resource.at("descriptor").at("protocol"), + "adminive.resource") << case_id; + EXPECT_EQ(resource.at("view").at("protocol"), + "adminive.view") << case_id; + } EXPECT_FALSE(session.handle(Viewport_Resize{{480, 280}}).has_value()) << case_id; const auto frame = wait_for_pixel_frame(session, 480, 280); ASSERT_TRUE(frame.has_value()) << case_id; @@ -836,15 +622,10 @@ TEST(RenderiveWebGallery, ActionsMutateLiveCore2ObjectsAndReturnTelemetry) { const auto patch = session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"line_interpolation":"Step_Left","max_hold_visible":true}})")); + R"({"category":"event","type":"gallery_patch","target":"spectrum","patch":{"interpolation_mode":"Step_Left","max_hold_visible":true}})")); ASSERT_TRUE(patch.has_value()); EXPECT_EQ(parse_json(patch->payload).at("type"), "case_state"); - const auto rebuild = session.handle(gallery_request( - Gallery_Request_Kind::Action, - R"({"category":"event","type":"gallery_action","action":"rebuild_renderable"})")); - ASSERT_TRUE(rebuild.has_value()); - EXPECT_EQ(parse_json(rebuild->payload).at("type"), "case_state"); } TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers) { @@ -882,7 +663,7 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); const auto high_frequency = low_latency.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000}})")); + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000000000}})")); ASSERT_TRUE(high_frequency.has_value()); ASSERT_EQ(parse_json(high_frequency->payload).at("type"), "case_state"); ASSERT_TRUE(low_latency.handle(Frame_Request{})); @@ -991,7 +772,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres "case_state"); ASSERT_EQ(response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000}})"))).at("type"), + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000}})"))).at("type"), "case_state"); ASSERT_TRUE(wait_for_condition([&session] { @@ -1034,7 +815,7 @@ TEST(RenderiveWebGallery, FrequencyLimitCannotBeBypassedByForegroundRescheduleEv const auto patched = patch_controls(session, { {"frequency_limit_enabled", true}, {"max_render_fps", 30.0}, - {"consumer_feedback_enabled", false} + {"feedback", {{"enabled", false}}} }); ASSERT_EQ(patched.at("type"), "case_state"); const auto& patched_observer = patched.at("telemetry").at("kernel_observer"); @@ -1073,7 +854,7 @@ TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundReschedule "case_state"); ASSERT_EQ(patch_controls(session, { {"frequency_limit_enabled", false}, - {"consumer_feedback_enabled", true} + {"feedback", {{"enabled", true}}} }).at("type"), "case_state"); auto observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, @@ -1112,7 +893,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback "case_state"); ASSERT_EQ(response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000}})"))).at("type"), + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":1000000000}})"))).at("type"), "case_state"); auto observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, @@ -1139,7 +920,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback EXPECT_EQ(observer.at("consumer_interval_ns"), 22'000'000); observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"consumer_feedback_enabled":false,"frequency_limit_enabled":false}})"))); + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"feedback":{"enabled":false},"frequency_limit_enabled":false}})"))); observer = observed.at("telemetry").at("kernel_observer"); EXPECT_FALSE(observer.at("frequency_limit_enabled").get()); EXPECT_FALSE(observer.at("consumer_feedback_enabled").get()); @@ -1150,7 +931,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback observer.at("limit_state") == "unlimited"); const auto rejected = response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"pixel_stream_fps":30}})"))); + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"pixel_stream_fps":30}})"))); EXPECT_EQ(rejected.at("type"), "error"); } @@ -1161,10 +942,8 @@ TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndMa "case_state"); ASSERT_EQ(patch_controls(session, { {"frequency_limit_enabled", false}, - {"consumer_feedback_enabled", true}, - {"consumer_pixel_feedback_enabled", true}, - {"consumer_presentation_feedback_enabled", false}, - {"consumer_manual_feedback_enabled", false} + {"feedback", {{"enabled", true}, {"pixel", true}, + {"presentation", false}, {"manual", false}}} }).at("type"), "case_state"); auto observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, @@ -1175,23 +954,21 @@ TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndMa EXPECT_EQ(observer.at("consumer_presentation_interval_ns"), 16'000'000); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 40'000'000); observed = patch_controls(session, { - {"consumer_pixel_feedback_enabled", false}, - {"consumer_presentation_feedback_enabled", true} + {"feedback", {{"pixel", false}, {"presentation", true}}} }); observer = observed.at("telemetry").at("kernel_observer"); EXPECT_EQ(observer.at("consumer_feedback_source"), "presentation"); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 16'000'000); observed = patch_controls(session, { - {"consumer_presentation_feedback_enabled", false}, - {"consumer_manual_feedback_enabled", true}, - {"consumer_manual_fps", 20.0} + {"feedback", {{"presentation", false}, {"manual", true}, + {"manual_fps", 20.0}}} }); observer = observed.at("telemetry").at("kernel_observer"); EXPECT_EQ(observer.at("consumer_feedback_source"), "manual"); EXPECT_EQ(observer.at("consumer_manual_interval_ns"), 50'000'000); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 50'000'000); observed = patch_controls(session, { - {"consumer_manual_feedback_enabled", false} + {"feedback", {{"manual", false}}} }); observer = observed.at("telemetry").at("kernel_observer"); EXPECT_EQ(observer.at("consumer_feedback_source"), "none"); @@ -1279,7 +1056,7 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); ASSERT_TRUE(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})"))); + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})"))); ASSERT_TRUE(wait_for_condition([&session] { return successful_render_count(session) >= 4; })); @@ -1319,7 +1096,7 @@ TEST(RenderiveWebGallery, ProductionLowLatencyStopsWhileHiddenAndResumes) { Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); ASSERT_TRUE(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})"))); + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})"))); ASSERT_TRUE(wait_for_condition([&session] { return successful_render_count(session) >= 3; })); @@ -1345,7 +1122,7 @@ TEST(RenderiveWebGallery, ProductionLowLatencyOnlyRendersActiveSessions) { Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))); ASSERT_TRUE(session->handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":60}})"))); + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":60}})"))); if (index >= 2) set_gallery_view_active(*session, false); sessions.push_back(std::move(session)); @@ -1386,7 +1163,7 @@ void expect_low_latency_canvas_to_change(std::string_view case_id) { ASSERT_FALSE(session.handle(Viewport_Resize{{240, 180}}).has_value()); ASSERT_TRUE(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})"))); + R"({"category":"event","type":"gallery_patch","target":"scene","patch":{"max_render_fps":120}})"))); std::this_thread::sleep_for(std::chrono::milliseconds(90)); const auto first = session.handle(Frame_Request{}); @@ -1439,17 +1216,18 @@ TEST(RenderiveWebGallery, ConstellationLowLatencyPixelsChange) { expect_low_latency_canvas_to_change("constellation"); } -TEST(RenderiveWebGallery, PropertyChangesRebuildAndAllImageModesRender) { +TEST(RenderiveWebGallery, DirectPropertyChangesAndAllImageModesRender) { Gallery_Plot_Session constellation; ASSERT_TRUE(constellation.handle(gallery_request( Gallery_Request_Kind::Open, open_message("constellation", "low_latency")))); - const auto rebuilt = constellation.handle(gallery_request( + const auto patched = constellation.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"constellation_type":"PSK16","phase_offset":0.75}})")); - ASSERT_TRUE(rebuilt.has_value()); - const auto rebuilt_json = parse_json(rebuilt->payload); - EXPECT_EQ(rebuilt_json.at("controls").at("data").at("constellation_type"), "PSK16"); + R"({"category":"event","type":"gallery_patch","target":"constellation","patch":{"type":"PSK16","phase_offset_radians":0.75}})")); + ASSERT_TRUE(patched.has_value()); + const auto patched_json = parse_json(patched->payload); + EXPECT_EQ(patched_json.at("controls").at("resources").at(3).at("data").at("type"), + "PSK16"); ASSERT_TRUE(constellation.handle(Frame_Request{}).has_value()); Gallery_Plot_Session waterfall; @@ -1458,7 +1236,7 @@ TEST(RenderiveWebGallery, PropertyChangesRebuildAndAllImageModesRender) { open_message("waterfall", "low_latency")))); for (const std::string_view mode : {"Nearest", "Bilinear", "Bicubic"}) { const std::string patch = - std::string(R"({"category":"event","type":"gallery_patch","patch":{"image_interpolation":")") + + std::string(R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"interpolation_mode":")") + std::string(mode) + R"("}})"; ASSERT_TRUE(waterfall.handle(gallery_request(Gallery_Request_Kind::Patch, patch)).has_value()) << mode; @@ -1466,7 +1244,7 @@ TEST(RenderiveWebGallery, PropertyChangesRebuildAndAllImageModesRender) { } const auto palette = waterfall.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"color_map":"Ember"}})")); + R"({"category":"event","type":"gallery_patch","target":"waterfall","patch":{"color_map":"Ember"}})")); ASSERT_TRUE(palette.has_value()); ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value()); const auto waterfall_observer = waterfall.handle(gallery_request( diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 92bc3e1..8ccac67 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -59,24 +59,43 @@ function grouped(items) { return groups; } function adminiveControls(resource) { - const data = resource?.data || {}; - const group = resource?.view?.title || resource?.descriptor?.label || "控件属性"; - return (resource?.descriptor?.fields || []).filter(field => field.editable).map(field => { - const presentation = field.presentation || {}; - return { - id: field.name, - label: presentation.label || field.name, - api: presentation.description || "", - description: presentation.description || "", - group, - input: presentation.control || "text", - minimum: field.minimum, - maximum: field.maximum, - step: field.multiple_of, - options: presentation.options || [], - value: data[field.name] - }; - }); + const resources = resource?.resources || (resource ? [resource] : []); + const controls = []; + const visit = (fields, data, target, group, path = [], labels = []) => { + for (const field of fields || []) { + const presentation = field.presentation || {}; + const fieldPath = [...path, field.name]; + const fieldLabels = [...labels, presentation.label || field.name]; + if (field.children?.length) { + visit(field.children, data?.[field.name], target, group, fieldPath, fieldLabels); + continue; + } + if (!field.editable) continue; + controls.push({ + id: fieldPath.join("."), + target, + path: fieldPath, + label: fieldLabels.join(" / "), + api: presentation.description || fieldPath.join("."), + description: presentation.description || "", + group, + input: presentation.control === "automatic" ? "text" : presentation.control || "text", + minimum: field.minimum, + maximum: field.maximum, + step: field.multiple_of, + options: presentation.options || [], + value: data?.[field.name] + }); + } + }; + for (const item of resources) { + const group = item?.view?.title || item?.descriptor?.label || "控件属性"; + visit(item?.descriptor?.fields, item?.data || {}, item?.target, group); + } + return controls; +} +function nestedPatch(path, value) { + return [...path].reverse().reduce((result, key) => ({[key]: result}), value); } function flatten(value, prefix = "", result = []) { if (value !== null && typeof value === "object" && !Array.isArray(value)) { @@ -631,7 +650,7 @@ function renderControl(item) { } input.className = "control-input"; input.title = item.description || item.api; const submit = value => { - card.send("gallery_patch", {patch: {[item.id]: value}}); + card.send("gallery_patch", {target: item.target, patch: nestedPatch(item.path, value)}); elements.menuStatus.textContent = `提交 ${item.api} · 等待后端回读`; }; if (item.input === "number") {