功能比较完善的一版

This commit is contained in:
2026-08-21 22:12:47 +08:00
parent ae8d2f7272
commit e3d3bfd767
37 changed files with 563 additions and 224 deletions
+108 -28
View File
@@ -13,6 +13,7 @@
#include <atomic>
#include <cmath>
#include <cstring>
#include <memory>
#include <mutex>
#include <numbers>
#include <stdexcept>
@@ -31,6 +32,7 @@ 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>;
using Selection_Object = Impl<Selection_Rectangle_Overlay>;
template <typename Integer>
void append_binary(std::string& output, Integer value) {
@@ -241,6 +243,14 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
|| 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), ...);
@@ -282,6 +292,19 @@ Time_Axis_Object::Builder time_axis_builder(
return builder;
}
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;
@@ -301,6 +324,7 @@ void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) {
template <typename Scene_Object>
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
const auto dispatch = [&](auto event) { scene.dispatch_event(std::move(event)); };
const auto apply_pointer = [&](auto& event) {
event.position = input.position;
event.global_position = input.global_position;
@@ -312,34 +336,33 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
case Event_Type::pointer_move:
case Event_Type::pointer_press:
case Event_Type::pointer_release: {
Basic_Pointer_Event<Point_F> event(input.type);
apply_pointer(event);
static_cast<void>(scene.dispatch_event(event));
auto event = std::make_unique<Basic_Pointer_Event<Point_F>>(input.type);
apply_pointer(*event);
dispatch(std::move(event));
break;
}
case Event_Type::wheel: {
Basic_Wheel_Event<Point_F> event;
apply_pointer(event);
event.pixel_delta_x = input.pixel_delta_x;
event.pixel_delta_y = input.pixel_delta_y;
event.angle_delta_x = input.angle_delta_x;
event.angle_delta_y = input.angle_delta_y;
static_cast<void>(scene.dispatch_event(event));
auto event = std::make_unique<Basic_Wheel_Event<Point_F>>();
apply_pointer(*event);
event->pixel_delta_x = input.pixel_delta_x;
event->pixel_delta_y = input.pixel_delta_y;
event->angle_delta_x = input.angle_delta_x;
event->angle_delta_y = input.angle_delta_y;
dispatch(std::move(event));
break;
}
case Event_Type::key_press:
case Event_Type::key_release: {
Key_Event event(input.type);
event.key = input.key;
event.native_key = input.native_key;
event.modifiers = input.modifiers;
event.auto_repeat = input.auto_repeat;
static_cast<void>(scene.dispatch_event(event));
auto event = std::make_unique<Key_Event>(input.type);
event->key = input.key;
event->native_key = input.native_key;
event->modifiers = input.modifiers;
event->auto_repeat = input.auto_repeat;
dispatch(std::move(event));
break;
}
default: {
Event event(input.type);
static_cast<void>(scene.dispatch_event(event));
dispatch(std::make_unique<Event>(input.type));
break;
}
}
@@ -347,6 +370,43 @@ 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 = *frequency_axis_builder().build();
frequency->template set<&Abs_Axis::Prop::unit_text>("Hz");
auto numeric = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-100.0, 0.0}).build();
numeric->template set<&Abs_Axis::Prop::unit_text>("dB");
auto time = *time_axis_builder(
Axis_Orientation::horizontal, {64.0, 190.0}, 620.0).build();
time->template set<&Abs_Axis::Prop::unit_text>("Time");
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);
auto scene = *scene_builder.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_Event& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, numeric, time);
if (event.input) return;
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 = *frequency_axis_builder().build();
@@ -356,12 +416,15 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
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();
auto selection = selection_overlay(frequency.get(), vertical.get());
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());
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.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_Event& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
@@ -402,7 +465,7 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
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));
*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));
}
@@ -413,18 +476,22 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
auto vertical = *numeric_axis_builder(
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}).build();
auto trace = *Impl<Frequency_Trace>::Builder(time.get(), vertical.get()).build();
auto selection = selection_overlay(time.get(), vertical.get());
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());
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.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_Event& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
if (event.input) return;
constexpr double day_milliseconds = 86'400'000.0;
raw->append_sample(Time_Of_Day{static_cast<std::int64_t>(
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))},
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);
};
@@ -435,7 +502,7 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
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));
*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));
}
@@ -449,12 +516,15 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
.set(&Sweep_Spectrum::Prop::bins_per_block, std::size_t{8})
.set(&Sweep_Spectrum::Prop::block_count, std::size_t{64});
auto sweep = *sweep_builder.build();
auto selection = selection_overlay(frequency.get(), vertical.get());
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());
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
place_selection_over(scene.get(), selection.get(), sweep.get());
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get(), block_index = std::size_t{}](const Plot_Event& event) mutable {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
@@ -487,7 +557,7 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
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, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep));
*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));
}
@@ -501,12 +571,15 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
.set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0})
.set(&Afterglow::Prop::power_point_size, 96);
auto afterglow = *afterglow_builder.build();
auto selection = selection_overlay(frequency.get(), vertical.get());
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());
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.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_Event& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
@@ -531,7 +604,7 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
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));
*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));
}
@@ -544,12 +617,15 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
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();
auto selection = selection_overlay(frequency.get(), time.get());
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());
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.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_Event& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
if (event.input) return;
@@ -559,8 +635,9 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
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;
raw->append_row(Time_Of_Day{static_cast<std::int64_t>(
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))}, values);
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.">,
@@ -579,7 +656,7 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
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));
*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));
}
@@ -593,12 +670,15 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
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();
auto selection = selection_overlay(horizontal.get(), vertical.get());
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());
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.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_Event& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
if (event.input) return;
@@ -627,7 +707,7 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
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));
*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));
}