#include "Gallery_Plots_2D.hpp" #include "Renderable_Adapter.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace aethera::web { namespace { using namespace render_2d; using Scene_2D = Impl; using Frequency_Axis_Object = Impl; using Numeric_Axis_Object = Impl; using Time_Axis_Object = Impl; using Selection_Object = Impl; template class Scene_View_Model final : public Plot::Scene_View { public: struct Data_Generator { nlohmann::json schema; std::function generate; explicit operator bool() const noexcept { return static_cast(generate); } }; Scene_View_Model(std::vector> value_descriptors, std::function value_update, Data_Generator value_data_generator, Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)), update_scene(std::move(value_update)), data_generator(std::move(value_data_generator)), objects(std::move(owned_objects)...) {} nlohmann::json schema() const override { nlohmann::json components = nlohmann::json::array(); for (const auto& descriptor : descriptors) components.push_back(descriptor->schema()); return {{"protocol", "aethera.plot.inspector"}, {"version", 2}, {"components", std::move(components)}}; } nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override { const auto found = std::ranges::find_if(descriptors, [&](const auto& item) { return item->id() == component; }); if (found == descriptors.end()) return {{"success", false}, {"error", "unknown component"}}; auto result = (*found)->write_prop(key, value); result["component"] = component; return result; } nlohmann::json data_generator_schema() const override { if (!data_generator) return nullptr; return data_generator.schema; } nlohmann::json generate_data(const nlohmann::json& input) override { if (!data_generator) return {{"success", false}, {"error", "this plot has no raw data input"}}; auto result = data_generator.generate(input); if (result.value("success", false)) generated_data_active = true; return result; } void update(const Plot_Render_Tick& request) override { update_scene(request, !generated_data_active); } private: std::vector> descriptors; std::function update_scene; Data_Generator data_generator; bool generated_data_active{}; /* true 后保留用户生成的数据,不再用演示输入覆盖;viewport 更新仍持续。 */ std::tuple objects; }; template using Prop_Field = detail::Prop_Field; template using State_Field = detail::State_Field; template std::unique_ptr make_renderable_component( std::string id, std::string label, std::string kind, Object& object) { using Definition = typename Object::Attached_Object; using Tag = typename Definition::Base_Tag; using State = typename Definition::State; using Adapter = detail::Renderable_Adapter, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field>; return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object}); } template std::unique_ptr make_scene_component(Scene_Object& scene) { using Definition = typename Scene_Object::Attached_Object; using Prop = typename Definition::Prop; using Adapter = detail::Renderable_Adapter, detail::Prop_Field<&Prop::background, "background", "Scene clear color.">, detail::Prop_Field<&Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field, detail::State_Field>; return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene}); } template std::unique_ptr make_axis_component( std::string id, std::string label, Axis_Object& axis) { return make_renderable_component, Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">, Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">, Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">, Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">, Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">, Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">, Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">, Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">, Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">, Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">, Prop_Field<&Numeric_Axis::Prop::coordinate_range, "coordinate_range", "Visible coordinate range.">, Prop_Field<&Numeric_Axis::Prop::precision, "precision", "Maximum decimal precision.">, Prop_Field<&Numeric_Axis::Prop::locale, "locale", "Numeric label locale.">, Prop_Field<&Numeric_Axis::Prop::wheel_enabled, "wheel_enabled", "Allows wheel zoom.">, Prop_Field<&Numeric_Axis::Prop::drag_enabled, "drag_enabled", "Allows pointer drag panning.">>( std::move(id), std::move(label), "axis", axis); } template <> std::unique_ptr make_axis_component( std::string id, std::string label, Time_Axis_Object& axis) { return make_renderable_component, Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">, Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">, Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">, Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">, Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">, Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">, Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">, Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">, Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">, Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">, Prop_Field<&Time_Axis::Prop::visible_count, "visible_count", "Maximum visible time samples.">, Prop_Field<&Time_Axis::Prop::tick_label_spacing_px, "tick_label_spacing_px", "Spacing between time labels.">, Prop_Field<&Time_Axis::Prop::estimated_label_width_px, "estimated_label_width_px", "Estimated time label width.">, Prop_Field<&Time_Axis::Prop::format, "format", "Time label format.">, Prop_Field<&Time_Axis::Prop::newest_at_start, "newest_at_start", "Places the newest time at the range origin.">, State_Field>( std::move(id), std::move(label), "axis", axis); } using Json = nlohmann::json; Json generator_number_field(std::string key, std::string label, std::string description, double value, double minimum, double maximum, double step = 0.01) { return {{"key", std::move(key)}, {"label", std::move(label)}, {"description", std::move(description)}, {"editor", "number"}, {"editable", true}, {"value", value}, {"minimum", minimum}, {"maximum", maximum}, {"step", step}}; } Json generator_integer_field(std::string key, std::string label, std::string description, std::size_t value, std::size_t maximum = 1'000'000) { auto field = generator_number_field(std::move(key), std::move(label), std::move(description), static_cast(value), 1.0, static_cast(maximum), 1.0); field["editor"] = "integer"; return field; } double generator_number(const Json& input, std::string_view key) { const auto& value = input.at(std::string(key)); if (!value.is_number()) throw std::invalid_argument(std::string(key) + " must be a number"); const auto result = value.get(); if (!std::isfinite(result)) throw std::invalid_argument(std::string(key) + " must be finite"); return result; } std::size_t generator_count(const Json& input, std::string_view key, std::size_t maximum = 1'000'000) { const auto value = generator_number(input, key); if (value < 1.0 || value > static_cast(maximum) || std::floor(value) != value) throw std::invalid_argument(std::string(key) + " is outside the supported integer range"); return static_cast(value); } std::pair generator_range(const Json& input, std::string_view minimum_key, std::string_view maximum_key) { const auto minimum = generator_number(input, minimum_key); const auto maximum = generator_number(input, maximum_key); if (minimum >= maximum) throw std::invalid_argument(std::string(maximum_key) + " must be greater than " + std::string(minimum_key)); return {minimum, maximum}; } void generate_spectral_row(std::vector& values, std::size_t row, std::size_t signal_count, double minimum, double maximum, double noise_standard_deviation, std::mt19937_64& engine) { if (noise_standard_deviation < 0.0) throw std::invalid_argument("noise_stddev must not be negative"); std::normal_distribution noise(0.0, noise_standard_deviation); const double span = maximum - minimum; const double denominator = static_cast(std::max(1, values.size() - 1)); for (std::size_t index = 0; index < values.size(); ++index) { const double x = static_cast(index) / denominator; double value = minimum + span * 0.10 + noise(engine); for (std::size_t signal = 0; signal < signal_count; ++signal) { const double phase = static_cast(signal + 1) / static_cast(signal_count + 1); const double center = std::clamp(phase + 0.035 * std::sin(row * 0.09 + signal * 1.73), 0.01, 0.99); const double width = 0.003 + 0.018 * static_cast((signal % 5) + 1) / 5.0; const double distance = (x - center) / width; value += span * (0.45 + 0.45 * std::sin(signal * 2.17 + row * 0.037)) * std::exp(-0.5 * distance * distance); } values[index] = std::clamp(value, minimum, maximum); } } template Json generator_2d_schema() { Json fields = Json::array(); std::string label; std::string description; if constexpr (std::same_as) { label = "生成频谱采样"; description = "生成一条含可控噪声底和多个窄带谱峰的完整功率频谱。"; fields.push_back(generator_integer_field("sample_count", "频谱采样点数", "一次频谱更新包含的功率采样点数。", 4096)); fields.push_back(generator_number_field("power_min", "功率下界", "噪声底和谱峰最终裁剪的功率下界。", -110.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("power_max", "功率上界", "谱峰最终裁剪的功率上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_integer_field("signal_count", "窄带信号数", "叠加在噪声底上的漂移高斯谱峰数量。", 8, 256)); fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "功率噪声的标准差,单位与功率值一致。", 2.0, 0.0, 1'000'000.0)); } else if constexpr (std::same_as) { label = "生成频率轨迹"; description = "按时间顺序生成一组轨迹采样。"; fields.push_back(generator_integer_field("sample_count", "轨迹采样点数", "时间有序的轨迹点数量。", 4096)); fields.push_back(generator_number_field("value_min", "轨迹值下界", "随机轨迹值下界。", -1.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("value_max", "轨迹值上界", "随机轨迹值上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_integer_field("tick_step", "时间刻度步长", "相邻样本之间的整数时间刻度差。", 1, 1'000'000)); } else if constexpr (std::same_as) { label = "生成分块扫频"; description = "按块数与每块频点数生成一条完整扫频曲线。"; fields.push_back(generator_integer_field("block_count", "扫频块数", "组成一次完整扫频的块数量。", 64, 65'536)); fields.push_back(generator_integer_field("bins_per_block", "每块频点数", "每个扫频块保存的连续频点数量。", 8, 65'536)); fields.push_back(generator_number_field("power_min", "功率下界", "随机扫频功率下界。", -110.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("power_max", "功率上界", "随机扫频功率上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_integer_field("signal_count", "窄带信号数", "跨扫频块连续分布的谱峰数量。", 8, 256)); fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "扫频噪声底标准差。", 2.0, 0.0, 1'000'000.0)); } else if constexpr (std::same_as) { label = "生成余辉历史"; description = "生成多帧频谱历史,用于测试余辉累积和衰减。"; fields.push_back(generator_integer_field("history_count", "历史频谱帧数", "余辉中保留的历史频谱数量。", 32, 4096)); fields.push_back(generator_integer_field("samples_per_spectrum", "每帧采样点数", "每条历史频谱包含的功率采样点数。", 512, 65'536)); fields.push_back(generator_number_field("power_min", "功率下界", "随机功率值下界。", -110.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("power_max", "功率上界", "随机功率值上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_integer_field("signal_count", "漂移信号数", "在历史帧之间连续漂移的谱峰数量。", 8, 256)); fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "历史频谱噪声底标准差。", 2.0, 0.0, 1'000'000.0)); } else if constexpr (std::same_as) { label = "生成瀑布图历史"; description = "生成带时间刻度的多行频谱数据。"; fields.push_back(generator_integer_field("row_count", "瀑布行数", "瀑布图中保存的时间行数量。", 256, 4096)); fields.push_back(generator_integer_field("bins_per_row", "每行频点数", "每一时间行包含的频率采样点数。", 512, 65'536)); fields.push_back(generator_number_field("power_min", "功率下界", "随机功率值下界。", -110.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("power_max", "功率上界", "随机功率值上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_integer_field("signal_count", "漂移信号数", "沿时间行移动的窄带谱峰数量。", 8, 256)); fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "瀑布噪声底标准差。", 2.0, 0.0, 1'000'000.0)); } else if constexpr (std::same_as) { label = "生成星座采样"; description = "分别按 I/Q 坐标范围生成随机星座点。"; fields.push_back(generator_integer_field("point_count", "星座点数", "本次写入的 I/Q 采样数量。", 10'000)); fields.push_back(generator_number_field("i_min", "I 坐标下界", "同相分量随机范围下界。", -1.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("i_max", "I 坐标上界", "同相分量随机范围上界。", 1.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("q_min", "Q 坐标下界", "正交分量随机范围下界。", -1.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("q_max", "Q 坐标上界", "正交分量随机范围上界。", 1.0, -1'000'000.0, 1'000'000.0)); } else if constexpr (std::same_as) { label = "生成矩形选区"; description = "按 X/Y 坐标范围生成随机矩形区域。"; fields.push_back(generator_integer_field("region_count", "矩形数量", "本次写入的选区数量。", 128)); fields.push_back(generator_number_field("x_min", "X 坐标下界", "矩形起点 X 随机范围下界。", 0.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("x_max", "X 坐标上界", "矩形终点 X 随机范围上界。", 100.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("y_min", "Y 坐标下界", "矩形起点 Y 随机范围下界。", 0.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("y_max", "Y 坐标上界", "矩形终点 Y 随机范围上界。", 100.0, -1'000'000.0, 1'000'000.0)); } if (!fields.empty()) fields.push_back(generator_integer_field("seed", "随机种子", "固定种子可重现同一压力数据集,便于对比不同帧策略和像素传输模式。", 42, 4'294'967'295ULL)); return {{"label", std::move(label)}, {"description", std::move(description) + " 可通过数据规模与坐标/数值范围构造可重复的压力负载。"}, {"fields", std::move(fields)}}; } template nlohmann::json generate_2d_data(Object& object, const Json& input) { using Definition = typename Object::Attached_Object; try { std::mt19937_64 engine{generator_count(input, "seed", 4'294'967'295ULL)}; std::size_t generated_count{}; if constexpr (std::same_as) { const auto count = generator_count(input, "sample_count"); const auto [minimum, maximum] = generator_range(input, "power_min", "power_max"); std::vector values(count); generate_spectral_row(values, 0, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine); object.update_samples(values); generated_count = count; } else if constexpr (std::same_as) { const auto count = generator_count(input, "sample_count"); const auto tick_step = generator_count(input, "tick_step"); const auto [minimum, maximum] = generator_range(input, "value_min", "value_max"); std::uniform_real_distribution distribution(minimum, maximum); std::vector samples(count); for (std::size_t index = 0; index < count; ++index) samples[index] = {static_cast(index * tick_step), distribution(engine)}; for (const auto& sample : samples) object.append_sample(sample.tick, sample.value); generated_count = count; } else if constexpr (std::same_as) { const auto block_count = generator_count(input, "block_count", 65'536); const auto width = generator_count(input, "bins_per_block", 65'536); if (block_count > 1'000'000 / width) throw std::invalid_argument("sweep data exceeds 1,000,000 samples"); const auto [minimum, maximum] = generator_range(input, "power_min", "power_max"); std::vector> blocks(block_count, std::vector(width)); std::vector complete(block_count * width); generate_spectral_row(complete, 0, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine); for (std::size_t block = 0; block < block_count; ++block) std::ranges::copy_n(complete.begin() + block * width, width, blocks[block].begin()); object.template set<&Sweep_Spectrum::Prop::bins_per_block>(width); object.template set<&Sweep_Spectrum::Prop::block_count>(block_count); for (auto& block : blocks) object.append_block(block); generated_count = block_count * width; } else if constexpr (std::same_as) { const auto row_count = generator_count(input, "history_count", 4096); const auto width = generator_count(input, "samples_per_spectrum", 65'536); if (row_count > 1'000'000 / width) throw std::invalid_argument("afterglow data exceeds 1,000,000 samples"); const auto [minimum, maximum] = generator_range(input, "power_min", "power_max"); std::vector> spectra(row_count, std::vector(width)); const auto signal_count = generator_count(input, "signal_count", 256); const auto noise_stddev = generator_number(input, "noise_stddev"); for (std::size_t row = 0; row < row_count; ++row) generate_spectral_row(spectra[row], row, signal_count, minimum, maximum, noise_stddev, engine); for (auto& spectrum : spectra) object.append_spectrum(spectrum); generated_count = row_count * width; } else if constexpr (std::same_as) { const auto row_count = generator_count(input, "row_count", 4096); const auto width = generator_count(input, "bins_per_row", 65'536); if (row_count > 1'000'000 / width) throw std::invalid_argument("waterfall data exceeds 1,000,000 samples"); const auto [minimum, maximum] = generator_range(input, "power_min", "power_max"); std::vector rows; rows.reserve(row_count); const auto signal_count = generator_count(input, "signal_count", 256); const auto noise_stddev = generator_number(input, "noise_stddev"); for (std::size_t row = 0; row < row_count; ++row) { std::vector row_values(width); generate_spectral_row(row_values, row, signal_count, minimum, maximum, noise_stddev, engine); rows.push_back({static_cast(row), std::move(row_values)}); } object.template set<&Waterfall::Prop::frequency_bin_count>(width); for (auto& row : rows) object.append_row(row.tick, row.values); generated_count = row_count * width; } else if constexpr (std::same_as) { const auto count = generator_count(input, "point_count"); const auto [i_min, i_max] = generator_range(input, "i_min", "i_max"); const auto [q_min, q_max] = generator_range(input, "q_min", "q_max"); std::uniform_real_distribution i_distribution(i_min, i_max), q_distribution(q_min, q_max); std::vector points(count); const auto submitted = monotonic_milliseconds(); for (std::size_t index = 0; index < count; ++index) points[index] = {{i_distribution(engine), q_distribution(engine)}, submitted}; for (const auto& point : points) object.append_point(point.point); generated_count = count; } else if constexpr (std::same_as) { const auto count = generator_count(input, "region_count"); const auto [x_min, x_max] = generator_range(input, "x_min", "x_max"); const auto [y_min, y_max] = generator_range(input, "y_min", "y_max"); std::uniform_real_distribution x_distribution(x_min, x_max), y_distribution(y_min, y_max); std::vector regions(count); for (auto& region : regions) { const auto first_x = x_distribution(engine), second_x = x_distribution(engine); const auto first_y = y_distribution(engine), second_y = y_distribution(engine); region = {{std::min(first_x, second_x), std::max(first_x, second_x)}, {std::min(first_y, second_y), std::max(first_y, second_y)}}; } object.template set<&Selection_Rectangle_Overlay::Prop::selected_regions>(std::move(regions)); generated_count = count; } else { return {{"success", false}, {"error", "this plot has no raw data input"}}; } return {{"success", true}, {"generated_count", generated_count}}; } catch (const std::exception& error) { return {{"success", false}, {"error", error.what()}}; } } template std::unique_ptr make_scene_view( Object& object, Scene_2D& scene, std::function update, Owned_Objects&&... owned_objects) { using Definition = typename Object::Attached_Object; using Tag = typename Definition::Base_Tag; using State = typename Definition::State; std::vector> components; components.push_back(make_scene_component(scene)); components.push_back(make_renderable_component("plot", "主绘图组件", "renderable", object)); std::size_t axis_index{}; const auto append_owned = [&](const auto& owned) { using Owned = std::remove_cvref_t; if constexpr (std::same_as || std::same_as || std::same_as) { const auto id = axis_index++ == 0 ? "axis-x" : "axis-y"; components.push_back(make_axis_component(id, id == std::string_view{"axis-x"} ? "横向坐标轴" : "纵向坐标轴", *owned)); } else if constexpr (std::same_as && !std::same_as) { components.push_back(make_renderable_component, Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, State_Field>("selection", "矩形选区", "overlay", *owned)); } }; (append_owned(owned_objects), ...); return std::make_unique...>>( std::move(components), std::move(update), typename Scene_View_Model...>::Data_Generator{ generator_2d_schema(), [&object](const nlohmann::json& input) { return generate_2d_data(object, input); }}, std::forward(owned_objects)...); } std::unique_ptr make_frequency_axis() { auto result = Frequency_Axis_Object::Builder{} .set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal) .set(&Abs_Axis::Prop::position, Point_F{64.0, 370.0}) .set(&Abs_Axis::Prop::pixel_length, 620.0) .set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0}) .build(); if (!result) throw std::logic_error("frequency axis dependency graph is invalid"); return std::move(result).value(); } std::unique_ptr make_numeric_axis( Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Axis_Range range) { auto result = Numeric_Axis_Object::Builder{} .set(&Abs_Axis::Prop::orientation, orientation) .set(&Abs_Axis::Prop::position, position) .set(&Abs_Axis::Prop::pixel_length, length) .set(&Numeric_Axis::Prop::coordinate_range, range) .build(); if (!result) throw std::logic_error("numeric axis dependency graph is invalid"); return std::move(result).value(); } std::unique_ptr make_time_axis( Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length) { auto result = Time_Axis_Object::Builder{} .set(&Abs_Axis::Prop::orientation, orientation) .set(&Abs_Axis::Prop::position, position) .set(&Abs_Axis::Prop::pixel_length, length) .build(); if (!result) throw std::logic_error("time axis dependency graph is invalid"); return std::move(result).value(); } template std::unique_ptr selection_overlay(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis) { auto result = Selection_Object::Builder(horizontal_axis, vertical_axis).build(); if (!result) throw std::logic_error("selection overlay axes form an invalid dependency graph"); return std::move(result).value(); } template void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) { const Size previous_viewport = scene->template read_prop().viewport; if (previous_viewport == viewport || previous_viewport.empty()) return; const auto resize_axis = [&](auto* axis) { const auto layout = axis->template read_prop(); const double horizontal_scale = static_cast(viewport.width) / previous_viewport.width; const double vertical_scale = static_cast(viewport.height) / previous_viewport.height; axis->template set<&Abs_Axis::Prop::position>(Point_F{ layout.position.x * horizontal_scale, layout.position.y * vertical_scale }); axis->template set<&Abs_Axis::Prop::pixel_length>(layout.pixel_length * (layout.orientation == Axis_Orientation::horizontal ? horizontal_scale : vertical_scale)); }; (resize_axis(axes), ...); } } std::shared_ptr make_axes_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); frequency->template set<&Abs_Axis::Prop::unit_text>("Hz"); auto numeric = make_numeric_axis( Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-100.0, 0.0}); numeric->template set<&Abs_Axis::Prop::unit_text>("dB"); auto time = make_time_axis( Axis_Orientation::horizontal, {64.0, 190.0}, 620.0); time->template set<&Abs_Axis::Prop::unit_text>("Time"); auto scene = *Scene_2D::Builder{} .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_dependency_node(frequency.get()) .add_dependency_node(numeric.get()) .add_dependency_node(time.get()) .add_dependency_node(frequency.get()) .add_dependency_node(numeric.get()) .add_dependency_node(time.get()) .build(); auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Render_Tick& event, bool) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, numeric, time); constexpr double day_milliseconds = 86'400'000.0; time->append_time(Time_Of_Day{ static_cast( std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) }); }; std::vector> components; components.push_back(make_scene_component(*scene)); components.push_back(make_axis_component("axis-frequency", "频率轴", *frequency)); components.push_back(make_axis_component("axis-value", "数值轴", *numeric)); components.push_back(make_axis_component("axis-time", "时间轴", *time)); Json generator_fields = Json::array({ generator_integer_field("time_sample_count", "时间样本数", "写入时间轴保留窗口的连续时间样本数量。", 16'384), generator_integer_field("time_step_ms", "时间步长 (ms)", "相邻时间样本之间的毫秒间隔。", 10), generator_number_field("frequency_min", "频率下界", "频率轴可见坐标下界。", 0.0, -1'000'000'000.0, 1'000'000'000.0), generator_number_field("frequency_max", "频率上界", "频率轴可见坐标上界,必须大于下界。", 100.0, -1'000'000'000.0, 1'000'000'000.0), generator_number_field("value_min", "数值下界", "数值轴可见坐标下界。", -100.0, -1'000'000'000.0, 1'000'000'000.0), generator_number_field("value_max", "数值上界", "数值轴可见坐标上界,必须大于下界。", 0.0, -1'000'000'000.0, 1'000'000'000.0) }); auto generate = [frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Json& input) { try { const auto sample_count = generator_count(input, "time_sample_count"); const auto time_step = generator_count(input, "time_step_ms"); const auto [frequency_minimum, frequency_maximum] = generator_range(input, "frequency_min", "frequency_max"); const auto [value_minimum, value_maximum] = generator_range(input, "value_min", "value_max"); frequency->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{frequency_minimum, frequency_maximum}); numeric->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{value_minimum, value_maximum}); time->template set<&Time_Axis::Prop::visible_count>(static_cast(sample_count)); constexpr std::uint64_t day_milliseconds = 86'400'000; for (std::size_t index = 0; index < sample_count; ++index) time->append_time(Time_Of_Day{static_cast((index * time_step) % day_milliseconds)}); return Json{{"success", true}, {"generated_count", sample_count}}; } catch (const std::exception& error) { return Json{{"success", false}, {"error", error.what()}}; } }; auto view = std::make_unique>( std::move(components), std::move(update), Scene_View_Model::Data_Generator{ Json{{"label", "生成坐标轴压力数据"}, {"description", "按时间样本规模和三个业务坐标范围生成可重复的坐标轴压力负载。"}, {"fields", std::move(generator_fields)}}, std::move(generate)}, std::move(frequency), std::move(numeric), std::move(time)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis(Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); auto spectrum = *Impl::Builder(frequency.get(), vertical.get()) .set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Spectrum::Prop::max_hold_visible, true) .build(); auto selection = selection_overlay(frequency.get(), vertical.get()); auto scene = *Scene_2D::Builder() .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_renderable(spectrum.get()) .add_renderable(selection.get()) .add_dependency(selection.get(), spectrum.get()) .build(); auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (!demo_data) return; std::array samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { const double x = static_cast(i) / samples.size(); samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(event.time_milliseconds * 0.001), 2.0)) + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004); } raw->update_samples(samples); }; auto view = make_scene_view< Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency", "Frequency placed at the visual center of the spectrum axis.">, Prop_Field<&Spectrum::Prop::partition_count, "partition_count", "Number of partitions used to prepare and render spectrum samples.">, Prop_Field<&Spectrum::Prop::max_hold_visible, "max_hold_visible", "Shows the accumulated maximum-hold spectrum curve when enabled.">, Prop_Field<&Spectrum::Prop::min_hold_visible, "min_hold_visible", "Shows the accumulated minimum-hold spectrum curve when enabled.">, Prop_Field<&Spectrum::Prop::max_marker_visible, "max_marker_visible", "Displays the marker attached to the strongest visible sample.">, Prop_Field<&Spectrum::Prop::min_marker_visible, "min_marker_visible", "Displays the marker attached to the weakest visible sample.">, Prop_Field<&Spectrum::Prop::sweep_region_visible, "sweep_region_visible", "Highlights the configured sweep-frequency interval on the plot.">, Prop_Field<&Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts sample preparation to the frequency range currently visible on the axis.">, Prop_Field<&Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete input sample span onto frequency coordinates.">, Prop_Field<&Spectrum::Prop::sweep_frequency_range, "sweep_frequency_range", "Defines the frequency interval rendered as the sweep region.">, Prop_Field<&Spectrum::Prop::partition_mode, "partition_mode", "Selects how samples are divided between preparation tasks.">, Prop_Field<&Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects the interpolation algorithm used between adjacent spectrum samples.">, Prop_Field<&Spectrum::Prop::max_brush, "max_brush", "Fill brush used for the maximum-hold area.">, Prop_Field<&Spectrum::Prop::current_brush, "current_brush", "Fill brush used for the current spectrum area.">, Prop_Field<&Spectrum::Prop::min_brush, "min_brush", "Fill brush used for the minimum-hold area.">, Prop_Field<&Spectrum::Prop::max_pen, "max_pen", "Stroke style used for the maximum-hold curve.">, Prop_Field<&Spectrum::Prop::current_pen, "current_pen", "Stroke style used for the current spectrum curve.">, Prop_Field<&Spectrum::Prop::min_pen, "min_pen", "Stroke style used for the minimum-hold curve.">, Prop_Field<&Spectrum::Prop::selected_marker_pen, "selected_marker_pen", "Stroke style used to emphasize the currently selected marker.">, Prop_Field<&Spectrum::Prop::marker_pen, "marker_pen", "Default stroke style used for unselected spectrum markers.">, Prop_Field<&Spectrum::Prop::middle_frequency_pen, "middle_frequency_pen", "Stroke style used for the center-frequency indicator.">, Prop_Field<&Spectrum::Prop::sweep_region_brush, "sweep_region_brush", "Fill brush used to highlight the sweep-frequency interval.">, Prop_Field<&Spectrum::Prop::custom_markers, "custom_markers", "User-defined marker positions and presentation data.">, Prop_Field<&Spectrum::Prop::selected_marker, "selected_marker", "Index of the custom marker currently selected for interaction.">, State_Field, State_Field, State_Field>( *spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto time = make_time_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0); auto vertical = make_numeric_axis( Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}); auto trace = *Impl::Builder(time.get(), vertical.get()).build(); auto selection = selection_overlay(time.get(), vertical.get()); auto scene = *Scene_2D::Builder{} .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_renderable(trace.get()) .add_renderable(selection.get()) .add_dependency(selection.get(), trace.get()) .build(); auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, time, vertical); if (!demo_data) return; constexpr double day_milliseconds = 86'400'000.0; const auto tick = time->append_time(Time_Of_Day{ static_cast( std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) }); raw->append_sample(tick, std::sin(event.time_milliseconds * 0.0025) * 0.8 + std::sin(event.time_milliseconds * 0.0007) * 0.2); }; auto view = make_scene_view< Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">, Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">, Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode", "Selects how trace samples are divided between preparation tasks.">, State_Field, State_Field>( *trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis( Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); auto sweep = *Impl::Builder(frequency.get(), vertical.get()) .set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Sweep_Spectrum::Prop::bins_per_block, std::size_t{8}) .set(&Sweep_Spectrum::Prop::block_count, std::size_t{64}) .build(); auto selection = selection_overlay(frequency.get(), vertical.get()); auto scene = *Scene_2D::Builder{} .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_renderable(sweep.get()) .add_renderable(selection.get()) .add_dependency(selection.get(), sweep.get()) .build(); auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (!demo_data) return; const auto& state = raw->template read_prop(); const std::size_t block_count = std::max(1, state.block_count); const std::size_t bins_per_block = std::max(1, state.bins_per_block); const std::size_t block_index = static_cast(event.sequence % block_count); std::vector values(bins_per_block); for (std::size_t i = 0; i < values.size(); ++i) { const auto sweep_index = block_index * values.size() + i; values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002); } raw->append_block(values); }; auto view = make_scene_view< Prop_Field<&Sweep_Spectrum::Prop::bins_per_block, "bins_per_block", "Number of frequency bins stored in each incoming sweep block.">, Prop_Field<&Sweep_Spectrum::Prop::block_count, "block_count", "Number of blocks required to compose one complete sweep.">, Prop_Field<&Sweep_Spectrum::Prop::partition_count, "partition_count", "Number of partitions used during sweep preparation.">, Prop_Field<&Sweep_Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts preparation to the frequency interval visible on the axis.">, Prop_Field<&Sweep_Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete sweep span onto frequency coordinates.">, Prop_Field<&Sweep_Spectrum::Prop::partition_mode, "partition_mode", "Selects how sweep blocks are divided between preparation tasks.">, Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">, Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">, Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">, State_Field, State_Field, State_Field>( *sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis( Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); auto afterglow = *Impl::Builder(frequency.get(), vertical.get()) .set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0}) .set(&Afterglow::Prop::power_point_size, 96) .build(); auto selection = selection_overlay(frequency.get(), vertical.get()); auto scene = *Scene_2D::Builder{} .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_renderable(afterglow.get()) .add_renderable(selection.get()) .add_dependency(selection.get(), afterglow.get()) .build(); auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (!demo_data) return; std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow( static_cast(i) / values.size() - 0.5 - 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0)); raw->append_spectrum(values); }; auto view = make_scene_view< Prop_Field<&Afterglow::Prop::frequency_point_size, "frequency_point_size", "Number of frequency cells allocated across each afterglow row.">, Prop_Field<&Afterglow::Prop::power_point_size, "power_point_size", "Number of power cells allocated along the vertical afterglow range.">, Prop_Field<&Afterglow::Prop::partition_count, "partition_count", "Number of partitions used to prepare afterglow history.">, Prop_Field<&Afterglow::Prop::interpolate, "interpolate", "Enables interpolation when mapping samples into the afterglow grid.">, Prop_Field<&Afterglow::Prop::attenuation_rate, "attenuation_rate", "Controls how quickly historical energy fades between updates.">, Prop_Field<&Afterglow::Prop::frequency_range, "frequency_range", "Maps input samples onto the afterglow frequency axis.">, Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">, Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode", "Selects how afterglow cells are divided between preparation tasks.">, Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">, State_Field, State_Field, State_Field>( *afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto time = make_time_axis( Axis_Orientation::vertical, {64.0, 370.0}, -320.0); auto waterfall = *Impl::Builder(frequency.get(), time.get()) .set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}) .build(); auto selection = selection_overlay(frequency.get(), time.get()); auto scene = *Scene_2D::Builder{} .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_renderable(waterfall.get()) .add_renderable(selection.get()) .add_dependency(selection.get(), waterfall.get()) .build(); auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Render_Tick& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, time); if (!demo_data) return; std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow( static_cast(i) / values.size() - 0.5 - 0.22 * std::sin(event.time_milliseconds * 0.0006), 2.0)); constexpr double day_milliseconds = 86'400'000.0; const auto tick = time->append_time(Time_Of_Day{ static_cast( std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) }); raw->append_row(tick, values); }; auto view = make_scene_view< Prop_Field<&Waterfall::Prop::tooltip_enabled, "tooltip_enabled", "Enables value inspection tooltips over waterfall cells.">, Prop_Field<&Waterfall::Prop::tooltip_font, "tooltip_font", "Font used to render waterfall tooltip text.">, Prop_Field<&Waterfall::Prop::tooltip_text_pen, "tooltip_text_pen", "Pen used to draw tooltip text and its foreground color.">, Prop_Field<&Waterfall::Prop::tooltip_background_brush, "tooltip_background_brush", "Brush used to fill the tooltip background panel.">, Prop_Field<&Waterfall::Prop::frequency_bin_count, "frequency_bin_count", "Number of frequency bins expected in each waterfall row.">, Prop_Field<&Waterfall::Prop::partition_count, "partition_count", "Number of partitions used to prepare waterfall cells.">, Prop_Field<&Waterfall::Prop::visible_range_only, "visible_range_only", "Restricts preparation to frequencies visible on the current axis.">, Prop_Field<&Waterfall::Prop::frequency_range, "frequency_range", "Maps row samples onto waterfall frequency coordinates.">, Prop_Field<&Waterfall::Prop::power_range, "power_range", "Defines the power interval mapped through the waterfall color map.">, Prop_Field<&Waterfall::Prop::partition_mode, "partition_mode", "Selects how waterfall rows are divided between preparation tasks.">, Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">, Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">, State_Field, State_Field, State_Field>( *waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto horizontal = make_numeric_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}); auto vertical = make_numeric_axis( Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}); auto constellation = *Impl::Builder(horizontal.get(), vertical.get()) .set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2}) .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}) .build(); auto selection = selection_overlay(horizontal.get(), vertical.get()); auto scene = *Scene_2D::Builder{} .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_renderable(constellation.get()) .add_renderable(selection.get()) .add_dependency(selection.get(), constellation.get()) .build(); auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); if (!demo_data) return; const auto& state = raw->template read_prop(); const int anchor_count = static_cast(state.type); const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; const double phase = event.time_milliseconds * 0.001; for (int index = 0; index < anchor_count; ++index) { const double angle = state.phase_offset_radians + 2.0 * std::numbers::pi * static_cast(index) / anchor_count; const double noise_i = 0.025 * std::sin(phase * 11.0 + index * 1.73) + 0.012 * std::cos(phase * 23.0 + index * 0.61); const double noise_q = 0.025 * std::cos(phase * 13.0 + index * 1.37) + 0.012 * std::sin(phase * 19.0 + index * 0.47); raw->append_point({ state.i_range.center() + std::cos(angle) * radius + noise_i, state.q_range.center() + std::sin(angle) * radius + noise_q }); } }; auto view = make_scene_view< Prop_Field<&Constellation_Diagram::Prop::point_lifetime_ms, "point_lifetime_ms", "Time in milliseconds that an appended constellation point remains visible.">, Prop_Field<&Constellation_Diagram::Prop::type, "type", "Selects the modulation constellation used to generate reference anchors.">, Prop_Field<&Constellation_Diagram::Prop::phase_offset_radians, "phase_offset_radians", "Rotates constellation points and anchors by the specified phase angle.">, Prop_Field<&Constellation_Diagram::Prop::i_range, "i_range", "Defines the horizontal in-phase coordinate interval.">, Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range", "Defines the vertical quadrature coordinate interval.">, Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">, Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">, State_Field>( *constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; auto horizontal = make_numeric_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}); auto vertical = make_numeric_axis( Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}); auto selection = *Impl::Builder(horizontal.get(), vertical.get()).build(); auto scene = *Scene_2D::Builder{} .set(&Render_Scene_2D::Prop::viewport, canvas) .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) .set(&Render_Scene_2D::Prop::view_active, true) .add_renderable(selection.get()) .build(); auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); }; auto view = make_scene_view< Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">, Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, State_Field>( *selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } }