完美一版
This commit is contained in:
@@ -0,0 +1,592 @@
|
||||
#include "Gallery_Plots_2D.hpp"
|
||||
#include "Renderable_Adapter.hpp"
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::web {
|
||||
namespace {
|
||||
using namespace render_2d;
|
||||
using Scene_2D = Impl<Render_Scene_2D>;
|
||||
using Frequency_Axis_Object = Impl<Frequency_Axis>;
|
||||
using Numeric_Axis_Object = Impl<Numeric_Axis>;
|
||||
using Time_Axis_Object = Impl<Time_Axis>;
|
||||
using Selection_Object = Impl<Selection_Rectangle_Overlay>;
|
||||
template <typename... Owned_Objects>
|
||||
class Scene_View_Model final : public Plot::Scene_View {
|
||||
public:
|
||||
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
|
||||
std::function<void(const Plot_Frame_Request&)> value_update,
|
||||
Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)),
|
||||
update_scene(std::move(value_update)),
|
||||
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;
|
||||
}
|
||||
void update(const Plot_Frame_Request& request) override {
|
||||
update_scene(request);
|
||||
}
|
||||
private:
|
||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
|
||||
std::function<void(const Plot_Frame_Request&)> update_scene;
|
||||
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 Tag = typename Definition::Base_Tag;
|
||||
using State = typename Definition::State;
|
||||
using Adapter = detail::Renderable_Adapter<Object, Fields...,
|
||||
detail::State_Field<Tag, &State::prepare_dirty, "prepare_dirty", "Whether source changes require the prepare stage to run again.">,
|
||||
detail::State_Field<Tag, &State::paint_dirty, "paint_dirty", "Whether prepared visual data requires the paint stage to run again.">,
|
||||
detail::State_Field<Tag, &State::prepare_executed, "prepare_executed", "Whether the prepare stage executed during the latest scene cycle.">,
|
||||
detail::State_Field<Tag, &State::paint_executed, "paint_executed", "Whether the paint stage executed during the latest scene cycle.">,
|
||||
detail::State_Field<Tag, &State::prepare_graph_rebuilt, "prepare_graph_rebuilt", "Whether the prepare task graph was rebuilt during the latest cycle.">,
|
||||
detail::State_Field<Tag, &State::paint_graph_rebuilt, "paint_graph_rebuilt", "Whether the paint task graph was rebuilt during the latest cycle.">,
|
||||
detail::State_Field<Tag, &State::prepare_task_count, "prepare_task_count", "Number of tasks in the current prepare execution graph.">,
|
||||
detail::State_Field<Tag, &State::paint_task_count, "paint_task_count", "Number of tasks in the current paint execution graph.">,
|
||||
detail::State_Field<Tag, &State::prepare_execution_time_ns, "prepare_execution_time_ns", "Measured prepare-stage execution time in nanoseconds.">,
|
||||
detail::State_Field<Tag, &State::paint_execution_time_ns, "paint_execution_time_ns", "Measured paint-stage execution time in nanoseconds.">>;
|
||||
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.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_dependency_count, "taskflow_dependency_count", "Number of graph dependencies.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
||||
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.">,
|
||||
State_Field<Time_Axis, &Time_Axis::State::samples, "samples", "Published time sample window.">>(
|
||||
std::move(id), std::move(label), "axis", axis);
|
||||
}
|
||||
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_Frame_Request&)> 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.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">,
|
||||
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),
|
||||
std::forward<Owned_Objects>(owned_objects)...);
|
||||
}
|
||||
std::unique_ptr<Frequency_Axis_Object> 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<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{}
|
||||
.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{}
|
||||
.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(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(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<Prepare_Data_Tag>(frequency.get())
|
||||
.add_dependency_node<Prepare_Data_Tag>(numeric.get())
|
||||
.add_dependency_node<Prepare_Data_Tag>(time.get())
|
||||
.add_dependency_node<Paint_Tag>(frequency.get())
|
||||
.add_dependency_node<Paint_Tag>(numeric.get())
|
||||
.add_dependency_node<Paint_Tag>(time.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) {
|
||||
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));
|
||||
auto view = std::make_unique<Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>>(
|
||||
std::move(components), std::move(update), std::move(frequency), std::move(numeric), std::move(time));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Spectrum>::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<Paint_Tag>(selection.get(), spectrum.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
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->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<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(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Frequency_Trace>::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<Paint_Tag>(selection.get(), trace.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
|
||||
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->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.">,
|
||||
Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">,
|
||||
State_Field<Frequency_Trace, &Frequency_Trace::State::sample_count, "sample_count", "Number of samples retained by the current trace.">,
|
||||
State_Field<Frequency_Trace, &Frequency_Trace::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest trace frame.">>(
|
||||
*trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Sweep_Spectrum>::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<Paint_Tag>(selection.get(), sweep.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
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 = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % 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->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.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of frequency segments that currently contain data.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of points retained by the single composite sweep curve.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the single composite sweep curve.">>(
|
||||
*sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Afterglow>::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<Paint_Tag>(selection.get(), afterglow.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
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->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.">,
|
||||
Prop_Field<&Afterglow::Prop::spectra, "spectra", "Spectrum history currently retained for afterglow rendering.">,
|
||||
State_Field<Afterglow, &Afterglow::State::history_count, "history_count", "Number of spectrum frames retained in afterglow history.">,
|
||||
State_Field<Afterglow, &Afterglow::State::latest_spectrum_point_count, "latest_spectrum_point_count", "Number of samples in the most recently appended spectrum.">,
|
||||
State_Field<Afterglow, &Afterglow::State::rendered_cell_count, "rendered_cell_count", "Number of colored cells emitted for the latest frame.">>(
|
||||
*afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Waterfall>::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<Paint_Tag>(selection.get(), waterfall.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
||||
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(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::int64_t>(
|
||||
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.">,
|
||||
Prop_Field<&Waterfall::Prop::rows, "rows", "Time-ordered collection of spectrum rows retained by the waterfall.">,
|
||||
State_Field<Waterfall, &Waterfall::State::row_count, "row_count", "Number of waterfall rows currently retained.">,
|
||||
State_Field<Waterfall, &Waterfall::State::stored_point_count, "stored_point_count", "Total number of spectrum points retained across all rows.">,
|
||||
State_Field<Waterfall, &Waterfall::State::rendered_cell_count, "rendered_cell_count", "Number of raster cells emitted for the latest frame.">>(
|
||||
*waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Constellation_Diagram>::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<Paint_Tag>(selection.get(), constellation.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
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->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.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::points, "points", "Current time-stamped collection of received I/Q samples.">,
|
||||
State_Field<Constellation_Diagram, &Constellation_Diagram::State::point_count, "point_count", "Number of constellation samples currently retained.">>(
|
||||
*constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Selection_Rectangle_Overlay>::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_Frame_Request& event) {
|
||||
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.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">,
|
||||
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(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
}
|
||||
+1
-592
@@ -168,235 +168,6 @@ struct Prop_Write {
|
||||
Plot::Json_Handler handler;
|
||||
};
|
||||
using Plot_Input = std::variant<Frame_Submission, Schema_Query, Prop_Write>;
|
||||
template <typename... Owned_Objects>
|
||||
class Scene_View_Model final : public Plot::Scene_View {
|
||||
public:
|
||||
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
|
||||
std::function<void(const Plot_Frame_Request&)> value_update,
|
||||
Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)),
|
||||
update_scene(std::move(value_update)),
|
||||
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;
|
||||
}
|
||||
void update(const Plot_Frame_Request& request) override {
|
||||
update_scene(request);
|
||||
}
|
||||
private:
|
||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
|
||||
std::function<void(const Plot_Frame_Request&)> update_scene;
|
||||
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 Tag = typename Definition::Base_Tag;
|
||||
using State = typename Definition::State;
|
||||
using Adapter = detail::Renderable_Adapter<Object, Fields...,
|
||||
detail::State_Field<Tag, &State::prepare_dirty, "prepare_dirty", "Whether source changes require the prepare stage to run again.">,
|
||||
detail::State_Field<Tag, &State::paint_dirty, "paint_dirty", "Whether prepared visual data requires the paint stage to run again.">,
|
||||
detail::State_Field<Tag, &State::prepare_executed, "prepare_executed", "Whether the prepare stage executed during the latest scene cycle.">,
|
||||
detail::State_Field<Tag, &State::paint_executed, "paint_executed", "Whether the paint stage executed during the latest scene cycle.">,
|
||||
detail::State_Field<Tag, &State::prepare_graph_rebuilt, "prepare_graph_rebuilt", "Whether the prepare task graph was rebuilt during the latest cycle.">,
|
||||
detail::State_Field<Tag, &State::paint_graph_rebuilt, "paint_graph_rebuilt", "Whether the paint task graph was rebuilt during the latest cycle.">,
|
||||
detail::State_Field<Tag, &State::prepare_task_count, "prepare_task_count", "Number of tasks in the current prepare execution graph.">,
|
||||
detail::State_Field<Tag, &State::paint_task_count, "paint_task_count", "Number of tasks in the current paint execution graph.">,
|
||||
detail::State_Field<Tag, &State::prepare_execution_time_ns, "prepare_execution_time_ns", "Measured prepare-stage execution time in nanoseconds.">,
|
||||
detail::State_Field<Tag, &State::paint_execution_time_ns, "paint_execution_time_ns", "Measured paint-stage execution time in nanoseconds.">>;
|
||||
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.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_dependency_count, "taskflow_dependency_count", "Number of graph dependencies.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
||||
return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene});
|
||||
}
|
||||
template <>
|
||||
std::unique_ptr<detail::Renderable_Descriptor> make_scene_component<Scene_3D>(Scene_3D& scene) {
|
||||
using Adapter = detail::Renderable_Adapter<Scene_3D,
|
||||
detail::Prop_Field<&Render_Scene_3D::Prop::clear_color, "clear_color", "Linear scene clear color.">,
|
||||
detail::Prop_Field<&Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_dependency_count, "taskflow_dependency_count", "Number of graph dependencies.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
||||
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.">,
|
||||
State_Field<Time_Axis, &Time_Axis::State::samples, "samples", "Published time sample window.">>(
|
||||
std::move(id), std::move(label), "axis", axis);
|
||||
}
|
||||
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_Frame_Request&)> 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.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">,
|
||||
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),
|
||||
std::forward<Owned_Objects>(owned_objects)...);
|
||||
}
|
||||
std::unique_ptr<Frequency_Axis_Object> 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<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{}
|
||||
.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{}
|
||||
.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(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 <Renderable_Object Plot_Object>
|
||||
void place_selection_over(Scene_2D* scene, Selection_Object* selection, Plot_Object* plot) {
|
||||
const auto result = scene->template edit_dependency_graph<Paint_Tag>([&](auto& paint) {
|
||||
paint.add_dependency(selection, plot);
|
||||
});
|
||||
if (!result) throw std::logic_error("selection overlay paint order is invalid");
|
||||
}
|
||||
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), ...);
|
||||
}
|
||||
template <typename Scene_Object>
|
||||
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
|
||||
const auto dispatch = [&](auto event) {
|
||||
@@ -406,7 +177,7 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
|
||||
event.position = input.position;
|
||||
event.global_position = input.global_position;
|
||||
event.button = input.button;
|
||||
event.buttons = input.buttons;
|
||||
event.buttons = input.buttons; /* 保留移动事件的持续按键位,供相机手势识别拖拽。 */
|
||||
event.modifiers = input.modifiers;
|
||||
};
|
||||
switch (input.type) {
|
||||
@@ -445,368 +216,6 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
|
||||
}
|
||||
}
|
||||
}
|
||||
std::shared_ptr<Plot> 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)
|
||||
.build();
|
||||
const auto topology = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
|
||||
prepare.add(frequency.get());
|
||||
prepare.add(numeric.get());
|
||||
prepare.add(time.get());
|
||||
paint.add(frequency.get());
|
||||
paint.add(numeric.get());
|
||||
paint.add(time.get());
|
||||
});
|
||||
if (!topology) throw std::logic_error("axis gallery topology is invalid");
|
||||
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) {
|
||||
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));
|
||||
auto view = std::make_unique<Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>>(
|
||||
std::move(components), std::move(update), std::move(frequency), std::move(numeric), std::move(time));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Spectrum>::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(selection.get())
|
||||
.build();
|
||||
place_selection_over(scene.get(), selection.get(), spectrum.get());
|
||||
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
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->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<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(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Frequency_Trace>::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())
|
||||
.build();
|
||||
place_selection_over(scene.get(), selection.get(), trace.get());
|
||||
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
|
||||
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->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.">,
|
||||
Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">,
|
||||
State_Field<Frequency_Trace, &Frequency_Trace::State::sample_count, "sample_count", "Number of samples retained by the current trace.">,
|
||||
State_Field<Frequency_Trace, &Frequency_Trace::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest trace frame.">>(
|
||||
*trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Sweep_Spectrum>::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())
|
||||
.build();
|
||||
place_selection_over(scene.get(), selection.get(), sweep.get());
|
||||
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
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 = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % 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->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.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of frequency segments that currently contain data.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of points retained by the single composite sweep curve.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the single composite sweep curve.">>(
|
||||
*sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Afterglow>::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())
|
||||
.build();
|
||||
place_selection_over(scene.get(), selection.get(), afterglow.get());
|
||||
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
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->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.">,
|
||||
Prop_Field<&Afterglow::Prop::spectra, "spectra", "Spectrum history currently retained for afterglow rendering.">,
|
||||
State_Field<Afterglow, &Afterglow::State::history_count, "history_count", "Number of spectrum frames retained in afterglow history.">,
|
||||
State_Field<Afterglow, &Afterglow::State::latest_spectrum_point_count, "latest_spectrum_point_count", "Number of samples in the most recently appended spectrum.">,
|
||||
State_Field<Afterglow, &Afterglow::State::rendered_cell_count, "rendered_cell_count", "Number of colored cells emitted for the latest frame.">>(
|
||||
*afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Waterfall>::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())
|
||||
.build();
|
||||
place_selection_over(scene.get(), selection.get(), waterfall.get());
|
||||
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
||||
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(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::int64_t>(
|
||||
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.">,
|
||||
Prop_Field<&Waterfall::Prop::rows, "rows", "Time-ordered collection of spectrum rows retained by the waterfall.">,
|
||||
State_Field<Waterfall, &Waterfall::State::row_count, "row_count", "Number of waterfall rows currently retained.">,
|
||||
State_Field<Waterfall, &Waterfall::State::stored_point_count, "stored_point_count", "Total number of spectrum points retained across all rows.">,
|
||||
State_Field<Waterfall, &Waterfall::State::rendered_cell_count, "rendered_cell_count", "Number of raster cells emitted for the latest frame.">>(
|
||||
*waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Constellation_Diagram>::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())
|
||||
.build();
|
||||
place_selection_over(scene.get(), selection.get(), constellation.get());
|
||||
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
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->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.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::points, "points", "Current time-stamped collection of received I/Q samples.">,
|
||||
State_Field<Constellation_Diagram, &Constellation_Diagram::State::point_count, "point_count", "Number of constellation samples currently retained.">>(
|
||||
*constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> 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<Selection_Rectangle_Overlay>::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_Frame_Request& event) {
|
||||
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.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">,
|
||||
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(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
struct Plot::Private {
|
||||
using Scene = std::variant<std::unique_ptr<Scene_2D>, std::unique_ptr<Scene_3D>>;
|
||||
using Frame = std::variant<std::unique_ptr<Frame_2D>, std::unique_ptr<Frame_3D>>;
|
||||
|
||||
Reference in New Issue
Block a user