Files
Aethera/web_server/src/Plot.cpp
T
2026-08-21 14:38:18 +08:00

612 lines
40 KiB
C++

#include "Plot.hpp"
#include "Renderable_Adapter.hpp"
#include <asio/co_spawn.hpp>
#include <asio/error_code.hpp>
#include <asio/experimental/concurrent_channel.hpp>
#include <asio/redirect_error.hpp>
#include <asio/strand.hpp>
#include <asio/use_awaitable.hpp>
#include <render_2D/plottable/Plottables.hpp>
#include <render_3D/Render_3D.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <mutex>
#include <stdexcept>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
namespace aethera::web {
namespace {
using namespace render_2d;
using namespace render_3d;
using Scene_2D = Impl<Render_Scene_2D>;
using Scene_3D = Impl<Render_Scene_3D>;
using Frequency_Axis_Object = Impl<Frequency_Axis>;
using Numeric_Axis_Object = Impl<Numeric_Axis>;
using Time_Axis_Object = Impl<Time_Axis>;
template <typename Integer>
void append_binary(std::string& output, Integer value) {
const auto start = output.size();
output.resize(start + sizeof(Integer));
std::memcpy(output.data() + start, &value, sizeof(Integer));
}
std::string encode_frame(Image_View image, std::uint64_t sequence) {
std::string output;
output.reserve(24 + static_cast<std::size_t>(image.width) * image.height * 4);
append_binary(output, std::uint32_t{0x41544852});
append_binary(output, std::uint16_t{1});
append_binary(output, std::uint16_t{});
append_binary(output, static_cast<std::uint32_t>(image.width));
append_binary(output, static_cast<std::uint32_t>(image.height));
append_binary(output, sequence);
for (int y = 0; y < image.height; ++y) {
const auto* row = reinterpret_cast<const std::uint8_t*>(
image.data + static_cast<std::ptrdiff_t>(y) * image.stride);
for (int x = 0; x < image.width; ++x) {
const auto* pixel = row + x * 4;
output.push_back(static_cast<char>(pixel[2]));
output.push_back(static_cast<char>(pixel[1]));
output.push_back(static_cast<char>(pixel[0]));
output.push_back(static_cast<char>(pixel[3]));
}
}
return output;
}
std::string encode_frame(const Pixel_Frame& frame, std::uint64_t sequence) {
std::string output;
output.reserve(24 + frame.rgba8.size());
append_binary(output, std::uint32_t{0x41544852});
append_binary(output, std::uint16_t{1});
append_binary(output, std::uint16_t{});
append_binary(output, frame.extent.width);
append_binary(output, frame.extent.height);
append_binary(output, sequence);
output.append(reinterpret_cast<const char*>(frame.rgba8.data()), frame.rgba8.size());
return output;
}
struct Schema_Query { Plot::Json_Handler handler; };
struct Prop_Write {
std::string key;
nlohmann::json value;
Plot::Json_Handler handler;
};
using Plot_Input = std::variant<Plot_Event, Schema_Query, Prop_Write>;
template <typename... Owned_Objects>
class Scene_View_Model final : public Plot::Scene_View {
public:
Scene_View_Model(std::unique_ptr<detail::Renderable_Descriptor> value_descriptor,
std::function<void(const Plot_Event&)> value_update,
Owned_Objects... owned_objects)
: descriptor(std::move(value_descriptor)),
update_scene(std::move(value_update)),
objects(std::move(owned_objects)...) {}
nlohmann::json schema() const override { return descriptor->schema(); }
nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override {
return descriptor->write_prop(key, value);
}
void update(const Plot_Event& event) override { update_scene(event); }
private:
std::unique_ptr<detail::Renderable_Descriptor> descriptor;
std::function<void(const Plot_Event&)> 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... Fields, typename Object, typename... Owned_Objects>
std::unique_ptr<Plot::Scene_View> make_scene_view(
Object& object,
std::function<void(const Plot_Event&)> update,
Owned_Objects&&... owned_objects) {
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 std::make_unique<Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>>(
detail::make_renderable_descriptor(Adapter{object}),
std::move(update),
std::forward<Owned_Objects>(owned_objects)...);
}
Frequency_Axis_Object::Builder frequency_axis_builder(Size canvas) {
Frequency_Axis_Object::Builder builder;
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(&Abs_Axis::Prop::canvas_size, canvas)
.set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0});
return builder;
}
Numeric_Axis_Object::Builder numeric_axis_builder(
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length,
Axis_Range range, Size canvas) {
Numeric_Axis_Object::Builder builder;
builder
.set(&Abs_Axis::Prop::orientation, orientation)
.set(&Abs_Axis::Prop::position, position)
.set(&Abs_Axis::Prop::pixel_length, length)
.set(&Abs_Axis::Prop::canvas_size, canvas)
.set(&Numeric_Axis::Prop::coordinate_range, range);
return builder;
}
Time_Axis_Object::Builder time_axis_builder(
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) {
Time_Axis_Object::Builder builder;
builder
.set(&Abs_Axis::Prop::orientation, orientation)
.set(&Abs_Axis::Prop::position, position)
.set(&Abs_Axis::Prop::pixel_length, length)
.set(&Abs_Axis::Prop::canvas_size, canvas);
return builder;
}
template <typename... Axes>
void resize_axes(Size viewport, Axes*... axes) {
(axes->template set<&Abs_Axis::Prop::canvas_size>(viewport), ...);
}
}
std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
constexpr Size canvas{720, 420};
auto frequency = *frequency_axis_builder(canvas).build();
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build();
Impl<Spectrum>::Builder spectrum_builder(frequency.get(), vertical.get());
spectrum_builder.set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
.set(&Spectrum::Prop::max_hold_visible, true);
auto spectrum = *spectrum_builder.build();
Scene_2D::Builder scene_builder;
scene_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);
scene_builder.add_renderable(spectrum.get());
auto scene = *scene_builder.build();
auto update = [raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({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, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum));
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 = *time_axis_builder(
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas).build();
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas).build();
auto trace = *Impl<Frequency_Trace>::Builder(time.get(), vertical.get()).build();
Scene_2D::Builder scene_builder;
scene_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);
scene_builder.add_renderable(trace.get());
auto scene = *scene_builder.build();
auto update = [raw = trace.get(), time = time.get(), vertical = vertical.get(), tick = std::uint64_t{}](const Plot_Event& event) mutable {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
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, std::move(update), std::move(time), std::move(vertical), std::move(trace));
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 = *frequency_axis_builder(canvas).build();
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build();
Impl<Sweep_Spectrum>::Builder sweep_builder(frequency.get(), vertical.get());
sweep_builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0});
auto sweep = *sweep_builder.build();
Scene_2D::Builder scene_builder;
scene_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);
scene_builder.add_renderable(sweep.get());
auto scene = *scene_builder.build();
auto update = [raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
std::array<double, 64> values{};
for (std::size_t i = 0; i < values.size(); ++i)
values[i] = -90.0 + 35.0 * std::sin(i * 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", "Current collection of incremental sweep blocks.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of sweep blocks currently retained.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of frequency points retained across all blocks.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest sweep frame.">>(
*sweep, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep));
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 = *frequency_axis_builder(canvas).build();
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build();
Impl<Afterglow>::Builder afterglow_builder(frequency.get(), vertical.get());
afterglow_builder.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);
auto afterglow = *afterglow_builder.build();
Scene_2D::Builder scene_builder;
scene_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);
scene_builder.add_renderable(afterglow.get());
auto scene = *scene_builder.build();
auto update = [raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({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, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow));
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 = *frequency_axis_builder(canvas).build();
auto time = *time_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas).build();
Impl<Waterfall>::Builder waterfall_builder(frequency.get(), time.get());
waterfall_builder.set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0})
.set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0});
auto waterfall = *waterfall_builder.build();
Scene_2D::Builder scene_builder;
scene_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);
scene_builder.add_renderable(waterfall.get());
auto scene = *scene_builder.build();
auto update = [raw = waterfall.get(), frequency = frequency.get(), time = time.get(), tick = std::uint64_t{}](const Plot_Event& event) mutable {
resize_axes({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));
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, std::move(update), std::move(frequency), std::move(time), std::move(waterfall));
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 = *numeric_axis_builder(
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}, canvas).build();
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas).build();
Impl<Constellation_Diagram>::Builder constellation_builder(horizontal.get(), vertical.get());
constellation_builder.set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2})
.set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2});
auto constellation = *constellation_builder.build();
Scene_2D::Builder scene_builder;
scene_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);
scene_builder.add_renderable(constellation.get());
auto scene = *scene_builder.build();
auto update = [raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
const double phase = event.time_milliseconds * 0.003;
raw->append_point({std::cos(phase) * 0.82 + 0.04 * std::sin(phase * 7.0),
std::sin(phase) * 0.82 + 0.04 * std::cos(phase * 5.0)});
};
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, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation));
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 = *numeric_axis_builder(
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}, canvas).build();
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}, canvas).build();
auto selection = *Impl<Selection_Rectangle_Overlay>::Builder(horizontal.get(), vertical.get()).build();
Scene_2D::Builder scene_builder;
scene_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);
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
auto update = [horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
resize_axes({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, 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>>;
asio::strand<asio::any_io_executor> strand;
asio::experimental::concurrent_channel<void(asio::error_code, Plot_Input)> inputs;
Scene scene;
std::unique_ptr<Scene_View> view;
std::once_flag start_once;
std::mutex handlers_mutex;
std::unordered_map<const void*, Frame_Handler> handlers;
std::atomic_uint64_t frame_sequence{};
template <typename Scene_Object>
Private(asio::any_io_executor executor,
std::unique_ptr<Scene_Object> value_scene,
std::unique_ptr<Scene_View> value_view)
: strand(asio::make_strand(std::move(executor))), inputs(strand, 32),
scene(std::move(value_scene)), view(std::move(value_view)) {}
void publish(std::string pixels) {
std::vector<Frame_Handler> outputs;
{
std::lock_guard lock(handlers_mutex);
outputs.reserve(handlers.size());
for (const auto& [owner, handler] : handlers) outputs.push_back(handler);
}
for (auto& output : outputs) output(pixels);
}
};
Plot::Plot(asio::any_io_executor executor,
std::unique_ptr<Scene_2D> scene,
std::unique_ptr<Scene_View> view)
: d(std::make_unique<Private>(std::move(executor), std::move(scene), std::move(view))) {}
Plot::Plot(asio::any_io_executor executor,
std::unique_ptr<Scene_3D> scene,
std::unique_ptr<Scene_View> view)
: d(std::make_unique<Private>(std::move(executor), std::move(scene), std::move(view))) {}
Plot::~Plot() { d->inputs.close(); }
void Plot::ensure_started() {
std::call_once(d->start_once, [this] {
auto self = shared_from_this();
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) {
(*scene)->set_frame_callback([weak = weak_from_this()](Image_View image) {
if (auto owner = weak.lock()) {
const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1;
owner->d->publish(encode_frame(image, sequence));
}
});
} else {
std::get<std::unique_ptr<Scene_3D>>(d->scene)->set_frame_callback(
[weak = weak_from_this()](std::shared_ptr<const Pixel_Frame> frame) {
if (auto owner = weak.lock()) {
const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1;
owner->d->publish(encode_frame(*frame, sequence));
}
});
}
asio::co_spawn(d->strand, [self]() -> asio::awaitable<void> {
for (;;) {
asio::error_code error;
auto input = co_await self->d->inputs.async_receive(
asio::redirect_error(asio::use_awaitable, error));
if (error) co_return;
if (auto* query = std::get_if<Schema_Query>(&input)) {
query->handler(self->d->view->schema());
continue;
}
if (auto* write = std::get_if<Prop_Write>(&input)) {
write->handler(self->d->view->write_prop(write->key, write->value));
continue;
}
auto event = std::get<Plot_Event>(input);
event.width = std::clamp(event.width, 160U, 1920U);
event.height = std::clamp(event.height, 120U, 1080U);
self->d->view->update(event);
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&self->d->scene)) {
(*scene)->set<&Render_Scene_2D::Prop::viewport>(
Size{static_cast<int>(event.width), static_cast<int>(event.height)});
(*scene)->render();
} else {
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(self->d->scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{event.width, event.height});
scene_3d->render();
}
}
}, [](std::exception_ptr exception) {
if (exception) std::rethrow_exception(exception);
});
});
}
void Plot::attach(const void* owner, Frame_Handler handler) {
ensure_started();
{
std::lock_guard lock(d->handlers_mutex);
d->handlers.insert_or_assign(owner, std::move(handler));
}
submit({});
}
void Plot::detach(const void* owner) {
std::lock_guard lock(d->handlers_mutex);
d->handlers.erase(owner);
}
void Plot::submit(Plot_Event event) {
ensure_started();
static_cast<void>(d->inputs.try_send(asio::error_code{}, Plot_Input{event}));
}
void Plot::async_schema(Json_Handler handler) {
ensure_started();
if (!d->inputs.try_send(asio::error_code{}, Plot_Input{Schema_Query{std::move(handler)}}))
throw std::runtime_error("plot input queue is unavailable");
}
void Plot::async_write_prop(std::string key, nlohmann::json value, Json_Handler handler) {
ensure_started();
if (!d->inputs.try_send(asio::error_code{}, Plot_Input{
Prop_Write{std::move(key), std::move(value), std::move(handler)}}))
throw std::runtime_error("plot input queue is unavailable");
}
std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
using Visual_Object = Impl<Point_Visual>;
auto visual = *Visual_Object::Builder{}.build();
static_cast<void>(visual->update_items({
render_3d::Point{.position = {-0.55F, -0.2F, 0.0F}, .color = Color::red_color(), .diameter_px = 24.0F},
render_3d::Point{.position = {0.0F, 0.5F, 0.0F}, .color = Color::green_color(), .diameter_px = 30.0F},
render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F}}));
visual->advance();
auto scene = *Scene_3D::Builder(visual.get())
.set(&Render_Scene_3D::Prop::viewport, Extent{720, 420})
.set(&Render_Scene_3D::Prop::view_active, true)
.build();
using Adapter = detail::Renderable_Adapter<Visual_Object,
detail::Prop_Field<&Point_Visual::Prop::transform, "transform", "World transform applied to every point in the visual.">,
detail::Prop_Field<&Point_Visual::Prop::visible, "visible", "Controls whether the point visual participates in scene rendering.">,
detail::Prop_Field<&Point_Visual::Prop::depth_test, "depth_test", "Enables depth testing when point fragments are rendered.">,
detail::Prop_Field<&Point_Visual::Prop::items, "items", "Complete collection of 3D points submitted to the visual.">,
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::item_count, "item_count", "Number of point items currently published by the visual.">,
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::prepared_item_count, "prepared_item_count", "Number of point items prepared for the latest GPU submission.">,
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::prepared_revision, "prepared_revision", "Property revision represented by the currently prepared GPU data.">>;
auto view = std::make_unique<Scene_View_Model<decltype(visual)>>(
detail::make_renderable_descriptor(Adapter{*visual}),
[](const Plot_Event&) {}, std::move(visual));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
}