924 lines
66 KiB
C++
924 lines
66 KiB
C++
#include "Gallery_Plots_2D.hpp"
|
|
#include "Renderable_Adapter.hpp"
|
|
#include <render_2D/plottable/Plottables.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <memory>
|
|
#include <numbers>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <string_view>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
namespace aethera::web {
|
|
namespace {
|
|
using namespace render_2d;
|
|
using Scene_2D = Render_Scene_2D;
|
|
using Frequency_Axis_Object = Frequency_Axis;
|
|
using Numeric_Axis_Object = Numeric_Axis;
|
|
using Time_Axis_Object = Time_Axis;
|
|
using Selection_Object = Selection_Rectangle_Overlay;
|
|
template <typename... Owned_Objects>
|
|
struct Scene_View_Model final : public Plot::Scene_View {
|
|
public:
|
|
struct Data_Generator {
|
|
nlohmann::json schema;
|
|
std::function<nlohmann::json(const nlohmann::json &)> generate;
|
|
std::function<void(const nlohmann::json &, const Plot_Render_Tick &)> advance;
|
|
explicit operator bool() const noexcept {
|
|
return static_cast<bool>(generate);
|
|
}
|
|
};
|
|
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
|
|
std::function<void(const Plot_Render_Tick &, bool)> 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", 3}, {"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 component_state(std::string_view component) const 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"}};
|
|
return (*found)->state();
|
|
}
|
|
nlohmann::json component_snapshots() const override {
|
|
nlohmann::json result = nlohmann::json::object();
|
|
for (const auto& descriptor : descriptors)
|
|
result[std::string(descriptor->id())] = descriptor->snapshot();
|
|
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.store(std::make_shared<const nlohmann::json>(input),
|
|
std::memory_order_release);
|
|
return result;
|
|
}
|
|
void update(const Plot_Render_Tick& request) override {
|
|
const auto input = generated_data.load(std::memory_order_acquire);
|
|
if (input && data_generator.advance) data_generator.advance(*input, request);
|
|
update_scene(request, !input);
|
|
}
|
|
private:
|
|
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
|
|
std::function<void(const Plot_Render_Tick &, bool)> update_scene;
|
|
Data_Generator data_generator;
|
|
std::atomic<std::shared_ptr<const nlohmann::json>> generated_data{}; /* 成功生成后发布不可变参数;渲染任务按同一配置持续产生压力数据。 */
|
|
std::tuple<Owned_Objects...> objects;
|
|
};
|
|
template <auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
|
using Prop_Field = detail::Prop_Field<Member, Key, Description>;
|
|
template <typename Definition, auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
|
using State_Field = detail::State_Field<typename Definition::Base_Tag, Member, Key, Description>;
|
|
template <typename Object, typename... Fields>
|
|
std::unique_ptr<detail::Renderable_Descriptor> make_renderable_component(
|
|
std::string id, std::string label, std::string kind, Object& object) {
|
|
using Definition = typename Object::Attached_Object;
|
|
using Adapter = detail::Renderable_Adapter<Object, Fields...,
|
|
detail::Prop_Field < &Renderable_2D::Prop::cache_enabled, "cache_enabled", "Whether this renderable reuses its complete color result across unchanged frames.">>;
|
|
return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object});
|
|
}
|
|
template <typename Scene_Object>
|
|
std::unique_ptr<detail::Renderable_Descriptor> make_scene_component(Scene_Object& scene) {
|
|
using Definition = typename Scene_Object::Attached_Object;
|
|
using Prop = typename Definition::Prop;
|
|
using Adapter = detail::Renderable_Adapter<Scene_Object,
|
|
detail::Prop_Field < &Prop::viewport, "viewport", "Final scene viewport in physical pixels.">,
|
|
detail::Prop_Field < &Prop::background, "background", "Scene clear color." >,
|
|
detail::Prop_Field < &Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >>;
|
|
return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene});
|
|
}
|
|
template <typename Axis_Object>
|
|
std::unique_ptr<detail::Renderable_Descriptor> make_axis_component(
|
|
std::string id, std::string label, Axis_Object& axis) {
|
|
return make_renderable_component<Axis_Object,
|
|
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
|
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<detail::Renderable_Descriptor> make_axis_component<Time_Axis_Object>(
|
|
std::string id, std::string label, Time_Axis_Object& axis) {
|
|
return make_renderable_component<Time_Axis_Object,
|
|
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
|
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<Time_Axis, &Time_Axis::State::next_tick, "next_tick", "Next allocated time tick.">>(
|
|
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<double>(value), 1.0, static_cast<double>(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<double>();
|
|
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<double>(maximum) || std::floor(value) != value) throw std::invalid_argument(std::string(key) + " is outside the supported integer range");
|
|
return static_cast<std::size_t>(value);
|
|
}
|
|
std::pair<double, double> 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<Plot_Value>& 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<double> noise(0.0, noise_standard_deviation);
|
|
const double span = maximum - minimum;
|
|
const double denominator = static_cast<double>(std::max<std::size_t>(1, values.size() - 1));
|
|
for (std::size_t index = 0; index < values.size(); ++index) {
|
|
const double x = static_cast<double>(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<double>(signal + 1) / static_cast<double>(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<double>((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 <typename Definition>
|
|
Json generator_2d_schema() {
|
|
Json fields = Json::array();
|
|
std::string label;
|
|
std::string description;
|
|
if constexpr (std::same_as<Definition, Spectrum>) {
|
|
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<Definition, Frequency_Trace>) {
|
|
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<Definition, Sweep_Spectrum>) {
|
|
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<Definition, Afterglow>) {
|
|
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<Definition, Waterfall>) {
|
|
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<Definition, Constellation_Diagram>) {
|
|
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<Definition, Selection_Rectangle_Overlay>) {
|
|
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));
|
|
fields.push_back(generator_integer_field(
|
|
"update_every_n_frames", "更新帧间隔",
|
|
"每隔多少个渲染帧重新生成一次本图压力数据;1 表示每帧更新。",
|
|
1, 100'000));
|
|
}
|
|
return {{"label", std::move(label)}, {"description", std::move(description) + " 可通过数据规模与坐标/数值范围构造可重复的压力负载。"}, {"fields", std::move(fields)}};
|
|
}
|
|
template <typename Object>
|
|
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)};
|
|
const auto animation_row = input.value("_animation_row", std::size_t{});
|
|
std::size_t generated_count{};
|
|
if constexpr (std::same_as<Definition, Spectrum>) {
|
|
const auto count = generator_count(input, "sample_count");
|
|
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
|
|
std::vector<Plot_Value> values(count);
|
|
generate_spectral_row(values, animation_row, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine);
|
|
object.template pending_buffer<Spectrum_Frame_Tag>() = Spectrum_Frame{std::move(values)};
|
|
object.template mark_dirty<Render_Graph_Tag>();
|
|
generated_count = count;
|
|
}
|
|
else if constexpr (std::same_as<Definition, Frequency_Trace>) {
|
|
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<double> distribution(minimum, maximum);
|
|
std::vector<Frequency_Trace_Sample> samples(count);
|
|
for (std::size_t index = 0; index < count; ++index) samples[index] = {static_cast<Plot_Time_Tick>(index * tick_step), distribution(engine)};
|
|
for (const auto& sample : samples) object.template submit_stream<Frequency_Trace_Stream_Tag>(sample);
|
|
object.template mark_dirty<Render_Graph_Tag>();
|
|
generated_count = count;
|
|
}
|
|
else if constexpr (std::same_as<Definition, Sweep_Spectrum>) {
|
|
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<std::vector<Plot_Value>> blocks(block_count, std::vector<Plot_Value>(width));
|
|
std::vector<Plot_Value> complete(block_count * width);
|
|
generate_spectral_row(complete, animation_row, 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 (std::size_t block_index = 0; block_index < blocks.size(); ++block_index)
|
|
object.template submit_stream<Sweep_Spectrum_Stream_Tag>(
|
|
std::make_shared<const Sweep_Spectrum_Block>(
|
|
Sweep_Spectrum_Block{block_index, std::move(blocks[block_index])}));
|
|
object.template mark_dirty<Render_Graph_Tag>();
|
|
generated_count = block_count * width;
|
|
}
|
|
else if constexpr (std::same_as<Definition, Afterglow>) {
|
|
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<std::vector<Plot_Value>> spectra(row_count, std::vector<Plot_Value>(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], animation_row + row, signal_count, minimum, maximum, noise_stddev, engine);
|
|
for (auto& spectrum : spectra)
|
|
object.template submit_stream<Afterglow_Stream_Tag>(
|
|
std::make_shared<const std::vector<Plot_Value>>(std::move(spectrum)));
|
|
object.template mark_dirty<Render_Graph_Tag>();
|
|
generated_count = row_count * width;
|
|
}
|
|
else if constexpr (std::same_as<Definition, Waterfall>) {
|
|
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<Waterfall_Row> 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<Plot_Value> row_values(width);
|
|
generate_spectral_row(row_values, animation_row + row, signal_count, minimum, maximum, noise_stddev, engine);
|
|
rows.push_back({static_cast<Plot_Time_Tick>(row), std::move(row_values)});
|
|
}
|
|
object.template set<&Waterfall::Prop::frequency_bin_count>(width);
|
|
for (auto& row : rows)
|
|
object.template submit_stream<Waterfall_Stream_Tag>(
|
|
std::make_shared<const Waterfall_Row>(std::move(row)));
|
|
object.template mark_dirty<Render_Graph_Tag>();
|
|
generated_count = row_count * width;
|
|
}
|
|
else if constexpr (std::same_as<Definition, Constellation_Diagram>) {
|
|
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<double> i_distribution(i_min, i_max), q_distribution(q_min, q_max);
|
|
std::vector<Constellation_Point> 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.template submit_stream<Constellation_Stream_Tag>(point);
|
|
object.template mark_dirty<Render_Graph_Tag>();
|
|
generated_count = count;
|
|
}
|
|
else if constexpr (std::same_as<Definition, Selection_Rectangle_Overlay>) {
|
|
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<double> x_distribution(x_min, x_max), y_distribution(y_min, y_max);
|
|
std::vector<Axis_Rectangle> 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 <typename... Fields, typename Object, typename... Owned_Objects>
|
|
std::unique_ptr<Plot::Scene_View> make_scene_view(
|
|
Object & object,
|
|
Scene_2D & scene,
|
|
std::function < void(const Plot_Render_Tick &, bool) > update,
|
|
Owned_Objects &&... owned_objects) {
|
|
using Definition = typename Object::Attached_Object;
|
|
using Tag = typename Definition::Base_Tag;
|
|
using State = typename Definition::State;
|
|
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
|
|
components.push_back(make_scene_component(scene));
|
|
components.push_back(make_renderable_component<Object, Fields...>("plot", "主绘图组件", "renderable", object));
|
|
std::size_t axis_index{};
|
|
const auto append_owned = [&](const auto& owned) {
|
|
using Owned = std::remove_cvref_t<decltype(owned)>;
|
|
if constexpr (std::same_as<typename Owned::element_type, Frequency_Axis_Object>
|
|
|| std::same_as<typename Owned::element_type, Numeric_Axis_Object>
|
|
|| std::same_as<typename Owned::element_type, Time_Axis_Object>) {
|
|
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<typename Owned::element_type, Selection_Object> && !std::same_as<Object, Selection_Object>) {
|
|
components.push_back(make_renderable_component<Selection_Object,
|
|
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_Rectangle_Overlay, &Selection_Rectangle_Overlay::State::selected_region_count, "selected_region_count", "Number of rectangular regions currently selected.">>("selection", "矩形选区", "overlay", *owned));
|
|
}
|
|
};
|
|
(append_owned(owned_objects), ...);
|
|
return std::make_unique<Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>>(
|
|
std::move(components),
|
|
std::move(update),
|
|
typename Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>::Data_Generator{
|
|
generator_2d_schema<Definition>(),
|
|
[&object](const nlohmann::json& input) {
|
|
return generate_2d_data(object, input);
|
|
},
|
|
[&object](const nlohmann::json& input,
|
|
const Plot_Render_Tick& tick) {
|
|
const auto interval = generator_count(
|
|
input, "update_every_n_frames", 100'000);
|
|
if (tick.sequence % interval != 0) return;
|
|
auto frame_input = input;
|
|
constexpr std::uint64_t maximum_seed{4'294'967'295ULL};
|
|
const auto base_seed = generator_count(
|
|
input, "seed", maximum_seed);
|
|
frame_input["seed"] =
|
|
1 + (base_seed - 1 + tick.sequence) % maximum_seed;
|
|
frame_input["_animation_row"] = tick.sequence;
|
|
const auto result = generate_2d_data(object, frame_input);
|
|
if (!result.value("success", false))
|
|
throw std::runtime_error(result.value(
|
|
"error", "continuous 2D data generation failed"));
|
|
}
|
|
},
|
|
std::forward<Owned_Objects>(owned_objects)...);
|
|
}
|
|
std::unique_ptr<Frequency_Axis_Object> make_frequency_axis() {
|
|
auto result = Frequency_Axis_Object::Builder<Frequency_Axis_Object>{}
|
|
.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<Numeric_Axis_Object> make_numeric_axis(
|
|
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length,
|
|
Axis_Range range) {
|
|
auto result = Numeric_Axis_Object::Builder<Numeric_Axis_Object>{}
|
|
.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<Time_Axis_Object> make_time_axis(
|
|
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length) {
|
|
auto result = Time_Axis_Object::Builder<Time_Axis_Object>{}
|
|
.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 <Axis_Object Horizontal_Axis, Axis_Object Vertical_Axis>
|
|
std::unique_ptr<Selection_Object> selection_overlay(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis) {
|
|
auto result = Selection_Object::Builder<Selection_Object>(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 <typename... Axes>
|
|
void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) {
|
|
const Size previous_viewport = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
|
|
if (previous_viewport == viewport || previous_viewport.empty()) return;
|
|
const auto resize_axis = [&](auto* axis) {
|
|
const auto layout = axis->template read_prop<Abs_Axis::Base_Tag>();
|
|
const double horizontal_scale = static_cast<double>(viewport.width) / previous_viewport.width;
|
|
const double vertical_scale = static_cast<double>(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<Plot> make_axes_plot() {
|
|
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<Scene_2D>{}
|
|
.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<Render_Graph_Tag>(frequency.get())
|
|
.add_dependency_node<Render_Graph_Tag>(numeric.get())
|
|
.add_dependency_node<Render_Graph_Tag>(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<int>(event.width), static_cast<int>(event.height)}, frequency, numeric, time);
|
|
constexpr double day_milliseconds = 86'400'000.0;
|
|
time->append_time(Time_Of_Day{
|
|
static_cast<std::int64_t>(
|
|
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))
|
|
});
|
|
};
|
|
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> 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<Axis_Visible_Count>(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<std::int64_t>((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<Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>>(
|
|
std::move(components), std::move(update),
|
|
Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>::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<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
std::shared_ptr<Plot> make_spectrum_plot() {
|
|
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 = *Spectrum::Builder<Spectrum>(frequency.get(), vertical.get(), 4u)
|
|
.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<Scene_2D>()
|
|
.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<Render_Graph_Tag>(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<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
|
if (!demo_data) return;
|
|
std::array < double, 256 > samples{};
|
|
for (std::size_t i = 0; i < samples.size(); ++i) {
|
|
const double x = static_cast<double>(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->template pending_buffer<Spectrum_Frame_Tag>() =
|
|
Spectrum_Frame{std::vector<Spectrum_Power>(samples.begin(), samples.end())};
|
|
raw->template mark_dirty<Render_Graph_Tag>();
|
|
};
|
|
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::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<Spectrum, &Spectrum::State::sample_count, "sample_count", "Number of input spectrum samples available in the latest update.">,
|
|
State_Field<Spectrum, &Spectrum::State::rendered_point_count, "rendered_point_count", "Number of curve points emitted by the latest render preparation.">,
|
|
State_Field<Spectrum, &Spectrum::State::selectable_marker_count, "selectable_marker_count", "Number of markers currently eligible for selection.">>(
|
|
*spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection));
|
|
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
std::shared_ptr<Plot> make_frequency_trace_plot() {
|
|
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 = *Frequency_Trace::Builder<Frequency_Trace>(time.get(), vertical.get(), 4u).build();
|
|
auto selection = selection_overlay(time.get(), vertical.get());
|
|
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
|
.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<Render_Graph_Tag>(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<int>(event.width), static_cast<int>(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::int64_t>(
|
|
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))
|
|
});
|
|
raw->template submit_stream<Frequency_Trace_Stream_Tag>(Frequency_Trace_Sample{
|
|
tick, std::sin(event.time_milliseconds * 0.0025) * 0.8
|
|
+ std::sin(event.time_milliseconds * 0.0007) * 0.2
|
|
});
|
|
raw->template mark_dirty<Render_Graph_Tag>();
|
|
};
|
|
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.">>(
|
|
*trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection));
|
|
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
std::shared_ptr<Plot> make_sweep_spectrum_plot() {
|
|
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 = *Sweep_Spectrum::Builder<Sweep_Spectrum>(frequency.get(), vertical.get(), 4u)
|
|
.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<Scene_2D>{}
|
|
.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<Render_Graph_Tag>(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<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
|
if (!demo_data) return;
|
|
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
|
|
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
|
|
const std::size_t bins_per_block = std::max<std::size_t>(1, state.bins_per_block);
|
|
const std::size_t block_index = static_cast<std::size_t>(event.sequence % block_count);
|
|
std::vector<double> 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->template submit_stream<Sweep_Spectrum_Stream_Tag>(
|
|
std::make_shared<const Sweep_Spectrum_Block>(
|
|
Sweep_Spectrum_Block{block_index, std::move(values)}));
|
|
raw->template mark_dirty<Render_Graph_Tag>();
|
|
};
|
|
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::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.">>(
|
|
*sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection));
|
|
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
std::shared_ptr<Plot> make_afterglow_plot() {
|
|
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 = *Afterglow::Builder<Afterglow>(
|
|
frequency.get(), vertical.get(), Plot_Partition_Grid{4, 3})
|
|
.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<Scene_2D>{}
|
|
.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<Render_Graph_Tag>(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<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
|
if (!demo_data) return;
|
|
std::array < double, 192 > 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<double>(i) / values.size() - 0.5
|
|
- 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0));
|
|
raw->template submit_stream<Afterglow_Stream_Tag>(
|
|
std::make_shared<const std::vector<Plot_Value>>(values.begin(), values.end()));
|
|
raw->template mark_dirty<Render_Graph_Tag>();
|
|
};
|
|
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_grid, "partition_grid", "Framebuffer partition grid expressed as column count by row count.">,
|
|
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::color_map, "color_map", "Maps accumulated energy values to rendered colors.">>(
|
|
*afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection));
|
|
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
std::shared_ptr<Plot> make_waterfall_plot() {
|
|
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 = *Waterfall::Builder<Waterfall>(
|
|
frequency.get(), time.get(), Plot_Partition_Grid{4, 3})
|
|
.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<Scene_2D>{}
|
|
.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<Render_Graph_Tag>(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<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
|
if (!demo_data) return;
|
|
constexpr double day_milliseconds = 86'400'000.0;
|
|
const auto tick = time->append_time(Time_Of_Day{
|
|
static_cast<std::int64_t>(
|
|
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))
|
|
});
|
|
std::array < double, 192 > 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<double>(i) / values.size() - 0.5
|
|
- 0.22 * std::sin(static_cast<double>(tick) * 0.01), 2.0));
|
|
raw->template submit_stream<Waterfall_Stream_Tag>(
|
|
std::make_shared<const Waterfall_Row>(Waterfall_Row{
|
|
tick, {values.begin(), values.end()}
|
|
}));
|
|
raw->template mark_dirty<Render_Graph_Tag>();
|
|
};
|
|
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_grid, "partition_grid", "Framebuffer partition grid expressed as column count by row count.">,
|
|
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::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.">>(
|
|
*waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection));
|
|
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
std::shared_ptr<Plot> make_constellation_plot() {
|
|
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 = *Constellation_Diagram::Builder<Constellation_Diagram>(
|
|
horizontal.get(), vertical.get(), 4u)
|
|
.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<Scene_2D>{}
|
|
.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<Render_Graph_Tag>(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<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
|
if (!demo_data) return;
|
|
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
|
|
const int anchor_count = static_cast<int>(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<double>(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->template submit_stream<Constellation_Stream_Tag>(Constellation_Point{
|
|
{
|
|
state.i_range.center() + std::cos(angle) * radius + noise_i,
|
|
state.q_range.center() + std::sin(angle) * radius + noise_q
|
|
},
|
|
monotonic_milliseconds()
|
|
});
|
|
}
|
|
raw->template mark_dirty<Render_Graph_Tag>();
|
|
};
|
|
auto view = make_scene_view<
|
|
Prop_Field<&Constellation_Diagram::Prop::partition_count, "partition_count", "Number of partitions used to render received constellation points.">,
|
|
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.">>(
|
|
*constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection));
|
|
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
std::shared_ptr<Plot> make_selection_overlay_plot() {
|
|
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 = *Selection_Rectangle_Overlay::Builder<Selection_Rectangle_Overlay>(horizontal.get(), vertical.get()).build();
|
|
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
|
.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<int>(event.width), static_cast<int>(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_Rectangle_Overlay,
|
|
&Selection_Rectangle_Overlay::State::selected_region_count,
|
|
"selected_region_count",
|
|
"Number of rectangular regions currently selected.">>(
|
|
*selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection));
|
|
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
|
}
|
|
}
|