diff --git a/Kernel/src/renderive/state/Double_State_Strategy.hpp b/Kernel/src/renderive/state/Double_State_Strategy.hpp index 6d24875..c53cc1f 100644 --- a/Kernel/src/renderive/state/Double_State_Strategy.hpp +++ b/Kernel/src/renderive/state/Double_State_Strategy.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -30,23 +31,21 @@ struct Double_State_Strategy : That, State_Strategy_Base { explicit Double_State_Strategy(With_Observer option) requires std::default_initializable && std::default_initializable : That(), observer(std::move(option.observer)), states{}, render_state(&states[0]), cache_state(&states[1]), scratch_state(&states[2]) {} template - requires std::constructible_from explicit Double_State_Strategy(const State& state, Args&&... args) : That(std::forward(args)...), states{state, state, state}, render_state(&states[0]), cache_state(&states[1]), scratch_state(&states[2]) {} template - requires std::constructible_from Double_State_Strategy(const State& state, With_Observer option, Args&&... args) : That(std::forward(args)...), observer(std::move(option.observer)), states{state, state, state}, render_state(&states[0]), cache_state(&states[1]), scratch_state(&states[2]) {} template Value> Self& set(Value&& value) { - Observation observation; + std::optional observation; { std::lock_guard lock(mtx); cache_state->*Member = std::forward(value); ++cache_update_count; - observation = {Observation_Event::cache_updated, observer.now_ns(), cache_update_count, publish_count, *cache_state}; + observation.emplace(Observation_Event::cache_updated, observer.now_ns(), cache_update_count, publish_count, *cache_state); } - observer.observe(observation); + observer.observe(*observation); return *this; } template @@ -59,6 +58,25 @@ struct Double_State_Strategy : That, State_Strategy_Base { return value; } } + template + requires std::invocable + auto read(Read&& value) const { + std::lock_guard lock(mtx); + return std::invoke(std::forward(value), *cache_state); + } + template + requires std::invocable + Self& update(Update&& value) { + std::optional observation; + { + std::lock_guard lock(mtx); + std::invoke(std::forward(value), *cache_state); + ++cache_update_count; + observation.emplace(Observation_Event::cache_updated, observer.now_ns(), cache_update_count, publish_count, *cache_state); + } + observer.observe(*observation); + return *this; + } void publish() override { std::optional observation; { diff --git a/Qt/tests/Qt_Bridge_Tests.cpp b/Qt/tests/Qt_Bridge_Tests.cpp index 94b9685..4c8575a 100644 --- a/Qt/tests/Qt_Bridge_Tests.cpp +++ b/Qt/tests/Qt_Bridge_Tests.cpp @@ -24,10 +24,10 @@ TEST(Renderive_Qt, WidgetLifecycleDrivesAndStopsKernelScene) { .set_pixel_length(90) .set_coord_range({1.0, 0.0}) .build(); - auto spectrum = Spectrum::Builder(plot.root_renderable(), x_axis, y_axis) - .set_frequency_range({0.0, 10.0}) - .set_frequency_point_size(8) - .build(); + auto spectrum = Spectrum::Builder{} + .set<&Spectrum::Properties::frequency_range>(Range{0.0, 10.0}) + .set<&Spectrum::Properties::frequency_point_size>(8) + .build(plot.root_renderable(), x_axis, y_axis); spectrum->update_samples(std::vector{0.1, 0.3, 0.8, 0.5, 0.9, 0.4, 0.2, 0.7}); plot.show(); QTimer::singleShot(150, application, &QCoreApplication::quit); diff --git a/render_2D/plot/Plot_Core.cpp b/render_2D/plot/Plot_Core.cpp index 18eb7ef..c1d6b99 100644 --- a/render_2D/plot/Plot_Core.cpp +++ b/render_2D/plot/Plot_Core.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -352,6 +353,12 @@ bool Plot_Core::prepare_frame() { auto paint_frame = scene.frame_control.acquire_painter(); if (!paint_frame) return false; + const auto topology = scene.topology_snapshot(); + for (const auto& entry : topology.display) { + auto base = std::const_pointer_cast<::Renderable_Base>(entry.child); + if (auto state = std::dynamic_pointer_cast<::State_Strategy_Base>(base)) + state->publish(); + } auto& scene_state = static_cast::Scene_State_Strategy&>(scene); scene_state.template set<&::Scene2D_State::revision>(scene_state.state_revision() + 1); scene_state.publish(); diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp new file mode 100644 index 0000000..9304398 --- /dev/null +++ b/render_2D/plottable/Afterglow.cpp @@ -0,0 +1,88 @@ +#include "Afterglow.h" +#include "Heatmap_Utils.h" +#include "../render/Blend2D_Cache.h" +#include +#include +namespace renderive { +namespace detail { +namespace { +struct Afterglow_Runtime_Base {}; +struct Afterglow_Runtime { + std::deque> history; +}; +using Afterglow_Runtime_State = Double_State_Strategy; +} +struct Afterglow_Control::Impl { + Impl(std::shared_ptr frequency, std::shared_ptr power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {} + std::shared_ptr frequency_axis; + std::shared_ptr power_axis; + Afterglow_Runtime_State runtime; +}; +Afterglow_Control::Afterglow_Control(Plot_Core& plot, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) + : Plottable_State(plot, properties), impl_(std::make_unique(std::move(frequency_axis), std::move(power_axis))) {} +Afterglow_Control::~Afterglow_Control() = default; +std::size_t Afterglow_Control::history_count() const { + return impl_->runtime.read([](const Afterglow_Runtime& runtime) { return runtime.history.size(); }); +} +std::size_t Afterglow_Control::latest_spectrum_point_count() const { + return impl_->runtime.read([](const Afterglow_Runtime& runtime) { return runtime.history.empty() ? 0 : runtime.history.back().size(); }); +} +std::size_t Afterglow_Control::rendered_cell_count() const { + const auto state = properties(); + const auto runtime = impl_->runtime.read([](const Afterglow_Runtime& value) { return value; }); + if(runtime.history.empty()) + return 0; + const int width = std::min(state.frequency_point_size.get(), static_cast(runtime.history.back().size())); + const int height = state.power_point_size.get() > 0 ? state.power_point_size.get() : std::max(1, static_cast(impl_->power_axis->pixel_length())); + return width > 0 && height > 0 ? static_cast(width) * static_cast(height) : 0; +} +void Afterglow_Control::append_spectrum(std::span values) { + if(get<&Afterglow_Properties::frequency_point_size>() <= 0) + set<&Afterglow_Properties::frequency_point_size>(static_cast(values.size())); + impl_->runtime.update([values](Afterglow_Runtime& runtime) { + runtime.history.emplace_back(values.begin(), values.end()); + while(runtime.history.size() > 64) + runtime.history.pop_front(); + }); + changed(); +} +void Afterglow_Control::append_spectrum(std::pmr::vector&& values) { + append_spectrum(std::span(values.data(), values.size())); +} +void Afterglow_Control::publish() { + publish_properties(); + impl_->runtime.publish(); +} +void Afterglow_Control::paint(Painter& painter) { + const auto state = render_properties(); + const auto runtime = impl_->runtime.render_use_state(); + if(runtime.history.empty()) + return; + const int width = std::min(state.frequency_point_size.get(), static_cast(runtime.history.back().size())); + const int height = state.power_point_size.get() > 0 ? state.power_point_size.get() : std::max(1, static_cast(impl_->power_axis->pixel_length())); + if(width <= 0 || height <= 0) + return; + std::vector intensity(static_cast(width) * height); + double weight = 1.0; + const double decay = 1.0 - state.attenuation_rate.get(); + for(auto iterator = runtime.history.rbegin(); iterator != runtime.history.rend(); ++iterator) { + const int count = std::min(width, static_cast(iterator->size())); + for(int x = 0; x < count; ++x) { + const double normalized = normalized_value((*iterator)[static_cast(x)], state.power_range); + const int y = std::clamp(height - 1 - static_cast(normalized * (height - 1)), 0, height - 1); + intensity[static_cast(y) * width + x] += weight; + if(state.interpolate && y + 1 < height) + intensity[static_cast(y + 1) * width + x] += weight * 0.35; + } + weight *= decay; + if(weight < 0.01) + break; + } + const double maximum = std::max(1.0, *std::max_element(intensity.begin(), intensity.end())); + std::vector pixels(intensity.size()); + for(std::size_t index = 0; index < pixels.size(); ++index) + pixels[index] = state.color_map.at_normalized(intensity[index] / maximum); + painter.heatmap(mapped_rect(impl_->frequency_axis->transform(), impl_->power_axis->transform(), state.frequency_range, state.power_range), width, height, pixels, Image_Interpolation_Mode::Bilinear); +} +} +} diff --git a/render_2D/plottable/Afterglow.h b/render_2D/plottable/Afterglow.h new file mode 100644 index 0000000..e42b228 --- /dev/null +++ b/render_2D/plottable/Afterglow.h @@ -0,0 +1,39 @@ +#pragma once +#include "Plottable.h" +#include "../axis/Axis.h" +#include +#include +namespace renderive { +struct Afterglow_Properties { + Range frequency_range{0.0, 10.0}; + Range power_range{0.0, 10.0}; + Nonnegative_Count frequency_point_size; + Nonnegative_Count power_point_size; + bool interpolate = true; + Unit_Interval attenuation_rate{0.2}; + Color_Map color_map; +}; +namespace detail { +class LIB_DECL Afterglow_Control : public Plottable_State { +public: + Afterglow_Control(Plot_Core& plot, const Afterglow_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + ~Afterglow_Control() override; + [[nodiscard]] std::size_t history_count() const; + [[nodiscard]] std::size_t latest_spectrum_point_count() const; + [[nodiscard]] std::size_t rendered_cell_count() const; + void append_spectrum(std::span values); + void append_spectrum(std::pmr::vector&& values); + template + void append_spectrum(const Values& values) { + append_spectrum(std::span(values.data(), values.size())); + } +protected: + void paint(Painter& painter) override; +private: + struct Impl; + std::unique_ptr impl_; + void publish() override; +}; +} +using Afterglow = detail::Attach_Plottable; +} diff --git a/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/plottable/Constellation_Diagram.cpp new file mode 100644 index 0000000..0e05a01 --- /dev/null +++ b/render_2D/plottable/Constellation_Diagram.cpp @@ -0,0 +1,74 @@ +#include "Constellation_Diagram.h" +#include "../render/Blend2D_Cache.h" +#include +#include +#include +#include +#include +namespace renderive { +namespace detail { +namespace { +struct Timed_Point { + PointF point; + std::chrono::steady_clock::time_point time; +}; +struct Constellation_Runtime_Base {}; +struct Constellation_Runtime { + std::deque points; +}; +using Constellation_Runtime_State = Double_State_Strategy; +} +struct Constellation_Diagram_Control::Impl { + Impl(std::shared_ptr i, std::shared_ptr q) : i_axis(std::move(i)), q_axis(std::move(q)) {} + std::shared_ptr i_axis; + std::shared_ptr q_axis; + Constellation_Runtime_State runtime; +}; +Constellation_Diagram_Control::Constellation_Diagram_Control(Plot_Core& plot, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis) + : Plottable_State(plot, properties), impl_(std::make_unique(std::move(i_axis), std::move(q_axis))) {} +Constellation_Diagram_Control::~Constellation_Diagram_Control() = default; +void Constellation_Diagram_Control::append_point(PointF point) { + const auto now = std::chrono::steady_clock::now(); + const int lifetime = get<&Constellation_Diagram_Properties::point_lifetime_ms>(); + impl_->runtime.update([point, now, lifetime](Constellation_Runtime& runtime) { + runtime.points.push_back({point, now}); + const auto cutoff = now - std::chrono::milliseconds(lifetime); + while(!runtime.points.empty() && runtime.points.front().time < cutoff) + runtime.points.pop_front(); + }); + changed(); +} +std::size_t Constellation_Diagram_Control::point_count() const { + return impl_->runtime.read([](const Constellation_Runtime& runtime) { return runtime.points.size(); }); +} +void Constellation_Diagram_Control::fit_square_to_axes() { + const auto state = properties(); + const double side = std::max(state.i_range.size(), state.q_range.size()); + impl_->i_axis->set_coord_range({state.i_range.center() - side * 0.5, state.i_range.center() + side * 0.5}); + impl_->q_axis->set_coord_range({state.q_range.center() + side * 0.5, state.q_range.center() - side * 0.5}); +} +void Constellation_Diagram_Control::publish() { + publish_properties(); + impl_->runtime.publish(); +} +void Constellation_Diagram_Control::paint(Painter& painter) { + const auto state = render_properties(); + const auto runtime = impl_->runtime.render_use_state(); + const Axis_Transform x = impl_->i_axis->transform(); + const Axis_Transform y = impl_->q_axis->transform(); + const int count = static_cast(state.type); + const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; + for(int index = 0; index < count; ++index) { + const double angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; + const PointF point{state.i_range.center() + std::cos(angle) * radius, state.q_range.center() + std::sin(angle) * radius}; + painter.circle({x.coord_to_pixel(point.x), y.coord_to_pixel(point.y)}, 3.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::Solid}); + } + const auto cutoff = std::chrono::steady_clock::now() - std::chrono::milliseconds(state.point_lifetime_ms.get()); + for(const auto& value : runtime.points) { + if(value.time < cutoff) + continue; + painter.circle({x.coord_to_pixel(value.point.x), y.coord_to_pixel(value.point.y)}, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::Solid}); + } +} +} +} diff --git a/render_2D/plottable/Constellation_Diagram.h b/render_2D/plottable/Constellation_Diagram.h new file mode 100644 index 0000000..4b9015c --- /dev/null +++ b/render_2D/plottable/Constellation_Diagram.h @@ -0,0 +1,36 @@ +#pragma once +#include "Plottable.h" +#include "../axis/Axis.h" +namespace renderive { +enum class Constellation_Diagram_Type : std::uint8_t { + Psk4 = 4, + Psk8 = 8, + Psk16 = 16 +}; +struct Constellation_Diagram_Properties { + Range i_range{0.0, 100.0}; + Range q_range{0.0, 100.0}; + Color point_color = Color::red(); + Color anchor_color = Color::yellow(); + Nonnegative_Count point_lifetime_ms{1000}; + Constellation_Diagram_Type type = Constellation_Diagram_Type::Psk8; + double phase_offset_radians{}; +}; +namespace detail { +class LIB_DECL Constellation_Diagram_Control : public Plottable_State { +public: + Constellation_Diagram_Control(Plot_Core& plot, const Constellation_Diagram_Properties& properties, std::shared_ptr i_axis, std::shared_ptr q_axis); + ~Constellation_Diagram_Control() override; + void append_point(PointF point); + [[nodiscard]] std::size_t point_count() const; + void fit_square_to_axes(); +protected: + void paint(Painter& painter) override; +private: + struct Impl; + std::unique_ptr impl_; + void publish() override; +}; +} +using Constellation_Diagram = detail::Attach_Plottable; +} diff --git a/render_2D/plottable/Frequency_Trace.cpp b/render_2D/plottable/Frequency_Trace.cpp new file mode 100644 index 0000000..f32f986 --- /dev/null +++ b/render_2D/plottable/Frequency_Trace.cpp @@ -0,0 +1,60 @@ +#include "Frequency_Trace.h" +#include "../render/Blend2D_Cache.h" +#include +#include +#include +namespace renderive { +namespace detail { +namespace { +struct Frequency_Trace_Runtime_Base {}; +struct Frequency_Trace_Runtime { + std::deque> samples; +}; +using Frequency_Trace_Runtime_State = Double_State_Strategy; +} +struct Frequency_Trace_Control::Impl { + Impl(std::shared_ptr time, std::shared_ptr value) : time_axis(std::move(time)), value_axis(std::move(value)) {} + std::shared_ptr time_axis; + std::shared_ptr value_axis; + Frequency_Trace_Runtime_State runtime; +}; +Frequency_Trace_Control::Frequency_Trace_Control(Plot_Core& plot, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis) + : Plottable_State(plot, properties), impl_(std::make_unique(std::move(time_axis), std::move(value_axis))) {} +Frequency_Trace_Control::~Frequency_Trace_Control() = default; +void Frequency_Trace_Control::append_sample(int tick, double value) { + const int limit = std::max(2, impl_->time_axis->visible_time_point_count()); + impl_->runtime.update([tick, value, limit](Frequency_Trace_Runtime& runtime) { + runtime.samples.emplace_back(tick, value); + while(runtime.samples.size() > static_cast(limit)) + runtime.samples.pop_front(); + }); + changed(); +} +void Frequency_Trace_Control::append_sample(Time_Of_Day time, double value) { + append_sample(impl_->time_axis->append_time(time), value); +} +std::size_t Frequency_Trace_Control::sample_count() const { + return impl_->runtime.read([](const Frequency_Trace_Runtime& runtime) { return runtime.samples.size(); }); +} +std::size_t Frequency_Trace_Control::rendered_point_count() const { + return sample_count() >= 2 ? sample_count() : 0; +} +void Frequency_Trace_Control::publish() { + publish_properties(); + impl_->runtime.publish(); +} +void Frequency_Trace_Control::paint(Painter& painter) { + const auto state = render_properties(); + const auto runtime = impl_->runtime.render_use_state(); + if(runtime.samples.size() < 2) + return; + const Axis_Transform x = impl_->time_axis->transform(); + const Axis_Transform y = impl_->value_axis->transform(); + std::vector points; + points.reserve(runtime.samples.size()); + for(const auto& [tick, value] : runtime.samples) + points.push_back({x.coord_to_pixel(tick), y.coord_to_pixel(value)}); + painter.polyline(points, state.pen); +} +} +} diff --git a/render_2D/plottable/Frequency_Trace.h b/render_2D/plottable/Frequency_Trace.h new file mode 100644 index 0000000..eaf407b --- /dev/null +++ b/render_2D/plottable/Frequency_Trace.h @@ -0,0 +1,26 @@ +#pragma once +#include "Plottable.h" +#include "../axis/Axis.h" +namespace renderive { +struct Frequency_Trace_Properties { + Pen pen{Color::yellow()}; +}; +namespace detail { +class LIB_DECL Frequency_Trace_Control : public Plottable_State { +public: + Frequency_Trace_Control(Plot_Core& plot, const Frequency_Trace_Properties& properties, std::shared_ptr time_axis, std::shared_ptr value_axis); + ~Frequency_Trace_Control() override; + void append_sample(int tick, double value); + void append_sample(Time_Of_Day time, double value); + [[nodiscard]] std::size_t sample_count() const; + [[nodiscard]] std::size_t rendered_point_count() const; +protected: + void paint(Painter& painter) override; +private: + struct Impl; + std::unique_ptr impl_; + void publish() override; +}; +} +using Frequency_Trace = detail::Attach_Plottable; +} diff --git a/render_2D/plottable/Heatmap_Utils.h b/render_2D/plottable/Heatmap_Utils.h new file mode 100644 index 0000000..e3bb112 --- /dev/null +++ b/render_2D/plottable/Heatmap_Utils.h @@ -0,0 +1,48 @@ +#pragma once +#include "../axis/Axis.h" +#include +#include +#include +namespace renderive::detail { +inline RectF mapped_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical, Range horizontal_range, Range vertical_range) { + const double x1 = horizontal.coord_to_pixel(horizontal_range.origin); + const double x2 = horizontal.coord_to_pixel(horizontal_range.target); + const double y1 = vertical.coord_to_pixel(vertical_range.origin); + const double y2 = vertical.coord_to_pixel(vertical_range.target); + return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; +} +inline double normalized_value(double value, Range range) { + if(range.length() == 0.0) + return 0.0; + return std::clamp((value - range.origin) / range.length(), 0.0, 1.0); +} +struct Frequency_Columns { + int first{}; + int last{}; + Range range; +}; +inline std::optional frequency_columns(Range data_range, Range visible_range, int column_count, bool visible_only) { + if(column_count <= 0) + return std::nullopt; + if(!visible_only || column_count == 1 || data_range.length() == 0.0) + return Frequency_Columns{0, column_count - 1, data_range}; + const auto [data_low, data_high] = std::minmax(data_range.origin, data_range.target); + const auto [visible_low, visible_high] = std::minmax(visible_range.origin, visible_range.target); + const double clipped_low = std::max(data_low, visible_low); + const double clipped_high = std::min(data_high, visible_high); + if(clipped_low > clipped_high) + return std::nullopt; + const auto position = [data_range, column_count](double coordinate) { return (coordinate - data_range.origin) / data_range.length() * static_cast(column_count - 1); }; + const auto [position_low, position_high] = std::minmax(position(clipped_low), position(clipped_high)); + int first = std::clamp(static_cast(std::floor(position_low)), 0, column_count - 1); + int last = std::clamp(static_cast(std::ceil(position_high)), first, column_count - 1); + if(first == last) { + if(last + 1 < column_count) + ++last; + else if(first > 0) + --first; + } + const auto coordinate = [data_range, column_count](int index) { return data_range.origin + data_range.length() * static_cast(index) / static_cast(column_count - 1); }; + return Frequency_Columns{first, last, {coordinate(first), coordinate(last)}}; +} +} diff --git a/render_2D/plottable/Heatmaps.cpp b/render_2D/plottable/Heatmaps.cpp deleted file mode 100644 index 7ab1658..0000000 --- a/render_2D/plottable/Heatmaps.cpp +++ /dev/null @@ -1,484 +0,0 @@ -#include "Plottables.h" - -#include "../plot/Plot_Core.h" -#include "../render/Blend2D_Cache.h" - -#include -#include -#include -#include -#include - -namespace renderive { -namespace { - -RectF mapped_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical, - Range horizontal_range, Range vertical_range) { - const double x1 = horizontal.coord_to_pixel(horizontal_range.origin); - const double x2 = horizontal.coord_to_pixel(horizontal_range.target); - const double y1 = vertical.coord_to_pixel(vertical_range.origin); - const double y2 = vertical.coord_to_pixel(vertical_range.target); - return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; -} - -double normalized_value(double value, Range range) { - if (range.length() == 0.0) - return 0.0; - return std::clamp((value - range.origin) / range.length(), 0.0, 1.0); -} - -struct Frequency_Columns { - int first{}; - int last{}; - Range range; -}; - -std::optional frequency_columns(Range data_range, - Range visible_range, - int column_count, - bool visible_only) { - if (column_count <= 0) - return std::nullopt; - if (!visible_only || column_count == 1 || data_range.length() == 0.0) - return Frequency_Columns{0, column_count - 1, data_range}; - - const auto [data_low, data_high] = std::minmax(data_range.origin, data_range.target); - const auto [visible_low, visible_high] = std::minmax(visible_range.origin, - visible_range.target); - const double clipped_low = std::max(data_low, visible_low); - const double clipped_high = std::min(data_high, visible_high); - if (clipped_low > clipped_high) - return std::nullopt; - - const auto position = [data_range, column_count](double coordinate) { - return (coordinate - data_range.origin) / data_range.length() * - static_cast(column_count - 1); - }; - const auto [position_low, position_high] = - std::minmax(position(clipped_low), position(clipped_high)); - int first = std::clamp(static_cast(std::floor(position_low)), - 0, column_count - 1); - int last = std::clamp(static_cast(std::ceil(position_high)), - first, column_count - 1); - if (first == last) { - if (last + 1 < column_count) - ++last; - else if (first > 0) - --first; - } - const auto coordinate = [data_range, column_count](int index) { - return data_range.origin + data_range.length() * static_cast(index) / - static_cast(column_count - 1); - }; - return Frequency_Columns{first, last, {coordinate(first), coordinate(last)}}; -} - -} // namespace - -Waterfall::Waterfall(Plot_Core& plot, - std::shared_ptr frequency_axis, - std::shared_ptr time_axis) - : Renderable(plot) { - state_.frequency_axis = std::move(frequency_axis); - state_.time_axis = std::move(time_axis); -} - -void Waterfall::append_row(int tick, std::span values) { - std::shared_ptr axis; - { - std::lock_guard lock(mutex_); - state_.rows.push_back(Row{tick, {values.begin(), values.end()}}); - if (state_.bin_count <= 0) - state_.bin_count = static_cast(values.size()); - axis = state_.time_axis; - } - const std::size_t limit = axis - ? static_cast(std::max(2, axis->visible_time_point_count())) - : 256; - { - std::lock_guard lock(mutex_); - while (state_.rows.size() > limit) - state_.rows.pop_front(); - } - changed(); -} - -void Waterfall::append_row(int tick, std::pmr::vector&& values) { - append_row(tick, std::span(values.data(), values.size())); -} - -void Waterfall::append_row(Time_Of_Day time, std::span values) { - auto axis = time_axis(); - if (!axis) - return; - append_row(axis->append_time(time), values); -} - -void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { - append_row(time, std::span(values.data(), values.size())); -} - -std::shared_ptr Waterfall::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } -std::shared_ptr Waterfall::time_axis() const { std::lock_guard lock(mutex_); return state_.time_axis; } - -#define RENDERIVE_WATERFALL_PROPERTY(Type, Method, Field) \ - Type Waterfall::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ - void Waterfall::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } - -RENDERIVE_WATERFALL_PROPERTY(Range, frequency_range, frequency_range) -RENDERIVE_WATERFALL_PROPERTY(Range, power_range, power_range) -RENDERIVE_WATERFALL_PROPERTY(bool, visible_range_only, visible_only) -RENDERIVE_WATERFALL_PROPERTY(Image_Interpolation_Mode, interpolation_mode, interpolation) - -#undef RENDERIVE_WATERFALL_PROPERTY - -int Waterfall::frequency_bin_count() const { - std::lock_guard lock(mutex_); - return state_.bin_count; -} - -void Waterfall::set_frequency_bin_count(int value) { - { - std::lock_guard lock(mutex_); - state_.bin_count = std::max(0, value); - } - changed(); -} - -std::size_t Waterfall::row_count() const { - std::lock_guard lock(mutex_); - return state_.rows.size(); -} - -std::size_t Waterfall::stored_point_count() const { - std::lock_guard lock(mutex_); - std::size_t count{}; - for (const auto& row : state_.rows) - count += row.values.size(); - return count; -} - -std::size_t Waterfall::rendered_cell_count() const { - const State state = state_snapshot(); - if (!state.frequency_axis || state.rows.empty()) - return 0; - const int source_width = std::min( - state.bin_count, - static_cast(std::min_element( - state.rows.begin(), state.rows.end(), - [](const Row& left, const Row& right) { - return left.values.size() < right.values.size(); - })->values.size())); - if (source_width <= 0) - return 0; - const auto columns = frequency_columns(state.frequency_range, - state.frequency_axis->coord_range(), - source_width, state.visible_only); - return columns ? static_cast(columns->last - columns->first + 1) * - state.rows.size() - : 0; -} - -Waterfall::State Waterfall::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } - -void Waterfall::handle_event(const Event& event) { update_hover(event); } - -void Waterfall::paint(detail::Painter& painter) { - const State state = state_snapshot(); - if (!state.frequency_axis || !state.time_axis || state.rows.empty()) - return; - const int source_width = std::min(state.bin_count, - static_cast(std::min_element( - state.rows.begin(), state.rows.end(), - [](const Row& left, const Row& right) { - return left.values.size() < right.values.size(); - })->values.size())); - const int height = static_cast(state.rows.size()); - if (source_width <= 0 || height <= 0) - return; - const Axis_Transform horizontal = state.frequency_axis->transform(); - const auto columns = frequency_columns(state.frequency_range, - horizontal.coordinate_range, - source_width, - state.visible_only); - if (!columns) - return; - const int width = columns->last - columns->first + 1; - std::vector pixels(static_cast(width) * height); - for (int y = 0; y < height; ++y) { - const auto& row = state.rows[static_cast(y)].values; - for (int x = 0; x < width; ++x) { - pixels[static_cast(y) * width + x] = - state.color_map.at_normalized(normalized_value( - row[static_cast(columns->first + x)], - state.power_range)); - } - } - const Axis_Transform vertical = state.time_axis->transform(); - const Range time_range{static_cast(state.rows.front().tick), - static_cast(state.rows.back().tick)}; - RectF target = mapped_rect(horizontal, vertical, columns->range, time_range); - if (target.height < 1.0) - target.height = std::max(1.0, static_cast(state.time_axis->pixel_length())); - painter.heatmap(target, width, height, pixels, state.interpolation); - - const Hover_Tooltip_Snapshot tooltip = tooltip_snapshot(); - if (tooltip.enabled && tooltip.active && target.contains(tooltip.position)) { - const double frequency = horizontal.pixel_to_coord(tooltip.position.x); - std::ostringstream text; - text << std::fixed << std::setprecision(2) << frequency << " Hz"; - const RectF box{tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; - painter.rect(box, Pen{tooltip.text_pen.color}, tooltip.background); - painter.text({box.x + 4.0, box.y + 3.0}, text.str(), tooltip.font, tooltip.text_pen); - } -} - -Waterfall::Builder::Builder(std::shared_ptr parent, - std::shared_ptr frequency_axis, - std::shared_ptr time_axis) - : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), - time_axis_(std::move(time_axis)) {} - -std::shared_ptr Waterfall::Builder::build() { - if (!parent_ || !frequency_axis_ || !time_axis_) - return {}; - auto result = parent_->plot().make_renderable(parent_, frequency_axis_, time_axis_); - result->set_frequency_range(frequency_range_); - result->set_power_range(power_range_); - result->set_frequency_bin_count(bin_count_); - result->set_visible_range_only(visible_only_); - result->set_interpolation_mode(interpolation_); - { - std::lock_guard lock(result->mutex_); - result->state_.color_map = color_map_; - } - return result; -} - -Frequency_Trace::Frequency_Trace(Plot_Core& plot, - std::shared_ptr time_axis, - std::shared_ptr value_axis) - : Renderable(plot), time_axis_(std::move(time_axis)), value_axis_(std::move(value_axis)) {} - -void Frequency_Trace::append_sample(int tick, double value) { - int limit = 512; - { - std::lock_guard lock(mutex_); - samples_.emplace_back(tick, value); - if (time_axis_) - limit = std::max(2, time_axis_->visible_time_point_count()); - while (samples_.size() > static_cast(limit)) - samples_.pop_front(); - } - changed(); -} - -void Frequency_Trace::append_sample(Time_Of_Day time, double value) { - auto axis = time_axis(); - if (axis) - append_sample(axis->append_time(time), value); -} - -std::shared_ptr Frequency_Trace::time_axis() const { std::lock_guard lock(mutex_); return time_axis_; } -std::shared_ptr Frequency_Trace::value_axis() const { std::lock_guard lock(mutex_); return value_axis_; } -std::size_t Frequency_Trace::sample_count() const { std::lock_guard lock(mutex_); return samples_.size(); } -std::size_t Frequency_Trace::rendered_point_count() const { - std::lock_guard lock(mutex_); - return time_axis_ && value_axis_ && samples_.size() >= 2 ? samples_.size() : 0; -} - -void Frequency_Trace::paint(detail::Painter& painter) { - std::shared_ptr time_axis; - std::shared_ptr value_axis; - std::deque> samples; - Pen pen; - { - std::lock_guard lock(mutex_); - time_axis = time_axis_; - value_axis = value_axis_; - samples = samples_; - pen = pen_; - } - if (!time_axis || !value_axis || samples.size() < 2) - return; - const Axis_Transform x = time_axis->transform(); - const Axis_Transform y = value_axis->transform(); - std::vector points; - points.reserve(samples.size()); - for (const auto& [tick, value] : samples) - points.push_back({x.coord_to_pixel(tick), y.coord_to_pixel(value)}); - painter.polyline(points, pen); -} - -Frequency_Trace::Builder::Builder(std::shared_ptr parent, - std::shared_ptr time_axis, - std::shared_ptr value_axis) - : parent_(std::move(parent)), time_axis_(std::move(time_axis)), - value_axis_(std::move(value_axis)) {} - -std::shared_ptr Frequency_Trace::Builder::build() { - if (!parent_ || !time_axis_ || !value_axis_) - return {}; - auto result = parent_->plot().make_renderable(parent_, time_axis_, value_axis_); - { std::lock_guard lock(result->mutex_); result->pen_ = pen_; } - return result; -} - -Afterglow::Afterglow(Plot_Core& plot, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis) - : Renderable(plot) { - state_.frequency_axis = std::move(frequency_axis); - state_.power_axis = std::move(power_axis); -} - -std::shared_ptr Afterglow::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } -std::shared_ptr Afterglow::power_axis() const { std::lock_guard lock(mutex_); return state_.power_axis; } - -#define RENDERIVE_AFTERGLOW_PROPERTY(Type, Method, Field) \ - Type Afterglow::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ - void Afterglow::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } - -RENDERIVE_AFTERGLOW_PROPERTY(Range, frequency_range, frequency_range) -RENDERIVE_AFTERGLOW_PROPERTY(Range, power_range, power_range) -RENDERIVE_AFTERGLOW_PROPERTY(bool, interpolate, interpolate) - -#undef RENDERIVE_AFTERGLOW_PROPERTY - -int Afterglow::frequency_point_size() const { - std::lock_guard lock(mutex_); - return state_.frequency_count; -} - -void Afterglow::set_frequency_point_size(int value) { - { std::lock_guard lock(mutex_); state_.frequency_count = std::max(0, value); } - changed(); -} - -int Afterglow::power_point_size() const { - std::lock_guard lock(mutex_); - return state_.power_count; -} - -void Afterglow::set_power_point_size(int value) { - { std::lock_guard lock(mutex_); state_.power_count = std::max(0, value); } - changed(); -} - -double Afterglow::attenuation_rate() const { - std::lock_guard lock(mutex_); - return state_.attenuation; -} - -void Afterglow::set_attenuation_rate(double value) { - if (!std::isfinite(value)) - value = 0.0; - { std::lock_guard lock(mutex_); state_.attenuation = std::clamp(value, 0.0, 1.0); } - changed(); -} - -std::size_t Afterglow::history_count() const { - std::lock_guard lock(mutex_); - return state_.history.size(); -} - -std::size_t Afterglow::latest_spectrum_point_count() const { - std::lock_guard lock(mutex_); - return state_.history.empty() ? 0 : state_.history.back().size(); -} - -std::size_t Afterglow::rendered_cell_count() const { - const State state = state_snapshot(); - if (!state.power_axis || state.history.empty()) - return 0; - const int width = std::min(state.frequency_count, - static_cast(state.history.back().size())); - const int height = state.power_count > 0 - ? state.power_count - : std::max(1, static_cast(state.power_axis->pixel_length())); - return width > 0 && height > 0 - ? static_cast(width) * static_cast(height) - : 0; -} - -void Afterglow::append_spectrum(std::span values) { - { - std::lock_guard lock(mutex_); - state_.history.emplace_back(values.begin(), values.end()); - if (state_.frequency_count <= 0) - state_.frequency_count = static_cast(values.size()); - while (state_.history.size() > 64) - state_.history.pop_front(); - } - changed(); -} - -void Afterglow::append_spectrum(std::pmr::vector&& values) { - append_spectrum(std::span(values.data(), values.size())); -} - -Afterglow::State Afterglow::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } - -void Afterglow::paint(detail::Painter& painter) { - const State state = state_snapshot(); - if (!state.frequency_axis || !state.power_axis || state.history.empty()) - return; - const int width = std::min(state.frequency_count, - static_cast(state.history.back().size())); - const int height = state.power_count > 0 - ? state.power_count - : std::max(1, static_cast(state.power_axis->pixel_length())); - if (width <= 0 || height <= 0) - return; - std::vector intensity(static_cast(width) * height); - double weight = 1.0; - const double decay = 1.0 - state.attenuation; - for (auto iterator = state.history.rbegin(); iterator != state.history.rend(); ++iterator) { - const auto& spectrum = *iterator; - const int count = std::min(width, static_cast(spectrum.size())); - for (int x = 0; x < count; ++x) { - const double normalized = normalized_value(spectrum[static_cast(x)], - state.power_range); - const int y = std::clamp(height - 1 - static_cast(normalized * (height - 1)), - 0, height - 1); - intensity[static_cast(y) * width + x] += weight; - if (state.interpolate && y + 1 < height) - intensity[static_cast(y + 1) * width + x] += weight * 0.35; - } - weight *= decay; - if (weight < 0.01) - break; - } - const double maximum = std::max(1.0, *std::max_element(intensity.begin(), intensity.end())); - std::vector pixels(intensity.size()); - for (std::size_t index = 0; index < pixels.size(); ++index) - pixels[index] = state.color_map.at_normalized(intensity[index] / maximum); - const RectF target = mapped_rect(state.frequency_axis->transform(), state.power_axis->transform(), - state.frequency_range, state.power_range); - painter.heatmap(target, width, height, pixels, Image_Interpolation_Mode::Bilinear); -} - -Afterglow::Builder::Builder(std::shared_ptr parent, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis) - : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), - power_axis_(std::move(power_axis)) {} - -std::shared_ptr Afterglow::Builder::build() { - if (!parent_ || !frequency_axis_ || !power_axis_) - return {}; - auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); - result->set_frequency_range(frequency_range_); - result->set_power_range(power_range_); - result->set_frequency_point_size(frequency_count_); - result->set_power_point_size(power_count_); - result->set_interpolate(interpolate_); - result->set_attenuation_rate(attenuation_); - { - std::lock_guard lock(result->mutex_); - result->state_.color_map = color_map_; - } - return result; -} - -} // namespace renderive diff --git a/render_2D/plottable/Hover_Tooltip.h b/render_2D/plottable/Hover_Tooltip.h index 0174763..3cd8945 100644 --- a/render_2D/plottable/Hover_Tooltip.h +++ b/render_2D/plottable/Hover_Tooltip.h @@ -1,89 +1,29 @@ #pragma once - #include "../event/Event.h" - -#include - +#include "../render/Blend2D_Cache.h" namespace renderive { - -struct Hover_Tooltip_Snapshot { - bool enabled{true}; +struct Hover_Tooltip_Properties { + bool tooltip_enabled = true; + Font tooltip_font; + Pen tooltip_text_pen{Color::white()}; + Brush tooltip_background_brush{Color{20, 20, 20, 220}, Brush_Style::Solid}; +}; +namespace detail { +struct Hover_Tooltip_Runtime { bool active{}; - PointF position; - Brush background{Color::white(), Brush_Style::Solid}; - Pen text_pen{Color::black()}; - Font font; + PointF position{}; }; - -template -class Hover_Tooltip_Mixin { -public: - [[nodiscard]] bool hover_tooltip_enabled() const { - std::lock_guard lock(tooltip_mutex_); - return tooltip_.enabled; +inline bool update_hover_tooltip(Hover_Tooltip_Runtime& tooltip, const Event& event) { + if(event.type == Event_Type::Pointer_Move) { + tooltip.active = true; + tooltip.position = static_cast(event).position; + return true; } - void set_use_hover_info(bool enabled) { - { - std::lock_guard lock(tooltip_mutex_); - tooltip_.enabled = enabled; - if (!enabled) - tooltip_.active = false; - } - static_cast(this)->tooltip_changed(); + if(event.type == Event_Type::Leave) { + tooltip.active = false; + return true; } - [[nodiscard]] Font hover_tooltip_font() const { - std::lock_guard lock(tooltip_mutex_); - return tooltip_.font; - } - void set_hover_tooltip_font(Font value) { - { std::lock_guard lock(tooltip_mutex_); tooltip_.font = value; } - static_cast(this)->tooltip_changed(); - } - [[nodiscard]] Brush hover_info_background_brush() const { - std::lock_guard lock(tooltip_mutex_); - return tooltip_.background; - } - void set_hover_tooltip_background_brush(Brush value) { - { std::lock_guard lock(tooltip_mutex_); tooltip_.background = value; } - static_cast(this)->tooltip_changed(); - } - [[nodiscard]] Pen tooltip_text_pen() const { - std::lock_guard lock(tooltip_mutex_); - return tooltip_.text_pen; - } - void set_tooltip_text_pen(Pen value) { - { std::lock_guard lock(tooltip_mutex_); tooltip_.text_pen = value; } - static_cast(this)->tooltip_changed(); - } - -protected: - void update_hover(const Event& event) { - bool changed{}; - { - std::lock_guard lock(tooltip_mutex_); - if (event.type == Event_Type::Pointer_Move) { - const auto& pointer = static_cast(event); - changed = !tooltip_.active || tooltip_.position.x != pointer.position.x || - tooltip_.position.y != pointer.position.y; - tooltip_.active = tooltip_.enabled; - tooltip_.position = pointer.position; - } else if (event.type == Event_Type::Leave) { - changed = tooltip_.active; - tooltip_.active = false; - } - } - if (changed) - static_cast(this)->tooltip_changed(); - } - [[nodiscard]] Hover_Tooltip_Snapshot tooltip_snapshot() const { - std::lock_guard lock(tooltip_mutex_); - return tooltip_; - } - -private: - mutable std::mutex tooltip_mutex_; - Hover_Tooltip_Snapshot tooltip_; -}; - -} // namespace renderive - + return false; +} +} +} diff --git a/render_2D/plottable/Overlays.cpp b/render_2D/plottable/Overlays.cpp deleted file mode 100644 index 79982b8..0000000 --- a/render_2D/plottable/Overlays.cpp +++ /dev/null @@ -1,434 +0,0 @@ -#include "Plottables.h" - -#include "Curve_Sampling.h" -#include "../plot/Plot_Core.h" -#include "../render/Blend2D_Cache.h" - -#include -#include -#include -#include - -namespace renderive { -namespace { - -RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { - const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); - const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); - const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); - const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); - return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; -} - -} // namespace - -Sweep_Spectrum::Sweep_Spectrum(Plot_Core& plot, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis) - : Renderable(plot) { - state_.frequency_axis = std::move(frequency_axis); - state_.power_axis = std::move(power_axis); -} - -void Sweep_Spectrum::append_block(std::span values) { - { - std::lock_guard lock(mutex_); - state_.data.emplace_back(values.begin(), values.end()); - if (state_.bins <= 0) - state_.bins = static_cast(values.size()); - const int limit = std::max(1, state_.blocks); - while (state_.data.size() > static_cast(limit)) - state_.data.pop_front(); - } - changed(); -} - -void Sweep_Spectrum::append_block(std::pmr::vector&& values) { - append_block(std::span(values.data(), values.size())); -} - -std::shared_ptr Sweep_Spectrum::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } -std::shared_ptr Sweep_Spectrum::power_axis() const { std::lock_guard lock(mutex_); return state_.power_axis; } - -#define RENDERIVE_SWEEP_PROPERTY(Type, Method, Field) \ - Type Sweep_Spectrum::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ - void Sweep_Spectrum::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } - -RENDERIVE_SWEEP_PROPERTY(Range, frequency_range, frequency_range) -RENDERIVE_SWEEP_PROPERTY(Pen, pen, pen) -RENDERIVE_SWEEP_PROPERTY(Pen, cur_frequency_pen, current_pen) -RENDERIVE_SWEEP_PROPERTY(bool, visible_range_only, visible_only) -RENDERIVE_SWEEP_PROPERTY(Line_Interpolation_Mode, interpolation_mode, interpolation) - -#undef RENDERIVE_SWEEP_PROPERTY - -int Sweep_Spectrum::bins_per_block() const { - std::lock_guard lock(mutex_); - return state_.bins; -} - -void Sweep_Spectrum::set_bins_per_block(int value) { - { std::lock_guard lock(mutex_); state_.bins = std::max(0, value); } - changed(); -} - -int Sweep_Spectrum::block_count() const { - std::lock_guard lock(mutex_); - return state_.blocks; -} - -void Sweep_Spectrum::set_block_count(int value) { - { - std::lock_guard lock(mutex_); - state_.blocks = std::max(1, value); - while (state_.data.size() > static_cast(state_.blocks)) - state_.data.pop_front(); - } - changed(); -} - -std::size_t Sweep_Spectrum::stored_block_count() const { - std::lock_guard lock(mutex_); - return state_.data.size(); -} - -std::size_t Sweep_Spectrum::stored_point_count() const { - std::lock_guard lock(mutex_); - std::size_t count{}; - for (const auto& block : state_.data) - count += block.size(); - return count; -} - -std::size_t Sweep_Spectrum::rendered_point_count() const { - const State state = state_snapshot(); - if (!state.frequency_axis || !state.power_axis) - return 0; - std::vector values; - for (const auto& block : state.data) - values.insert(values.end(), block.begin(), block.end()); - return detail::curve_points(values, state.frequency_range, - state.frequency_axis->transform(), - state.power_axis->transform(), state.visible_only, - state.interpolation) - .size(); -} - -Sweep_Spectrum::State Sweep_Spectrum::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } - -void Sweep_Spectrum::paint(detail::Painter& painter) { - const State state = state_snapshot(); - if (!state.frequency_axis || !state.power_axis || state.data.empty()) - return; - std::vector values; - for (const auto& block : state.data) - values.insert(values.end(), block.begin(), block.end()); - if (values.size() < 2) - return; - const Axis_Transform x = state.frequency_axis->transform(); - const Axis_Transform y = state.power_axis->transform(); - auto points = detail::curve_points(values, state.frequency_range, x, y, - state.visible_only, state.interpolation); - painter.polyline(points, state.pen); - const double completed = state.blocks > 0 - ? std::min(1.0, static_cast(state.data.size()) / state.blocks) - : 1.0; - const double frequency = state.frequency_range.origin + state.frequency_range.length() * completed; - const RectF content = axis_content_rect(x, y); - const double marker_x = x.coord_to_pixel(frequency); - painter.line({marker_x, content.y}, {marker_x, content.bottom()}, state.current_pen); -} - -Sweep_Spectrum::Builder::Builder(std::shared_ptr parent, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis) - : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), - power_axis_(std::move(power_axis)) {} - -std::shared_ptr Sweep_Spectrum::Builder::build() { - if (!parent_ || !frequency_axis_ || !power_axis_) - return {}; - auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); - result->set_frequency_range(frequency_range_); - result->set_bins_per_block(bins_); - result->set_block_count(blocks_); - result->set_pen(pen_); - result->set_cur_frequency_pen(current_pen_); - result->set_visible_range_only(visible_only_); - result->set_interpolation_mode(interpolation_); - return result; -} - -Selection_Rectangle_Overlay::Selection_Rectangle_Overlay( - Plot_Core& plot, - std::shared_ptr horizontal_axis, - std::shared_ptr vertical_axis) - : Renderable(plot), horizontal_axis_(std::move(horizontal_axis)), - vertical_axis_(std::move(vertical_axis)) {} - -void Selection_Rectangle_Overlay::set_horizontal_axis(std::shared_ptr axis) { - { std::lock_guard lock(mutex_); horizontal_axis_ = std::move(axis); } - changed(); -} - -void Selection_Rectangle_Overlay::set_vertical_axis(std::shared_ptr axis) { - { std::lock_guard lock(mutex_); vertical_axis_ = std::move(axis); } - changed(); -} - -#define RENDERIVE_SELECTION_PROPERTY(Type, Method, Field) \ - Type Selection_Rectangle_Overlay::Method() const { std::lock_guard lock(mutex_); return Field; } \ - void Selection_Rectangle_Overlay::set_##Method(Type value) { { std::lock_guard lock(mutex_); Field = std::move(value); } changed(); } - -RENDERIVE_SELECTION_PROPERTY(Font, label_font, font_) -RENDERIVE_SELECTION_PROPERTY(Pen, label_pen, label_pen_) -RENDERIVE_SELECTION_PROPERTY(Brush, selection_brush, brush_) -RENDERIVE_SELECTION_PROPERTY(Pen, selection_border_pen, border_) - -#undef RENDERIVE_SELECTION_PROPERTY - -std::vector Selection_Rectangle_Overlay::selected_regions() const { - std::lock_guard lock(mutex_); - return regions_; -} - -void Selection_Rectangle_Overlay::clear_selected_regions() { - { std::lock_guard lock(mutex_); regions_.clear(); selecting_ = false; } - changed(); -} - -void Selection_Rectangle_Overlay::handle_event(const Event& event) { - std::shared_ptr horizontal; - std::shared_ptr vertical; - { - std::lock_guard lock(mutex_); - horizontal = horizontal_axis_; - vertical = vertical_axis_; - } - if (!horizontal || !vertical) - return; - const RectF content = axis_content_rect(horizontal->transform(), vertical->transform()); - if (event.type == Event_Type::Pointer_Press) { - const auto& pointer = static_cast(event); - if (pointer.button != Mouse_Button::Left || !content.contains(pointer.position)) - return; - { - std::lock_guard lock(mutex_); - selecting_ = true; - selection_start_ = selection_current_ = pointer.position; - } - event.accept(); - changed(); - } else if (event.type == Event_Type::Pointer_Move) { - const auto& pointer = static_cast(event); - { - std::lock_guard lock(mutex_); - if (!selecting_) - return; - selection_current_ = pointer.position; - } - event.accept(); - changed(); - } else if (event.type == Event_Type::Pointer_Release) { - const auto& pointer = static_cast(event); - PointF start; - { - std::lock_guard lock(mutex_); - if (!selecting_) - return; - selecting_ = false; - start = selection_start_; - selection_current_ = pointer.position; - } - const double x1 = horizontal->pixel_to_coord(start.x); - const double x2 = horizontal->pixel_to_coord(pointer.position.x); - const double y1 = vertical->pixel_to_coord(start.y); - const double y2 = vertical->pixel_to_coord(pointer.position.y); - const RectF region{x1, y1, x2 - x1, y2 - y1}; - if (std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9) { - std::lock_guard lock(mutex_); - regions_.push_back(region.normalized()); - } - event.accept(); - changed(); - } -} - -void Selection_Rectangle_Overlay::paint(detail::Painter& painter) { - std::shared_ptr horizontal; - std::shared_ptr vertical; - std::vector regions; - Font font; - Pen label_pen; - Brush brush; - Pen border; - bool selecting{}; - PointF start; - PointF current; - { - std::lock_guard lock(mutex_); - horizontal = horizontal_axis_; - vertical = vertical_axis_; - regions = regions_; - font = font_; - label_pen = label_pen_; - brush = brush_; - border = border_; - selecting = selecting_; - start = selection_start_; - current = selection_current_; - } - if (!horizontal || !vertical) - return; - for (const RectF& region : regions) { - const double x1 = horizontal->coord_to_pixel(region.x); - const double x2 = horizontal->coord_to_pixel(region.right()); - const double y1 = vertical->coord_to_pixel(region.y); - const double y2 = vertical->coord_to_pixel(region.bottom()); - RectF pixels{x1, y1, x2 - x1, y2 - y1}; - painter.rect(pixels, border, brush); - std::ostringstream text; - text << region.width << " x " << region.height; - const RectF normalized = pixels.normalized(); - painter.text({normalized.x + 3.0, normalized.y + 3.0}, text.str(), font, label_pen); - } - if (selecting) - painter.rect({start.x, start.y, current.x - start.x, current.y - start.y}, border, brush); -} - -Selection_Rectangle_Overlay::Builder::Builder( - std::shared_ptr parent, - std::shared_ptr horizontal_axis, - std::shared_ptr vertical_axis) - : parent_(std::move(parent)), horizontal_axis_(std::move(horizontal_axis)), - vertical_axis_(std::move(vertical_axis)) {} - -std::shared_ptr Selection_Rectangle_Overlay::Builder::build() { - if (!parent_ || !horizontal_axis_ || !vertical_axis_) - return {}; - auto result = parent_->plot().make_renderable( - parent_, horizontal_axis_, vertical_axis_); - result->set_label_font(font_); - result->set_label_pen(label_pen_); - result->set_selection_brush(brush_); - result->set_selection_border_pen(border_); - return result; -} - -Constellation_Diagram::Constellation_Diagram(Plot_Core& plot, - std::shared_ptr i_axis, - std::shared_ptr q_axis) - : Renderable(plot) { - state_.i_axis = std::move(i_axis); - state_.q_axis = std::move(q_axis); -} - -void Constellation_Diagram::append_point(PointF point) { - const auto now = std::chrono::steady_clock::now(); - { - std::lock_guard lock(mutex_); - state_.points.push_back({point, now}); - const auto cutoff = now - std::chrono::milliseconds(std::max(0, state_.lifetime_ms)); - while (!state_.points.empty() && state_.points.front().time < cutoff) - state_.points.pop_front(); - } - changed(); -} - -std::shared_ptr Constellation_Diagram::i_axis() const { std::lock_guard lock(mutex_); return state_.i_axis; } -std::shared_ptr Constellation_Diagram::q_axis() const { std::lock_guard lock(mutex_); return state_.q_axis; } - -#define RENDERIVE_CONSTELLATION_PROPERTY(Type, Method, Field) \ - Type Constellation_Diagram::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ - void Constellation_Diagram::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } - -RENDERIVE_CONSTELLATION_PROPERTY(Range, i_range, i_range) -RENDERIVE_CONSTELLATION_PROPERTY(Range, q_range, q_range) -RENDERIVE_CONSTELLATION_PROPERTY(Color, point_color, point_color) -RENDERIVE_CONSTELLATION_PROPERTY(Color, anchor_color, anchor_color) - -#undef RENDERIVE_CONSTELLATION_PROPERTY - -int Constellation_Diagram::point_lifetime_ms() const { - std::lock_guard lock(mutex_); - return state_.lifetime_ms; -} - -void Constellation_Diagram::set_point_lifetime_ms(int value) { - { - std::lock_guard lock(mutex_); - state_.lifetime_ms = std::max(0, value); - } - changed(); -} - -std::size_t Constellation_Diagram::point_count() const { - std::lock_guard lock(mutex_); - return state_.points.size(); -} - -void Constellation_Diagram::fit_square_to_axes() { - auto horizontal = i_axis(); - auto vertical = q_axis(); - if (!horizontal || !vertical) - return; - const Range i = i_range(); - const Range q = q_range(); - const double side = std::max(i.size(), q.size()); - horizontal->set_coord_range({i.center() - side * 0.5, i.center() + side * 0.5}); - vertical->set_coord_range({q.center() + side * 0.5, q.center() - side * 0.5}); -} - -Constellation_Diagram::State Constellation_Diagram::state_snapshot() const { - std::lock_guard lock(mutex_); - return state_; -} - -void Constellation_Diagram::paint(detail::Painter& painter) { - const State state = state_snapshot(); - if (!state.i_axis || !state.q_axis) - return; - const Axis_Transform x = state.i_axis->transform(); - const Axis_Transform y = state.q_axis->transform(); - const int count = static_cast(state.type); - const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; - for (int index = 0; index < count; ++index) { - const double angle = state.phase + 2.0 * std::numbers::pi * index / count; - const PointF point{state.i_range.center() + std::cos(angle) * radius, - state.q_range.center() + std::sin(angle) * radius}; - painter.circle({x.coord_to_pixel(point.x), y.coord_to_pixel(point.y)}, 3.0, - Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::Solid}); - } - const auto cutoff = std::chrono::steady_clock::now() - - std::chrono::milliseconds(std::max(0, state.lifetime_ms)); - for (const Timed_Point& value : state.points) { - if (value.time < cutoff) - continue; - painter.circle({x.coord_to_pixel(value.point.x), y.coord_to_pixel(value.point.y)}, 2.0, - Pen{state.point_color}, Brush{state.point_color, Brush_Style::Solid}); - } -} - -Constellation_Diagram::Builder::Builder(std::shared_ptr parent, - std::shared_ptr i_axis, - std::shared_ptr q_axis) - : parent_(std::move(parent)), i_axis_(std::move(i_axis)), q_axis_(std::move(q_axis)) {} - -std::shared_ptr Constellation_Diagram::Builder::build() { - if (!parent_ || !i_axis_ || !q_axis_) - return {}; - auto result = parent_->plot().make_renderable(parent_, i_axis_, q_axis_); - result->set_i_range(i_range_); - result->set_q_range(q_range_); - result->set_point_color(point_color_); - result->set_anchor_color(anchor_color_); - result->set_point_lifetime_ms(lifetime_); - { - std::lock_guard lock(result->mutex_); - result->state_.type = type_; - result->state_.phase = phase_; - } - return result; -} - -} // namespace renderive diff --git a/render_2D/plottable/Plottable.h b/render_2D/plottable/Plottable.h new file mode 100644 index 0000000..6787a7b --- /dev/null +++ b/render_2D/plottable/Plottable.h @@ -0,0 +1,86 @@ +#pragma once +#include "../renderable/Renderable_Builder.h" +#include +#include +#include +#include +#include +namespace renderive { +template +class Clamped_Property { +public: + using Value = Value_Type; + Clamped_Property() : value(Default) {} + Clamped_Property(Value_Type value) : value(std::clamp(value, Minimum, Maximum)) {} + Clamped_Property& operator=(Value_Type input) { + value = std::clamp(input, Minimum, Maximum); + return *this; + } + const Value_Type& get() const noexcept { + return value; + } + operator const Value_Type&() const noexcept { + return value; + } +private: + Value_Type value; +}; +using Nonnegative_Count = Clamped_Property::max(), 0>; +using Positive_Count = Clamped_Property::max(), 1>; +class Unit_Interval { +public: + using Value = double; + Unit_Interval() = default; + Unit_Interval(double value) : value(std::isfinite(value) ? std::clamp(value, 0.0, 1.0) : 0.0) {} + Unit_Interval& operator=(double input) { + value = std::isfinite(input) ? std::clamp(input, 0.0, 1.0) : 0.0; + return *this; + } + const double& get() const noexcept { + return value; + } + operator const double&() const noexcept { + return value; + } +private: + double value{}; +}; +namespace detail { +template +class Plottable_State : public Double_State_Strategy { +public: + using Properties = Properties_Type; + using Base = Double_State_Strategy; + Plottable_State(Plot_Core& plot, const Properties& properties) : Base(properties, plot) {} + template Value> + Plottable_State& set(Value&& value) { + Base::template set(std::forward(value)); + this->changed(); + return *this; + } + template + auto get() const { + return Base::template get(); + } +protected: + Properties properties() const { + return Base::read([](const Properties& value) { return value; }); + } + Properties render_properties() const { + return Base::render_use_state(); + } + void publish_properties() { + Base::publish(); + } +private: + std::uint64_t state_revision() const override { + return Base::state_revision(); + } + using Base::read; + using Base::render_use_state; + using Base::update; +}; +template +using Attach_Plottable = Attach_Builder; +} +} diff --git a/render_2D/plottable/Plottables.h b/render_2D/plottable/Plottables.h index b7085dd..d91e071 100644 --- a/render_2D/plottable/Plottables.h +++ b/render_2D/plottable/Plottables.h @@ -1,548 +1,8 @@ #pragma once - -#include "../axis/Axis.h" -#include "Hover_Tooltip.h" - -#include -#include -#include -#include -#include - -namespace renderive { - -class LIB_DECL Spectrum final : public Renderable, public Event_Handler, - public Hover_Tooltip_Mixin { -public: - Spectrum(Plot_Core& plot, std::shared_ptr frequency_axis, - std::shared_ptr power_axis); - - [[nodiscard]] std::shared_ptr frequency_axis() const; - void set_frequency_axis(std::shared_ptr value); - [[nodiscard]] std::shared_ptr power_axis() const; - void set_power_axis(std::shared_ptr value); - [[nodiscard]] int frequency_point_size() const; - void set_frequency_point_size(int value); - [[nodiscard]] Range frequency_range() const; - void set_frequency_range(Range value); - [[nodiscard]] double center_frequency() const; - void set_center_frequency(double value); - [[nodiscard]] Range sweep_frequency_range() const; - void set_sweep_frequency_range(Range value); - [[nodiscard]] bool max_hold_visible() const; - void set_max_hold_visible(bool value); - [[nodiscard]] bool min_hold_visible() const; - void set_min_hold_visible(bool value); - [[nodiscard]] bool max_marker_visible() const; - void set_max_marker_visible(bool value); - [[nodiscard]] bool use_min_marker() const; - void set_use_min_marker(bool value); - [[nodiscard]] bool sweep_region_visible() const; - void set_sweep_region_visible(bool value); - [[nodiscard]] bool visible_range_only() const; - void set_visible_range_only(bool value); - [[nodiscard]] Line_Interpolation_Mode interpolation_mode() const; - void set_interpolation_mode(Line_Interpolation_Mode value); - [[nodiscard]] Brush max_brush() const; - void set_max_brush(Brush value); - [[nodiscard]] Brush current_brush() const; - void set_current_brush(Brush value); - [[nodiscard]] Brush min_brush() const; - void set_min_brush(Brush value); - [[nodiscard]] Pen max_pen() const; - void set_max_pen(Pen value); - [[nodiscard]] Pen current_pen() const; - void set_current_pen(Pen value); - [[nodiscard]] Pen min_pen() const; - void set_min_pen(Pen value); - [[nodiscard]] Pen selected_marker_pen() const; - void set_selected_marker_pen(Pen value); - [[nodiscard]] Pen marker_pen() const; - void set_marker_pen(Pen value); - [[nodiscard]] Pen middle_frequency_pen() const; - void set_middle_frequency_pen(Pen value); - [[nodiscard]] Brush sweep_region_brush() const; - void set_sweep_region_brush(Brush value); - - void update_samples(std::span values); - void update_samples(std::pmr::vector&& values); - template void update_samples(const Values& values) { - update_samples(std::span(values.data(), values.size())); - } - [[nodiscard]] std::size_t sample_count() const; - [[nodiscard]] std::size_t rendered_point_count() const; - [[nodiscard]] double power_at(double frequency, bool& ok) const; - void add_custom_marker(double frequency); - void add_custom_line_marker(double frequency); - void remove_custom_marker(double frequency); - void remove_selected_marker(); - void clear_custom_markers(); - [[nodiscard]] int selectable_line_marker_count() const; - [[nodiscard]] int selected_marker_index() const; - void set_selected_marker_index(int index); - void select_next_marker(); - void select_previous_marker(); - void clear_marker_selection(); - [[nodiscard]] double marker_frequency(int index) const; - void set_marker_frequency(int index, double frequency); - void set_current_marker_frequency(double frequency); - void handle_event(const Event& event) override; - void tooltip_changed() { changed(); } - - class Builder { - public: - Builder(std::shared_ptr parent, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis); - Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } - Builder& set_frequency_point_size(int value) { point_count_ = value; return *this; } - Builder& set_center_frequency(double value) { center_ = value; return *this; } - Builder& set_sweep_frequency_range(Range value) { sweep_range_ = value; return *this; } - Builder& set_max_hold_visible(bool value) { max_hold_ = value; return *this; } - Builder& set_min_hold_visible(bool value) { min_hold_ = value; return *this; } - Builder& set_max_marker_visible(bool value) { max_marker_ = value; return *this; } - Builder& set_use_min_marker(bool value) { min_marker_ = value; return *this; } - Builder& set_sweep_region_visible(bool value) { sweep_visible_ = value; return *this; } - Builder& set_visible_range_only(bool value) { visible_only_ = value; return *this; } - Builder& set_interpolation_mode(Line_Interpolation_Mode value) { interpolation_ = value; return *this; } - std::shared_ptr build(); - private: - std::shared_ptr parent_; - std::shared_ptr frequency_axis_; - std::shared_ptr power_axis_; - Range frequency_range_{}; - int point_count_{}; - double center_ = 50.0; - Range sweep_range_{40.0, 60.0}; - bool max_hold_{}; - bool min_hold_{}; - bool max_marker_{}; - bool min_marker_{}; - bool sweep_visible_{}; - bool visible_only_{true}; - Line_Interpolation_Mode interpolation_ = Line_Interpolation_Mode::Linear_Value; - }; - -protected: - void paint(detail::Painter& painter) override; - -private: - struct State { - std::shared_ptr frequency_axis; - std::shared_ptr power_axis; - int point_count{}; - Range frequency_range{}; - double center_frequency = 50.0; - Range sweep_range{40.0, 60.0}; - bool max_hold{}; - bool min_hold{}; - bool max_marker{}; - bool min_marker{}; - bool sweep_visible{}; - bool visible_only{true}; - Line_Interpolation_Mode interpolation = Line_Interpolation_Mode::Linear_Value; - Brush max_brush; - Brush current_brush; - Brush min_brush; - Pen max_pen{Color::red()}; - Pen current_pen{Color::green()}; - Pen min_pen{Color::white()}; - Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; - Pen marker_pen{Color::red()}; - Pen middle_pen{Color::red()}; - Brush sweep_brush{Color{255, 255, 0, 100}, Brush_Style::Solid}; - std::vector samples; - std::vector maxima; - std::vector minima; - std::vector markers; - int selected_marker = -1; - }; - [[nodiscard]] State state_snapshot() const; - mutable std::mutex mutex_; - State state_; -}; - -class LIB_DECL Waterfall final : public Renderable, public Event_Handler, - public Hover_Tooltip_Mixin { -public: - Waterfall(Plot_Core& plot, std::shared_ptr frequency_axis, - std::shared_ptr time_axis); - void append_row(int tick, std::span values); - void append_row(int tick, std::pmr::vector&& values); - void append_row(Time_Of_Day time, std::span values); - void append_row(Time_Of_Day time, std::pmr::vector&& values); - template void append_row(Time_Of_Day time, const Values& values) { - append_row(time, std::span(values.data(), values.size())); - } - [[nodiscard]] std::shared_ptr frequency_axis() const; - [[nodiscard]] std::shared_ptr time_axis() const; - [[nodiscard]] Range frequency_range() const; - void set_frequency_range(Range value); - [[nodiscard]] Range power_range() const; - void set_power_range(Range value); - [[nodiscard]] int frequency_bin_count() const; - void set_frequency_bin_count(int value); - [[nodiscard]] std::size_t row_count() const; - [[nodiscard]] std::size_t stored_point_count() const; - [[nodiscard]] std::size_t rendered_cell_count() const; - [[nodiscard]] bool visible_range_only() const; - void set_visible_range_only(bool value); - [[nodiscard]] Image_Interpolation_Mode interpolation_mode() const; - void set_interpolation_mode(Image_Interpolation_Mode value); - void handle_event(const Event& event) override; - void tooltip_changed() { changed(); } - - class Builder { - public: - Builder(std::shared_ptr parent, - std::shared_ptr frequency_axis, - std::shared_ptr time_axis); - Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } - Builder& set_power_range(Range value) { power_range_ = value; return *this; } - Builder& set_frequency_bin_count(int value) { bin_count_ = value; return *this; } - Builder& set_visible_range_only(bool value) { visible_only_ = value; return *this; } - Builder& set_interpolation_mode(Image_Interpolation_Mode value) { interpolation_ = value; return *this; } - Builder& set_color_map(Color_Map value) { color_map_ = std::move(value); return *this; } - std::shared_ptr build(); - private: - std::shared_ptr parent_; - std::shared_ptr frequency_axis_; - std::shared_ptr time_axis_; - Range frequency_range_{0.0, 10.0}; - Range power_range_{0.0, 10.0}; - int bin_count_{}; - bool visible_only_{true}; - Image_Interpolation_Mode interpolation_ = Image_Interpolation_Mode::Nearest; - Color_Map color_map_; - }; - -protected: - void paint(detail::Painter& painter) override; - -private: - struct Row { int tick{}; std::vector values; }; - struct State { - std::shared_ptr frequency_axis; - std::shared_ptr time_axis; - Range frequency_range{0.0, 10.0}; - Range power_range{0.0, 10.0}; - int bin_count{}; - bool visible_only{true}; - Image_Interpolation_Mode interpolation = Image_Interpolation_Mode::Nearest; - Color_Map color_map; - std::deque rows; - }; - [[nodiscard]] State state_snapshot() const; - mutable std::mutex mutex_; - State state_; -}; - -class LIB_DECL Frequency_Trace final : public Renderable { -public: - Frequency_Trace(Plot_Core& plot, std::shared_ptr time_axis, - std::shared_ptr value_axis); - void append_sample(int tick, double value); - void append_sample(Time_Of_Day time, double value); - [[nodiscard]] std::shared_ptr time_axis() const; - [[nodiscard]] std::shared_ptr value_axis() const; - [[nodiscard]] std::size_t sample_count() const; - [[nodiscard]] std::size_t rendered_point_count() const; - - class Builder { - public: - Builder(std::shared_ptr parent, std::shared_ptr time_axis, - std::shared_ptr value_axis); - Builder& set_color(Color value) { pen_.color = value; return *this; } - Builder& set_pen(Pen value) { pen_ = value; return *this; } - std::shared_ptr build(); - private: - std::shared_ptr parent_; - std::shared_ptr time_axis_; - std::shared_ptr value_axis_; - Pen pen_{Color::yellow()}; - }; - -protected: - void paint(detail::Painter& painter) override; - -private: - mutable std::mutex mutex_; - std::shared_ptr time_axis_; - std::shared_ptr value_axis_; - Pen pen_{Color::yellow()}; - std::deque> samples_; -}; - -class LIB_DECL Afterglow final : public Renderable { -public: - Afterglow(Plot_Core& plot, std::shared_ptr frequency_axis, - std::shared_ptr power_axis); - [[nodiscard]] std::shared_ptr frequency_axis() const; - [[nodiscard]] std::shared_ptr power_axis() const; - [[nodiscard]] Range frequency_range() const; - void set_frequency_range(Range value); - [[nodiscard]] Range power_range() const; - void set_power_range(Range value); - [[nodiscard]] int frequency_point_size() const; - void set_frequency_point_size(int value); - [[nodiscard]] int power_point_size() const; - void set_power_point_size(int value); - [[nodiscard]] bool interpolate() const; - void set_interpolate(bool value); - [[nodiscard]] double attenuation_rate() const; - void set_attenuation_rate(double value); - [[nodiscard]] std::size_t history_count() const; - [[nodiscard]] std::size_t latest_spectrum_point_count() const; - [[nodiscard]] std::size_t rendered_cell_count() const; - void append_spectrum(std::span values); - void append_spectrum(std::pmr::vector&& values); - template void append_spectrum(const Values& values) { - append_spectrum(std::span(values.data(), values.size())); - } - - class Builder { - public: - Builder(std::shared_ptr parent, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis); - Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } - Builder& set_power_range(Range value) { power_range_ = value; return *this; } - Builder& set_frequency_bin_count(int value) { frequency_count_ = value; return *this; } - Builder& set_power_bin_count(int value) { power_count_ = value; return *this; } - Builder& set_interpolate_power_bins(bool value) { interpolate_ = value; return *this; } - Builder& set_decay_rate(double value) { attenuation_ = value; return *this; } - Builder& set_color_map(Color_Map value) { color_map_ = std::move(value); return *this; } - std::shared_ptr build(); - private: - std::shared_ptr parent_; - std::shared_ptr frequency_axis_; - std::shared_ptr power_axis_; - Range frequency_range_{0.0, 10.0}; - Range power_range_{0.0, 10.0}; - int frequency_count_{}; - int power_count_{}; - bool interpolate_{true}; - double attenuation_ = 0.2; - Color_Map color_map_; - }; - -protected: - void paint(detail::Painter& painter) override; - -private: - struct State { - std::shared_ptr frequency_axis; - std::shared_ptr power_axis; - Range frequency_range{0.0, 10.0}; - Range power_range{0.0, 10.0}; - int frequency_count{}; - int power_count{}; - bool interpolate{true}; - double attenuation = 0.2; - Color_Map color_map; - std::deque> history; - }; - [[nodiscard]] State state_snapshot() const; - mutable std::mutex mutex_; - State state_; -}; - -class LIB_DECL Sweep_Spectrum final : public Renderable { -public: - Sweep_Spectrum(Plot_Core& plot, std::shared_ptr frequency_axis, - std::shared_ptr power_axis); - void append_block(std::span values); - void append_block(std::pmr::vector&& values); - template void append_block(const Values& values) { - append_block(std::span(values.data(), values.size())); - } - [[nodiscard]] std::shared_ptr frequency_axis() const; - [[nodiscard]] std::shared_ptr power_axis() const; - [[nodiscard]] Range frequency_range() const; - void set_frequency_range(Range value); - [[nodiscard]] int bins_per_block() const; - void set_bins_per_block(int value); - [[nodiscard]] int block_count() const; - void set_block_count(int value); - [[nodiscard]] std::size_t stored_block_count() const; - [[nodiscard]] std::size_t stored_point_count() const; - [[nodiscard]] std::size_t rendered_point_count() const; - [[nodiscard]] Pen pen() const; - void set_pen(Pen value); - [[nodiscard]] Pen cur_frequency_pen() const; - void set_cur_frequency_pen(Pen value); - [[nodiscard]] bool visible_range_only() const; - void set_visible_range_only(bool value); - [[nodiscard]] Line_Interpolation_Mode interpolation_mode() const; - void set_interpolation_mode(Line_Interpolation_Mode value); - - class Builder { - public: - Builder(std::shared_ptr parent, std::shared_ptr frequency_axis, - std::shared_ptr power_axis); - Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } - Builder& set_bins_per_block(int value) { bins_ = value; return *this; } - Builder& set_block_num(int value) { blocks_ = value; return *this; } - Builder& set_pen(Pen value) { pen_ = value; return *this; } - Builder& set_current_frequency_pen(Pen value) { current_pen_ = value; return *this; } - Builder& set_visible_range_only(bool value) { visible_only_ = value; return *this; } - Builder& set_interpolation_mode(Line_Interpolation_Mode value) { interpolation_ = value; return *this; } - std::shared_ptr build(); - private: - std::shared_ptr parent_; - std::shared_ptr frequency_axis_; - std::shared_ptr power_axis_; - Range frequency_range_{}; - int bins_{}; - int blocks_{}; - Pen pen_{Color::yellow()}; - Pen current_pen_{Color::red(), 2.0}; - bool visible_only_{true}; - Line_Interpolation_Mode interpolation_ = Line_Interpolation_Mode::Linear_Value; - }; - -protected: - void paint(detail::Painter& painter) override; - -private: - struct State { - std::shared_ptr frequency_axis; - std::shared_ptr power_axis; - Range frequency_range{}; - int bins{}; - int blocks{}; - Pen pen{Color::yellow()}; - Pen current_pen{Color::red(), 2.0}; - bool visible_only{true}; - Line_Interpolation_Mode interpolation = Line_Interpolation_Mode::Linear_Value; - std::deque> data; - }; - [[nodiscard]] State state_snapshot() const; - mutable std::mutex mutex_; - State state_; -}; - -class LIB_DECL Selection_Rectangle_Overlay final : public Renderable, public Event_Handler { -public: - Selection_Rectangle_Overlay(Plot_Core& plot, std::shared_ptr horizontal_axis, - std::shared_ptr vertical_axis); - void set_horizontal_axis(std::shared_ptr axis); - void set_vertical_axis(std::shared_ptr axis); - [[nodiscard]] Font label_font() const; - [[nodiscard]] Pen label_pen() const; - [[nodiscard]] Brush selection_brush() const; - [[nodiscard]] Pen selection_border_pen() const; - void set_label_font(Font value); - void set_label_pen(Pen value); - void set_selection_brush(Brush value); - void set_selection_border_pen(Pen value); - [[nodiscard]] std::vector selected_regions() const; - void clear_selected_regions(); - void handle_event(const Event& event) override; - - class Builder { - public: - Builder(std::shared_ptr parent, std::shared_ptr horizontal_axis, - std::shared_ptr vertical_axis); - Builder& set_font(Font value) { font_ = value; return *this; } - Builder& set_font_pen(Pen value) { label_pen_ = value; return *this; } - Builder& set_rect_brush(Brush value) { brush_ = value; return *this; } - Builder& set_border_pen(Pen value) { border_ = value; return *this; } - std::shared_ptr build(); - private: - std::shared_ptr parent_; - std::shared_ptr horizontal_axis_; - std::shared_ptr vertical_axis_; - Font font_; - Pen label_pen_{Color::white()}; - Brush brush_{Color{0, 0, 255, 50}, Brush_Style::Solid}; - Pen border_{Color::white(), 1.0, Line_Style::Dash}; - }; - -protected: - void paint(detail::Painter& painter) override; - -private: - mutable std::mutex mutex_; - std::shared_ptr horizontal_axis_; - std::shared_ptr vertical_axis_; - Font font_; - Pen label_pen_{Color::white()}; - Brush brush_{Color{0, 0, 255, 50}, Brush_Style::Solid}; - Pen border_{Color::white(), 1.0, Line_Style::Dash}; - std::vector regions_; - bool selecting_{}; - PointF selection_start_{}; - PointF selection_current_{}; -}; - -enum class Constellation_Diagram_Type : std::uint8_t { Psk4 = 4, Psk8 = 8, Psk16 = 16 }; - -class LIB_DECL Constellation_Diagram final : public Renderable { -public: - Constellation_Diagram(Plot_Core& plot, std::shared_ptr i_axis, - std::shared_ptr q_axis); - void append_point(PointF point); - [[nodiscard]] std::shared_ptr i_axis() const; - [[nodiscard]] std::shared_ptr q_axis() const; - [[nodiscard]] Range i_range() const; - void set_i_range(Range value); - [[nodiscard]] Range q_range() const; - void set_q_range(Range value); - [[nodiscard]] Color point_color() const; - void set_point_color(Color value); - [[nodiscard]] Color anchor_color() const; - void set_anchor_color(Color value); - [[nodiscard]] int point_lifetime_ms() const; - void set_point_lifetime_ms(int value); - [[nodiscard]] std::size_t point_count() const; - void fit_square_to_axes(); - - class Builder { - public: - Builder(std::shared_ptr parent, std::shared_ptr i_axis, - std::shared_ptr q_axis); - Builder& set_i_range(Range value) { i_range_ = value; return *this; } - Builder& set_q_range(Range value) { q_range_ = value; return *this; } - Builder& set_point_color(Color value) { point_color_ = value; return *this; } - Builder& set_anchor_color(Color value) { anchor_color_ = value; return *this; } - Builder& set_point_lifetime_ms(int value) { lifetime_ = value; return *this; } - Builder& set_type(Constellation_Diagram_Type value) { type_ = value; return *this; } - Builder& set_phase_offset_radians(double value) { phase_ = value; return *this; } - std::shared_ptr build(); - private: - std::shared_ptr parent_; - std::shared_ptr i_axis_; - std::shared_ptr q_axis_; - Range i_range_{0.0, 100.0}; - Range q_range_{0.0, 100.0}; - Color point_color_ = Color::red(); - Color anchor_color_ = Color::yellow(); - int lifetime_ = 1000; - Constellation_Diagram_Type type_ = Constellation_Diagram_Type::Psk8; - double phase_{}; - }; - -protected: - void paint(detail::Painter& painter) override; - -private: - struct Timed_Point { PointF point; std::chrono::steady_clock::time_point time; }; - struct State { - std::shared_ptr i_axis; - std::shared_ptr q_axis; - Range i_range{0.0, 100.0}; - Range q_range{0.0, 100.0}; - Color point_color = Color::red(); - Color anchor_color = Color::yellow(); - int lifetime_ms = 1000; - Constellation_Diagram_Type type = Constellation_Diagram_Type::Psk8; - double phase{}; - std::deque points; - }; - [[nodiscard]] State state_snapshot() const; - mutable std::mutex mutex_; - State state_; -}; - -} // namespace renderive +#include "Afterglow.h" +#include "Constellation_Diagram.h" +#include "Frequency_Trace.h" +#include "Selection_Rectangle_Overlay.h" +#include "Spectrum.h" +#include "Sweep_Spectrum.h" +#include "Waterfall.h" diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/plottable/Selection_Rectangle_Overlay.cpp new file mode 100644 index 0000000..fff33b7 --- /dev/null +++ b/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -0,0 +1,112 @@ +#include "Selection_Rectangle_Overlay.h" +#include "../render/Blend2D_Cache.h" +#include +#include +#include +namespace renderive { +namespace detail { +namespace { +struct Selection_Runtime_Base {}; +struct Selection_Runtime { + std::vector regions; + bool selecting{}; + PointF selection_start{}; + PointF selection_current{}; +}; +using Selection_Runtime_State = Double_State_Strategy; +RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { + const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); + const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); + const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); + const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); + return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; +} +} +struct Selection_Rectangle_Overlay_Control::Impl { + Impl(std::shared_ptr horizontal, std::shared_ptr vertical) : horizontal_axis(std::move(horizontal)), vertical_axis(std::move(vertical)) {} + std::shared_ptr horizontal_axis; + std::shared_ptr vertical_axis; + Selection_Runtime_State runtime; +}; +Selection_Rectangle_Overlay_Control::Selection_Rectangle_Overlay_Control(Plot_Core& plot, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis) + : Plottable_State(plot, properties), impl_(std::make_unique(std::move(horizontal_axis), std::move(vertical_axis))) {} +Selection_Rectangle_Overlay_Control::~Selection_Rectangle_Overlay_Control() = default; +std::vector Selection_Rectangle_Overlay_Control::selected_regions() const { + return impl_->runtime.get<&Selection_Runtime::regions>(); +} +void Selection_Rectangle_Overlay_Control::clear_selected_regions() { + impl_->runtime.update([](Selection_Runtime& runtime) { + runtime.regions.clear(); + runtime.selecting = false; + }); + changed(); +} +void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) { + const RectF content = axis_content_rect(impl_->horizontal_axis->transform(), impl_->vertical_axis->transform()); + if(event.type == Event_Type::Pointer_Press) { + const auto& pointer = static_cast(event); + if(pointer.button != Mouse_Button::Left || !content.contains(pointer.position)) + return; + impl_->runtime.update([&](Selection_Runtime& runtime) { + runtime.selecting = true; + runtime.selection_start = runtime.selection_current = pointer.position; + }); + event.accept(); + changed(); + return; + } + if(event.type == Event_Type::Pointer_Move) { + const auto& pointer = static_cast(event); + bool selecting{}; + impl_->runtime.update([&](Selection_Runtime& runtime) { + selecting = runtime.selecting; + if(selecting) + runtime.selection_current = pointer.position; + }); + if(!selecting) + return; + event.accept(); + changed(); + return; + } + if(event.type != Event_Type::Pointer_Release) + return; + const auto& pointer = static_cast(event); + PointF start; + bool selecting{}; + impl_->runtime.update([&](Selection_Runtime& runtime) { + selecting = runtime.selecting; + if(!selecting) + return; + runtime.selecting = false; + start = runtime.selection_start; + runtime.selection_current = pointer.position; + }); + if(!selecting) + return; + const RectF region{impl_->horizontal_axis->pixel_to_coord(start.x), impl_->vertical_axis->pixel_to_coord(start.y), impl_->horizontal_axis->pixel_to_coord(pointer.position.x) - impl_->horizontal_axis->pixel_to_coord(start.x), impl_->vertical_axis->pixel_to_coord(pointer.position.y) - impl_->vertical_axis->pixel_to_coord(start.y)}; + if(std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9) + impl_->runtime.update([region](Selection_Runtime& runtime) { runtime.regions.push_back(region.normalized()); }); + event.accept(); + changed(); +} +void Selection_Rectangle_Overlay_Control::publish() { + publish_properties(); + impl_->runtime.publish(); +} +void Selection_Rectangle_Overlay_Control::paint(Painter& painter) { + const auto state = render_properties(); + const auto runtime = impl_->runtime.render_use_state(); + for(const RectF& region : runtime.regions) { + RectF pixels{impl_->horizontal_axis->coord_to_pixel(region.x), impl_->vertical_axis->coord_to_pixel(region.y), impl_->horizontal_axis->coord_to_pixel(region.right()) - impl_->horizontal_axis->coord_to_pixel(region.x), impl_->vertical_axis->coord_to_pixel(region.bottom()) - impl_->vertical_axis->coord_to_pixel(region.y)}; + painter.rect(pixels, state.selection_border_pen, state.selection_brush); + std::ostringstream text; + text << region.width << " x " << region.height; + const RectF normalized = pixels.normalized(); + painter.text({normalized.x + 3.0, normalized.y + 3.0}, text.str(), state.label_font, state.label_pen); + } + if(runtime.selecting) + painter.rect({runtime.selection_start.x, runtime.selection_start.y, runtime.selection_current.x - runtime.selection_start.x, runtime.selection_current.y - runtime.selection_start.y}, state.selection_border_pen, state.selection_brush); +} +} +} diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.h b/render_2D/plottable/Selection_Rectangle_Overlay.h new file mode 100644 index 0000000..e239a54 --- /dev/null +++ b/render_2D/plottable/Selection_Rectangle_Overlay.h @@ -0,0 +1,28 @@ +#pragma once +#include "Plottable.h" +#include "../axis/Axis.h" +namespace renderive { +struct Selection_Rectangle_Overlay_Properties { + Font label_font; + Pen label_pen{Color::white()}; + Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::Solid}; + Pen selection_border_pen{Color::white(), 1.0, Line_Style::Dash}; +}; +namespace detail { +class LIB_DECL Selection_Rectangle_Overlay_Control : public Plottable_State, public Event_Handler { +public: + Selection_Rectangle_Overlay_Control(Plot_Core& plot, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis); + ~Selection_Rectangle_Overlay_Control() override; + [[nodiscard]] std::vector selected_regions() const; + void clear_selected_regions(); + void handle_event(const Event& event) override; +protected: + void paint(Painter& painter) override; +private: + struct Impl; + std::unique_ptr impl_; + void publish() override; +}; +} +using Selection_Rectangle_Overlay = detail::Attach_Plottable; +} diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index 1d566a3..1421841 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -1,17 +1,22 @@ -#include "Plottables.h" - +#include "Spectrum.h" #include "Curve_Sampling.h" -#include "../plot/Plot_Core.h" -#include "../render/Blend2D_Cache.h" - #include #include #include #include - namespace renderive { +namespace detail { namespace { - +struct Spectrum_Runtime_Base {}; +struct Spectrum_Runtime { + std::vector samples; + std::vector maxima; + std::vector minima; + std::vector markers; + int selected_marker = -1; + Hover_Tooltip_Runtime tooltip; +}; +using Spectrum_Runtime_State = Double_State_Strategy; RectF axes_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); @@ -19,21 +24,11 @@ RectF axes_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; } - -void draw_curve(detail::Painter& painter, - std::span values, - Range domain, - const Axis_Transform& x_axis, - const Axis_Transform& y_axis, - bool visible_only, - Line_Interpolation_Mode interpolation, - const Pen& pen, - const Brush& brush) { - auto points = detail::curve_points(values, domain, x_axis, y_axis, - visible_only, interpolation); - if (points.size() < 2) +void draw_curve(Painter& painter, std::span values, Range domain, const Axis_Transform& x_axis, const Axis_Transform& y_axis, bool visible_only, Line_Interpolation_Mode interpolation, const Pen& pen, const Brush& brush) { + auto points = curve_points(values, domain, x_axis, y_axis, visible_only, interpolation); + if(points.size() < 2) return; - if (brush.enabled()) { + if(brush.enabled()) { std::vector polygon; polygon.reserve(points.size() + 2); polygon.push_back({points.front().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)}); @@ -43,314 +38,215 @@ void draw_curve(detail::Painter& painter, } painter.polyline(points, pen); } - -} // namespace - -Spectrum::Spectrum(Plot_Core& plot, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis) - : Renderable(plot), state_{} { - state_.frequency_axis = std::move(frequency_axis); - state_.power_axis = std::move(power_axis); -} - -#define RENDERIVE_SPECTRUM_PROPERTY(Type, Method, Field) \ - Type Spectrum::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ - void Spectrum::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } - -RENDERIVE_SPECTRUM_PROPERTY(std::shared_ptr, frequency_axis, frequency_axis) -RENDERIVE_SPECTRUM_PROPERTY(std::shared_ptr, power_axis, power_axis) -RENDERIVE_SPECTRUM_PROPERTY(Range, frequency_range, frequency_range) -RENDERIVE_SPECTRUM_PROPERTY(double, center_frequency, center_frequency) -RENDERIVE_SPECTRUM_PROPERTY(Range, sweep_frequency_range, sweep_range) -RENDERIVE_SPECTRUM_PROPERTY(bool, max_hold_visible, max_hold) -RENDERIVE_SPECTRUM_PROPERTY(bool, min_hold_visible, min_hold) -RENDERIVE_SPECTRUM_PROPERTY(bool, max_marker_visible, max_marker) -RENDERIVE_SPECTRUM_PROPERTY(bool, use_min_marker, min_marker) -RENDERIVE_SPECTRUM_PROPERTY(bool, sweep_region_visible, sweep_visible) -RENDERIVE_SPECTRUM_PROPERTY(bool, visible_range_only, visible_only) -RENDERIVE_SPECTRUM_PROPERTY(Line_Interpolation_Mode, interpolation_mode, interpolation) -RENDERIVE_SPECTRUM_PROPERTY(Brush, max_brush, max_brush) -RENDERIVE_SPECTRUM_PROPERTY(Brush, current_brush, current_brush) -RENDERIVE_SPECTRUM_PROPERTY(Brush, min_brush, min_brush) -RENDERIVE_SPECTRUM_PROPERTY(Pen, max_pen, max_pen) -RENDERIVE_SPECTRUM_PROPERTY(Pen, current_pen, current_pen) -RENDERIVE_SPECTRUM_PROPERTY(Pen, min_pen, min_pen) -RENDERIVE_SPECTRUM_PROPERTY(Pen, selected_marker_pen, selected_marker_pen) -RENDERIVE_SPECTRUM_PROPERTY(Pen, marker_pen, marker_pen) -RENDERIVE_SPECTRUM_PROPERTY(Pen, middle_frequency_pen, middle_pen) -RENDERIVE_SPECTRUM_PROPERTY(Brush, sweep_region_brush, sweep_brush) - -#undef RENDERIVE_SPECTRUM_PROPERTY - -int Spectrum::frequency_point_size() const { - std::lock_guard lock(mutex_); - return state_.point_count; -} - -void Spectrum::set_frequency_point_size(int value) { - { std::lock_guard lock(mutex_); state_.point_count = std::max(0, value); } - changed(); -} - -void Spectrum::update_samples(std::span values) { - { - std::lock_guard lock(mutex_); - state_.samples.assign(values.begin(), values.end()); - if (state_.point_count <= 0) - state_.point_count = static_cast(values.size()); - if (state_.maxima.size() != values.size()) - state_.maxima.assign(values.begin(), values.end()); - else - for (std::size_t index = 0; index < values.size(); ++index) - state_.maxima[index] = std::max(state_.maxima[index], values[index]); - if (state_.minima.size() != values.size()) - state_.minima.assign(values.begin(), values.end()); - else - for (std::size_t index = 0; index < values.size(); ++index) - state_.minima[index] = std::min(state_.minima[index], values[index]); - } - changed(); -} - -void Spectrum::update_samples(std::pmr::vector&& values) { - update_samples(std::span(values.data(), values.size())); -} - -std::size_t Spectrum::sample_count() const { - std::lock_guard lock(mutex_); - return state_.samples.size(); -} - -std::size_t Spectrum::rendered_point_count() const { - const State state = state_snapshot(); - if (!state.frequency_axis || !state.power_axis) - return 0; - return detail::curve_points(state.samples, state.frequency_range, - state.frequency_axis->transform(), - state.power_axis->transform(), state.visible_only, - state.interpolation) - .size(); -} - -double Spectrum::power_at(double frequency, bool& ok) const { - std::lock_guard lock(mutex_); +double spectrum_power_at(const Spectrum_Properties& properties, const Spectrum_Runtime& runtime, double frequency, bool& ok) { ok = false; - if (state_.samples.empty() || !state_.frequency_range.contains(frequency) || - state_.frequency_range.length() == 0.0) + if(runtime.samples.empty() || !properties.frequency_range.contains(frequency) || properties.frequency_range.length() == 0.0) return 0.0; - const double normalized = (frequency - state_.frequency_range.origin) / - state_.frequency_range.length(); - const double position = std::clamp(normalized, 0.0, 1.0) * - static_cast(state_.samples.size() - 1); + const double normalized = (frequency - properties.frequency_range.origin) / properties.frequency_range.length(); + const double position = std::clamp(normalized, 0.0, 1.0) * static_cast(runtime.samples.size() - 1); const auto lower = static_cast(std::floor(position)); - const auto upper = std::min(lower + 1, state_.samples.size() - 1); + const auto upper = std::min(lower + 1, runtime.samples.size() - 1); const double fraction = position - static_cast(lower); ok = true; - return state_.samples[lower] * (1.0 - fraction) + state_.samples[upper] * fraction; + return runtime.samples[lower] * (1.0 - fraction) + runtime.samples[upper] * fraction; } - -void Spectrum::add_custom_marker(double frequency) { add_custom_line_marker(frequency); } - -void Spectrum::add_custom_line_marker(double frequency) { - { std::lock_guard lock(mutex_); state_.markers.push_back(frequency); } +} +struct Spectrum_Control::Impl { + Impl(std::shared_ptr frequency, std::shared_ptr power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {} + std::shared_ptr frequency_axis; + std::shared_ptr power_axis; + Spectrum_Runtime_State runtime; +}; +Spectrum_Control::Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) + : Plottable_State(plot, properties), impl_(std::make_unique(std::move(frequency_axis), std::move(power_axis))) {} +Spectrum_Control::~Spectrum_Control() = default; +void Spectrum_Control::update_samples(std::span values) { + if(get<&Spectrum_Properties::frequency_point_size>() <= 0) + set<&Spectrum_Properties::frequency_point_size>(static_cast(values.size())); + impl_->runtime.update([values](Spectrum_Runtime& runtime) { + runtime.samples.assign(values.begin(), values.end()); + if(runtime.maxima.size() != values.size()) + runtime.maxima.assign(values.begin(), values.end()); + else + for(std::size_t index = 0; index < values.size(); ++index) + runtime.maxima[index] = std::max(runtime.maxima[index], values[index]); + if(runtime.minima.size() != values.size()) + runtime.minima.assign(values.begin(), values.end()); + else + for(std::size_t index = 0; index < values.size(); ++index) + runtime.minima[index] = std::min(runtime.minima[index], values[index]); + }); changed(); } - -void Spectrum::remove_custom_marker(double frequency) { - { - std::lock_guard lock(mutex_); - if (state_.markers.empty()) +void Spectrum_Control::update_samples(std::pmr::vector&& values) { + update_samples(std::span(values.data(), values.size())); +} +std::size_t Spectrum_Control::sample_count() const { + return impl_->runtime.read([](const Spectrum_Runtime& runtime) { return runtime.samples.size(); }); +} +std::size_t Spectrum_Control::rendered_point_count() const { + const auto state = properties(); + const auto runtime = impl_->runtime.read([](const Spectrum_Runtime& value) { return value; }); + return curve_points(runtime.samples, state.frequency_range, impl_->frequency_axis->transform(), impl_->power_axis->transform(), state.visible_range_only, state.interpolation_mode).size(); +} +double Spectrum_Control::power_at(double frequency, bool& ok) const { + const auto state = properties(); + return impl_->runtime.read([&](const Spectrum_Runtime& runtime) { return spectrum_power_at(state, runtime, frequency, ok); }); +} +void Spectrum_Control::add_custom_marker(double frequency) { + add_custom_line_marker(frequency); +} +void Spectrum_Control::add_custom_line_marker(double frequency) { + impl_->runtime.update([frequency](Spectrum_Runtime& runtime) { runtime.markers.push_back(frequency); }); + changed(); +} +void Spectrum_Control::remove_custom_marker(double frequency) { + bool removed{}; + impl_->runtime.update([&](Spectrum_Runtime& runtime) { + if(runtime.markers.empty()) return; - auto closest = std::min_element(state_.markers.begin(), state_.markers.end(), - [frequency](double left, double right) { - return std::abs(left - frequency) < std::abs(right - frequency); - }); - const int removed = static_cast(std::distance(state_.markers.begin(), closest)); - state_.markers.erase(closest); - if (state_.selected_marker == removed) - state_.selected_marker = -1; - else if (state_.selected_marker > removed) - --state_.selected_marker; - } - changed(); + auto closest = std::min_element(runtime.markers.begin(), runtime.markers.end(), [frequency](double left, double right) { return std::abs(left - frequency) < std::abs(right - frequency); }); + const int removed_index = static_cast(std::distance(runtime.markers.begin(), closest)); + runtime.markers.erase(closest); + if(runtime.selected_marker == removed_index) + runtime.selected_marker = -1; + else if(runtime.selected_marker > removed_index) + --runtime.selected_marker; + removed = true; + }); + if(removed) + changed(); } - -void Spectrum::remove_selected_marker() { - { - std::lock_guard lock(mutex_); - if (state_.selected_marker < 0 || state_.selected_marker >= static_cast(state_.markers.size())) +void Spectrum_Control::remove_selected_marker() { + bool removed{}; + impl_->runtime.update([&](Spectrum_Runtime& runtime) { + if(runtime.selected_marker < 0 || runtime.selected_marker >= static_cast(runtime.markers.size())) return; - state_.markers.erase(state_.markers.begin() + state_.selected_marker); - state_.selected_marker = -1; - } + runtime.markers.erase(runtime.markers.begin() + runtime.selected_marker); + runtime.selected_marker = -1; + removed = true; + }); + if(removed) + changed(); +} +void Spectrum_Control::clear_custom_markers() { + impl_->runtime.update([](Spectrum_Runtime& runtime) { + runtime.markers.clear(); + runtime.selected_marker = -1; + }); changed(); } - -void Spectrum::clear_custom_markers() { - { std::lock_guard lock(mutex_); state_.markers.clear(); state_.selected_marker = -1; } +int Spectrum_Control::selectable_line_marker_count() const { + return impl_->runtime.read([](const Spectrum_Runtime& runtime) { return static_cast(runtime.markers.size()); }); +} +int Spectrum_Control::selected_marker_index() const { + return impl_->runtime.get<&Spectrum_Runtime::selected_marker>(); +} +void Spectrum_Control::set_selected_marker_index(int index) { + impl_->runtime.update([index](Spectrum_Runtime& runtime) { runtime.selected_marker = index >= 0 && index < static_cast(runtime.markers.size()) ? index : -1; }); changed(); } - -int Spectrum::selectable_line_marker_count() const { - std::lock_guard lock(mutex_); - return static_cast(state_.markers.size()); -} - -int Spectrum::selected_marker_index() const { std::lock_guard lock(mutex_); return state_.selected_marker; } - -void Spectrum::set_selected_marker_index(int index) { - { - std::lock_guard lock(mutex_); - state_.selected_marker = index >= 0 && index < static_cast(state_.markers.size()) ? index : -1; - } +void Spectrum_Control::select_next_marker() { + impl_->runtime.update([](Spectrum_Runtime& runtime) { + if(runtime.markers.empty()) + runtime.selected_marker = -1; + else + runtime.selected_marker = (runtime.selected_marker + 1) % static_cast(runtime.markers.size()); + }); changed(); } - -void Spectrum::select_next_marker() { - std::lock_guard lock(mutex_); - if (state_.markers.empty()) - state_.selected_marker = -1; - else - state_.selected_marker = (state_.selected_marker + 1) % static_cast(state_.markers.size()); - invalidate_cache(); - plot().notify_model_dirty(); +void Spectrum_Control::select_previous_marker() { + impl_->runtime.update([](Spectrum_Runtime& runtime) { + if(runtime.markers.empty()) + runtime.selected_marker = -1; + else + runtime.selected_marker = (runtime.selected_marker <= 0 ? static_cast(runtime.markers.size()) : runtime.selected_marker) - 1; + }); + changed(); } - -void Spectrum::select_previous_marker() { - std::lock_guard lock(mutex_); - if (state_.markers.empty()) - state_.selected_marker = -1; - else - state_.selected_marker = (state_.selected_marker <= 0 - ? static_cast(state_.markers.size()) - : state_.selected_marker) - 1; - invalidate_cache(); - plot().notify_model_dirty(); +void Spectrum_Control::clear_marker_selection() { + set_selected_marker_index(-1); } - -void Spectrum::clear_marker_selection() { set_selected_marker_index(-1); } - -double Spectrum::marker_frequency(int index) const { - std::lock_guard lock(mutex_); - return index >= 0 && index < static_cast(state_.markers.size()) ? state_.markers[index] : 0.0; +double Spectrum_Control::marker_frequency(int index) const { + return impl_->runtime.read([index](const Spectrum_Runtime& runtime) { return index >= 0 && index < static_cast(runtime.markers.size()) ? runtime.markers[index] : 0.0; }); } - -void Spectrum::set_marker_frequency(int index, double frequency) { - { - std::lock_guard lock(mutex_); - if (index < 0 || index >= static_cast(state_.markers.size())) +void Spectrum_Control::set_marker_frequency(int index, double frequency) { + bool updated{}; + impl_->runtime.update([&](Spectrum_Runtime& runtime) { + if(index < 0 || index >= static_cast(runtime.markers.size())) return; - state_.markers[index] = frequency; - } - changed(); + runtime.markers[index] = frequency; + updated = true; + }); + if(updated) + changed(); } - -void Spectrum::set_current_marker_frequency(double frequency) { +void Spectrum_Control::set_current_marker_frequency(double frequency) { const int index = selected_marker_index(); - if (index >= 0) + if(index >= 0) set_marker_frequency(index, frequency); } - -Spectrum::State Spectrum::state_snapshot() const { - std::lock_guard lock(mutex_); - return state_; +void Spectrum_Control::handle_event(const Event& event) { + bool updated{}; + impl_->runtime.update([&](Spectrum_Runtime& runtime) { updated = update_hover_tooltip(runtime.tooltip, event); }); + if(updated) + changed(); } - -void Spectrum::handle_event(const Event& event) { - update_hover(event); +void Spectrum_Control::publish() { + publish_properties(); + impl_->runtime.publish(); } - -void Spectrum::paint(detail::Painter& painter) { - const State state = state_snapshot(); - if (!state.frequency_axis || !state.power_axis) - return; - const Axis_Transform horizontal = state.frequency_axis->transform(); - const Axis_Transform vertical = state.power_axis->transform(); +void Spectrum_Control::paint(Painter& painter) { + const auto state = render_properties(); + const auto runtime = impl_->runtime.render_use_state(); + const Axis_Transform horizontal = impl_->frequency_axis->transform(); + const Axis_Transform vertical = impl_->power_axis->transform(); const RectF content = axes_rect(horizontal, vertical); - - if (state.sweep_visible) { - const double first = horizontal.coord_to_pixel(state.sweep_range.origin); - const double last = horizontal.coord_to_pixel(state.sweep_range.target); - painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, - Pen{.style = Line_Style::None}, state.sweep_brush); + if(state.sweep_region_visible) { + const double first = horizontal.coord_to_pixel(state.sweep_frequency_range.origin); + const double last = horizontal.coord_to_pixel(state.sweep_frequency_range.target); + painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, Pen{.style = Line_Style::None}, state.sweep_region_brush); } - if (state.max_hold) - draw_curve(painter, state.maxima, state.frequency_range, horizontal, vertical, - state.visible_only, state.interpolation, state.max_pen, state.max_brush); - if (state.min_hold) - draw_curve(painter, state.minima, state.frequency_range, horizontal, vertical, - state.visible_only, state.interpolation, state.min_pen, state.min_brush); - draw_curve(painter, state.samples, state.frequency_range, horizontal, vertical, - state.visible_only, state.interpolation, state.current_pen, state.current_brush); - - if (state.middle_pen.enabled()) { + if(state.max_hold_visible) + draw_curve(painter, runtime.maxima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.max_pen, state.max_brush); + if(state.min_hold_visible) + draw_curve(painter, runtime.minima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.min_pen, state.min_brush); + draw_curve(painter, runtime.samples, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.current_pen, state.current_brush); + if(state.middle_frequency_pen.enabled()) { const double x = horizontal.coord_to_pixel(state.center_frequency); - painter.line({x, content.y}, {x, content.bottom()}, state.middle_pen); + painter.line({x, content.y}, {x, content.bottom()}, state.middle_frequency_pen); } - for (std::size_t index = 0; index < state.markers.size(); ++index) { - const double frequency = state.markers[index]; - const double x = horizontal.coord_to_pixel(frequency); - const Pen& pen = static_cast(index) == state.selected_marker - ? state.selected_marker_pen - : state.marker_pen; - painter.line({x, content.y}, {x, content.bottom()}, pen); + for(std::size_t index = 0; index < runtime.markers.size(); ++index) { + const double x = horizontal.coord_to_pixel(runtime.markers[index]); + painter.line({x, content.y}, {x, content.bottom()}, static_cast(index) == runtime.selected_marker ? state.selected_marker_pen : state.marker_pen); } - if (!state.samples.empty() && (state.max_marker || state.min_marker)) { + if(!runtime.samples.empty() && (state.max_marker_visible || state.use_min_marker)) { const auto draw_extreme = [&](bool maximum) { - auto iterator = maximum ? std::max_element(state.samples.begin(), state.samples.end()) - : std::min_element(state.samples.begin(), state.samples.end()); - const std::size_t index = static_cast(std::distance(state.samples.begin(), iterator)); - const double denominator = state.samples.size() > 1 ? state.samples.size() - 1.0 : 1.0; - const double frequency = state.frequency_range.origin + - state.frequency_range.length() * index / denominator; + auto iterator = maximum ? std::max_element(runtime.samples.begin(), runtime.samples.end()) : std::min_element(runtime.samples.begin(), runtime.samples.end()); + const std::size_t index = static_cast(std::distance(runtime.samples.begin(), iterator)); + const double denominator = runtime.samples.size() > 1 ? runtime.samples.size() - 1.0 : 1.0; + const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator; const PointF point{horizontal.coord_to_pixel(frequency), vertical.coord_to_pixel(*iterator)}; const Pen& pen = maximum ? state.max_pen : state.min_pen; painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid}); }; - if (state.max_marker) + if(state.max_marker_visible) draw_extreme(true); - if (state.min_marker) + if(state.use_min_marker) draw_extreme(false); } - - const Hover_Tooltip_Snapshot tooltip = tooltip_snapshot(); - if (tooltip.enabled && tooltip.active && content.contains(tooltip.position)) { - const double frequency = horizontal.pixel_to_coord(tooltip.position.x); + if(state.tooltip_enabled && runtime.tooltip.active && content.contains(runtime.tooltip.position)) { + const double frequency = horizontal.pixel_to_coord(runtime.tooltip.position.x); bool ok{}; - const double power = power_at(frequency, ok); - if (ok) { + const double power = spectrum_power_at(state, runtime, frequency, ok); + if(ok) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency << " Hz " << power; - const RectF box{tooltip.position.x + 8.0, tooltip.position.y + 8.0, 170.0, 24.0}; - painter.rect(box, Pen{tooltip.text_pen.color}, tooltip.background); - painter.text({box.x + 4.0, box.y + 3.0}, text.str(), tooltip.font, tooltip.text_pen); + const RectF box{runtime.tooltip.position.x + 8.0, runtime.tooltip.position.y + 8.0, 170.0, 24.0}; + painter.rect(box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); + painter.text({box.x + 4.0, box.y + 3.0}, text.str(), state.tooltip_font, state.tooltip_text_pen); } } } - -Spectrum::Builder::Builder(std::shared_ptr parent, - std::shared_ptr frequency_axis, - std::shared_ptr power_axis) - : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), - power_axis_(std::move(power_axis)) {} - -std::shared_ptr Spectrum::Builder::build() { - if (!parent_ || !frequency_axis_ || !power_axis_) - return {}; - auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); - result->set_frequency_range(frequency_range_); - result->set_frequency_point_size(point_count_); - result->set_center_frequency(center_); - result->set_sweep_frequency_range(sweep_range_); - result->set_max_hold_visible(max_hold_); - result->set_min_hold_visible(min_hold_); - result->set_max_marker_visible(max_marker_); - result->set_use_min_marker(min_marker_); - result->set_sweep_region_visible(sweep_visible_); - result->set_visible_range_only(visible_only_); - result->set_interpolation_mode(interpolation_); - return result; } - -} // namespace renderive +} diff --git a/render_2D/plottable/Spectrum.h b/render_2D/plottable/Spectrum.h new file mode 100644 index 0000000..9b731fc --- /dev/null +++ b/render_2D/plottable/Spectrum.h @@ -0,0 +1,70 @@ +#pragma once +#include "Hover_Tooltip.h" +#include "Plottable.h" +#include "../axis/Axis.h" +#include +#include +#include +namespace renderive { +struct Spectrum_Properties : Hover_Tooltip_Properties { + Nonnegative_Count frequency_point_size; + Range frequency_range{}; + double center_frequency = 50.0; + Range sweep_frequency_range{40.0, 60.0}; + bool max_hold_visible{}; + bool min_hold_visible{}; + bool max_marker_visible{}; + bool use_min_marker{}; + bool sweep_region_visible{}; + bool visible_range_only{true}; + Line_Interpolation_Mode interpolation_mode = Line_Interpolation_Mode::Linear_Value; + Brush max_brush; + Brush current_brush; + Brush min_brush; + Pen max_pen{Color::red()}; + Pen current_pen{Color::green()}; + Pen min_pen{Color::white()}; + Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; + Pen marker_pen{Color::red()}; + Pen middle_frequency_pen{Color::red()}; + Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::Solid}; +}; +namespace detail { +class LIB_DECL Spectrum_Control : public Plottable_State, public Event_Handler { +public: + Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + ~Spectrum_Control() override; + void update_samples(std::span values); + void update_samples(std::pmr::vector&& values); + template + void update_samples(const Values& values) { + update_samples(std::span(values.data(), values.size())); + } + [[nodiscard]] std::size_t sample_count() const; + [[nodiscard]] std::size_t rendered_point_count() const; + [[nodiscard]] double power_at(double frequency, bool& ok) const; + void add_custom_marker(double frequency); + void add_custom_line_marker(double frequency); + void remove_custom_marker(double frequency); + void remove_selected_marker(); + void clear_custom_markers(); + [[nodiscard]] int selectable_line_marker_count() const; + [[nodiscard]] int selected_marker_index() const; + void set_selected_marker_index(int index); + void select_next_marker(); + void select_previous_marker(); + void clear_marker_selection(); + [[nodiscard]] double marker_frequency(int index) const; + void set_marker_frequency(int index, double frequency); + void set_current_marker_frequency(double frequency); + void handle_event(const Event& event) override; +protected: + void paint(Painter& painter) override; +private: + struct Impl; + std::unique_ptr impl_; + void publish() override; +}; +} +using Spectrum = detail::Attach_Plottable; +} diff --git a/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/plottable/Sweep_Spectrum.cpp new file mode 100644 index 0000000..de15c29 --- /dev/null +++ b/render_2D/plottable/Sweep_Spectrum.cpp @@ -0,0 +1,94 @@ +#include "Sweep_Spectrum.h" +#include "Curve_Sampling.h" +#include "../render/Blend2D_Cache.h" +#include +#include +#include +namespace renderive { +namespace detail { +namespace { +struct Sweep_Spectrum_Runtime_Base {}; +struct Sweep_Spectrum_Runtime { + std::deque> blocks; +}; +using Sweep_Spectrum_Runtime_State = Double_State_Strategy; +RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { + const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); + const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); + const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); + const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); + return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; +} +std::vector flatten(const Sweep_Spectrum_Runtime& runtime) { + std::vector values; + for(const auto& block : runtime.blocks) + values.insert(values.end(), block.begin(), block.end()); + return values; +} +} +struct Sweep_Spectrum_Control::Impl { + Impl(std::shared_ptr frequency, std::shared_ptr power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {} + std::shared_ptr frequency_axis; + std::shared_ptr power_axis; + Sweep_Spectrum_Runtime_State runtime; +}; +Sweep_Spectrum_Control::Sweep_Spectrum_Control(Plot_Core& plot, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) + : Plottable_State(plot, properties), impl_(std::make_unique(std::move(frequency_axis), std::move(power_axis))) {} +Sweep_Spectrum_Control::~Sweep_Spectrum_Control() = default; +void Sweep_Spectrum_Control::append_block(std::span values) { + if(get<&Sweep_Spectrum_Properties::bins_per_block>() <= 0) + set<&Sweep_Spectrum_Properties::bins_per_block>(static_cast(values.size())); + const int limit = get<&Sweep_Spectrum_Properties::block_count>(); + impl_->runtime.update([values, limit](Sweep_Spectrum_Runtime& runtime) { + runtime.blocks.emplace_back(values.begin(), values.end()); + while(runtime.blocks.size() > static_cast(limit)) + runtime.blocks.pop_front(); + }); + changed(); +} +void Sweep_Spectrum_Control::append_block(std::pmr::vector&& values) { + append_block(std::span(values.data(), values.size())); +} +std::size_t Sweep_Spectrum_Control::stored_block_count() const { + return impl_->runtime.read([](const Sweep_Spectrum_Runtime& runtime) { return runtime.blocks.size(); }); +} +std::size_t Sweep_Spectrum_Control::stored_point_count() const { + return impl_->runtime.read([](const Sweep_Spectrum_Runtime& runtime) { + std::size_t count{}; + for(const auto& block : runtime.blocks) + count += block.size(); + return count; + }); +} +std::size_t Sweep_Spectrum_Control::rendered_point_count() const { + const auto state = properties(); + const auto runtime = impl_->runtime.read([](const Sweep_Spectrum_Runtime& value) { return value; }); + const auto values = flatten(runtime); + return curve_points(values, state.frequency_range, impl_->frequency_axis->transform(), impl_->power_axis->transform(), state.visible_range_only, state.interpolation_mode).size(); +} +void Sweep_Spectrum_Control::publish() { + publish_properties(); + const int limit = get<&Sweep_Spectrum_Properties::block_count>(); + impl_->runtime.update([limit](Sweep_Spectrum_Runtime& runtime) { + while(runtime.blocks.size() > static_cast(limit)) + runtime.blocks.pop_front(); + }); + impl_->runtime.publish(); +} +void Sweep_Spectrum_Control::paint(Painter& painter) { + const auto state = render_properties(); + const auto runtime = impl_->runtime.render_use_state(); + const auto values = flatten(runtime); + if(values.size() < 2) + return; + const Axis_Transform x = impl_->frequency_axis->transform(); + const Axis_Transform y = impl_->power_axis->transform(); + painter.polyline(curve_points(values, state.frequency_range, x, y, state.visible_range_only, state.interpolation_mode), state.pen); + const double completed = std::min(1.0, static_cast(runtime.blocks.size()) / state.block_count.get()); + const double frequency = state.frequency_range.origin + state.frequency_range.length() * completed; + const RectF content = axis_content_rect(x, y); + const double marker_x = x.coord_to_pixel(frequency); + painter.line({marker_x, content.y}, {marker_x, content.bottom()}, state.current_frequency_pen); +} +} +} diff --git a/render_2D/plottable/Sweep_Spectrum.h b/render_2D/plottable/Sweep_Spectrum.h new file mode 100644 index 0000000..728fb50 --- /dev/null +++ b/render_2D/plottable/Sweep_Spectrum.h @@ -0,0 +1,39 @@ +#pragma once +#include "Plottable.h" +#include "../axis/Axis.h" +#include +#include +namespace renderive { +struct Sweep_Spectrum_Properties { + Range frequency_range{}; + Nonnegative_Count bins_per_block; + Positive_Count block_count; + Pen pen{Color::yellow()}; + Pen current_frequency_pen{Color::red(), 2.0}; + bool visible_range_only{true}; + Line_Interpolation_Mode interpolation_mode = Line_Interpolation_Mode::Linear_Value; +}; +namespace detail { +class LIB_DECL Sweep_Spectrum_Control : public Plottable_State { +public: + Sweep_Spectrum_Control(Plot_Core& plot, const Sweep_Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis); + ~Sweep_Spectrum_Control() override; + void append_block(std::span values); + void append_block(std::pmr::vector&& values); + template + void append_block(const Values& values) { + append_block(std::span(values.data(), values.size())); + } + [[nodiscard]] std::size_t stored_block_count() const; + [[nodiscard]] std::size_t stored_point_count() const; + [[nodiscard]] std::size_t rendered_point_count() const; +protected: + void paint(Painter& painter) override; +private: + struct Impl; + std::unique_ptr impl_; + void publish() override; +}; +} +using Sweep_Spectrum = detail::Attach_Plottable; +} diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp new file mode 100644 index 0000000..650ef5b --- /dev/null +++ b/render_2D/plottable/Waterfall.cpp @@ -0,0 +1,118 @@ +#include "Waterfall.h" +#include "Heatmap_Utils.h" +#include +#include +#include +#include +namespace renderive { +namespace detail { +namespace { +struct Waterfall_Row { + int tick{}; + std::vector values; +}; +struct Waterfall_Runtime_Base {}; +struct Waterfall_Runtime { + std::deque rows; + Hover_Tooltip_Runtime tooltip; +}; +using Waterfall_Runtime_State = Double_State_Strategy; +} +struct Waterfall_Control::Impl { + Impl(std::shared_ptr frequency, std::shared_ptr time) : frequency_axis(std::move(frequency)), time_axis(std::move(time)) {} + std::shared_ptr frequency_axis; + std::shared_ptr time_axis; + Waterfall_Runtime_State runtime; +}; +Waterfall_Control::Waterfall_Control(Plot_Core& plot, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis) + : Plottable_State(plot, properties), impl_(std::make_unique(std::move(frequency_axis), std::move(time_axis))) {} +Waterfall_Control::~Waterfall_Control() = default; +void Waterfall_Control::append_row(int tick, std::span values) { + const std::size_t limit = static_cast(std::max(2, impl_->time_axis->visible_time_point_count())); + if(get<&Waterfall_Properties::frequency_bin_count>() <= 0) + set<&Waterfall_Properties::frequency_bin_count>(static_cast(values.size())); + impl_->runtime.update([tick, values, limit](Waterfall_Runtime& runtime) { + runtime.rows.push_back({tick, {values.begin(), values.end()}}); + while(runtime.rows.size() > limit) + runtime.rows.pop_front(); + }); + changed(); +} +void Waterfall_Control::append_row(int tick, std::pmr::vector&& values) { + append_row(tick, std::span(values.data(), values.size())); +} +void Waterfall_Control::append_row(Time_Of_Day time, std::span values) { + append_row(impl_->time_axis->append_time(time), values); +} +void Waterfall_Control::append_row(Time_Of_Day time, std::pmr::vector&& values) { + append_row(time, std::span(values.data(), values.size())); +} +std::size_t Waterfall_Control::row_count() const { + return impl_->runtime.read([](const Waterfall_Runtime& runtime) { return runtime.rows.size(); }); +} +std::size_t Waterfall_Control::stored_point_count() const { + return impl_->runtime.read([](const Waterfall_Runtime& runtime) { + std::size_t count{}; + for(const auto& row : runtime.rows) + count += row.values.size(); + return count; + }); +} +std::size_t Waterfall_Control::rendered_cell_count() const { + const auto state = properties(); + const auto runtime = impl_->runtime.read([](const Waterfall_Runtime& value) { return value; }); + if(runtime.rows.empty()) + return 0; + const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(runtime.rows.begin(), runtime.rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); + if(source_width <= 0) + return 0; + const auto columns = frequency_columns(state.frequency_range, impl_->frequency_axis->coord_range(), source_width, state.visible_range_only); + return columns ? static_cast(columns->last - columns->first + 1) * runtime.rows.size() : 0; +} +void Waterfall_Control::handle_event(const Event& event) { + bool updated{}; + impl_->runtime.update([&](Waterfall_Runtime& runtime) { updated = update_hover_tooltip(runtime.tooltip, event); }); + if(updated) + changed(); +} +void Waterfall_Control::publish() { + publish_properties(); + impl_->runtime.publish(); +} +void Waterfall_Control::paint(Painter& painter) { + const auto state = render_properties(); + const auto runtime = impl_->runtime.render_use_state(); + if(runtime.rows.empty()) + return; + const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(runtime.rows.begin(), runtime.rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); + const int height = static_cast(runtime.rows.size()); + if(source_width <= 0 || height <= 0) + return; + const Axis_Transform horizontal = impl_->frequency_axis->transform(); + const auto columns = frequency_columns(state.frequency_range, horizontal.coordinate_range, source_width, state.visible_range_only); + if(!columns) + return; + const int width = columns->last - columns->first + 1; + std::vector pixels(static_cast(width) * height); + for(int y = 0; y < height; ++y) { + const auto& row = runtime.rows[static_cast(y)].values; + for(int x = 0; x < width; ++x) + pixels[static_cast(y) * width + x] = state.color_map.at_normalized(normalized_value(row[static_cast(columns->first + x)], state.power_range)); + } + const Axis_Transform vertical = impl_->time_axis->transform(); + const Range time_range{static_cast(runtime.rows.front().tick), static_cast(runtime.rows.back().tick)}; + RectF target = mapped_rect(horizontal, vertical, columns->range, time_range); + if(target.height < 1.0) + target.height = std::max(1.0, static_cast(impl_->time_axis->pixel_length())); + painter.heatmap(target, width, height, pixels, state.interpolation_mode); + if(state.tooltip_enabled && runtime.tooltip.active && target.contains(runtime.tooltip.position)) { + const double frequency = horizontal.pixel_to_coord(runtime.tooltip.position.x); + std::ostringstream text; + text << std::fixed << std::setprecision(2) << frequency << " Hz"; + const RectF box{runtime.tooltip.position.x + 8.0, runtime.tooltip.position.y + 8.0, 110.0, 24.0}; + painter.rect(box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); + painter.text({box.x + 4.0, box.y + 3.0}, text.str(), state.tooltip_font, state.tooltip_text_pen); + } +} +} +} diff --git a/render_2D/plottable/Waterfall.h b/render_2D/plottable/Waterfall.h new file mode 100644 index 0000000..809e664 --- /dev/null +++ b/render_2D/plottable/Waterfall.h @@ -0,0 +1,46 @@ +#pragma once +#include "Hover_Tooltip.h" +#include "Plottable.h" +#include "../axis/Axis.h" +#include +#include +namespace renderive { +struct Waterfall_Properties : Hover_Tooltip_Properties { + Range frequency_range{0.0, 10.0}; + Range power_range{0.0, 10.0}; + Nonnegative_Count frequency_bin_count; + bool visible_range_only{true}; + Image_Interpolation_Mode interpolation_mode = Image_Interpolation_Mode::Nearest; + Color_Map color_map; +}; +namespace detail { +class LIB_DECL Waterfall_Control : public Plottable_State, public Event_Handler { +public: + Waterfall_Control(Plot_Core& plot, const Waterfall_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr time_axis); + ~Waterfall_Control() override; + void append_row(int tick, std::span values); + void append_row(int tick, std::pmr::vector&& values); + void append_row(Time_Of_Day time, std::span values); + void append_row(Time_Of_Day time, std::pmr::vector&& values); + template + void append_row(int tick, const Values& values) { + append_row(tick, std::span(values.data(), values.size())); + } + template + void append_row(Time_Of_Day time, const Values& values) { + append_row(time, std::span(values.data(), values.size())); + } + [[nodiscard]] std::size_t row_count() const; + [[nodiscard]] std::size_t stored_point_count() const; + [[nodiscard]] std::size_t rendered_cell_count() const; + void handle_event(const Event& event) override; +protected: + void paint(Painter& painter) override; +private: + struct Impl; + std::unique_ptr impl_; + void publish() override; +}; +} +using Waterfall = detail::Attach_Plottable; +} diff --git a/render_2D/renderable/Renderable_Builder.h b/render_2D/renderable/Renderable_Builder.h new file mode 100644 index 0000000..da31a59 --- /dev/null +++ b/render_2D/renderable/Renderable_Builder.h @@ -0,0 +1,53 @@ +#pragma once +#include "Renderable.h" +#include "../plot/Plot_Core.h" +#include +#include +#include +#include +#include +namespace renderive { +template Validator_Type = No_Property_Validator> +class Renderable_Builder { +public: + using Product = Product_Type; + using Properties = Properties_Type; + using Validator = Validator_Type; + using Self = Renderable_Builder; + Renderable_Builder() requires std::default_initializable && std::default_initializable : properties{}, validator{} {} + explicit Renderable_Builder(Properties properties) requires std::default_initializable : properties(std::move(properties)), validator{} {} + Renderable_Builder(Properties properties, Validator validator) : properties(std::move(properties)), validator(std::move(validator)) {} + template Value> + Self& set(Value&& value) { + properties.*Member = std::forward(value); + return *this; + } + template Configure> + Self& configure(Configure&& configure) { + std::invoke(std::forward(configure), properties); + return *this; + } + const Properties& properties_value() const noexcept { + return properties; + } + template + requires std::derived_from && std::constructible_from + std::shared_ptr build(const std::shared_ptr& parent, Args&&... args) const { + if(!parent || !(valid_argument(args) && ...)) + return {}; + validator(properties); + return parent->plot().make_renderable(parent, properties, std::forward(args)...); + } +private: + template + static bool valid_argument(const std::shared_ptr& value) { + return static_cast(value); + } + template + static bool valid_argument(const T&) { + return true; + } + Properties properties; + [[no_unique_address]] Validator validator; +}; +} diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 64f8c41..0cd689f 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -114,11 +114,11 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { .set_pixel_length(142) .set_coord_range({-120.0, 0.0}) .build(); - auto spectrum = Spectrum::Builder(root, frequency_axis, power_axis) - .set_frequency_range({88.0, 108.0}) - .set_frequency_point_size(64) - .set_max_hold_visible(true) - .build(); + auto spectrum = Spectrum::Builder{} + .set<&Spectrum::Properties::frequency_range>(Range{88.0, 108.0}) + .set<&Spectrum::Properties::frequency_point_size>(64) + .set<&Spectrum::Properties::max_hold_visible>(true) + .build(root, frequency_axis, power_axis); std::vector samples(64); for (std::size_t index = 0; index < samples.size(); ++index) samples[index] = -100.0 + static_cast(index % 24) * 3.0; @@ -320,46 +320,44 @@ TEST(Renderive_Core2, RetainedSpectrumApisRoundTripAndMarkersRemainObservable) { const auto power = Axis::Builder(root, Orientation::Vertical) .set_x(30).set_y(20).set_pixel_length(190) .set_coord_range({-20.0, -120.0}).build(); - auto spectrum = Spectrum::Builder(root, frequency, power) - .set_frequency_range({90.0, 110.0}) - .set_frequency_point_size(4) - .set_center_frequency(100.0) - .set_sweep_frequency_range({96.0, 104.0}) - .set_max_hold_visible(true) - .set_min_hold_visible(true) - .set_max_marker_visible(true) - .set_use_min_marker(true) - .set_sweep_region_visible(true) - .set_visible_range_only(false) - .set_interpolation_mode(Line_Interpolation_Mode::Cubic_Value) - .build(); + auto spectrum = Spectrum::Builder{} + .set<&Spectrum::Properties::frequency_range>(Range{90.0, 110.0}) + .set<&Spectrum::Properties::frequency_point_size>(4) + .set<&Spectrum::Properties::center_frequency>(100.0) + .set<&Spectrum::Properties::sweep_frequency_range>(Range{96.0, 104.0}) + .set<&Spectrum::Properties::max_hold_visible>(true) + .set<&Spectrum::Properties::min_hold_visible>(true) + .set<&Spectrum::Properties::max_marker_visible>(true) + .set<&Spectrum::Properties::use_min_marker>(true) + .set<&Spectrum::Properties::sweep_region_visible>(true) + .set<&Spectrum::Properties::visible_range_only>(false) + .set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value) + .build(root, frequency, power); ASSERT_TRUE(spectrum); - spectrum->set_max_brush({Color{1, 2, 3, 80}, Brush_Style::Solid}); - spectrum->set_current_brush({Color{4, 5, 6, 90}, Brush_Style::Solid}); - spectrum->set_min_brush({Color{7, 8, 9, 100}, Brush_Style::Solid}); - spectrum->set_max_pen({Color{10, 20, 30, 255}, 2.0}); - spectrum->set_current_pen({Color{40, 50, 60, 255}, 2.5}); - spectrum->set_min_pen({Color{70, 80, 90, 255}, 3.0}); - spectrum->set_selected_marker_pen({Color{11, 22, 33, 255}, 4.0}); - spectrum->set_marker_pen({Color{44, 55, 66, 255}, 1.5}); - spectrum->set_middle_frequency_pen({Color{77, 88, 99, 255}, 1.25}); - spectrum->set_sweep_region_brush({Color{12, 34, 56, 70}, Brush_Style::Solid}); - EXPECT_EQ(spectrum->frequency_axis(), frequency); - EXPECT_EQ(spectrum->power_axis(), power); - EXPECT_EQ(spectrum->frequency_point_size(), 4); - EXPECT_EQ(spectrum->frequency_range(), (Range{90.0, 110.0})); - EXPECT_DOUBLE_EQ(spectrum->center_frequency(), 100.0); - EXPECT_EQ(spectrum->sweep_frequency_range(), (Range{96.0, 104.0})); - EXPECT_TRUE(spectrum->max_hold_visible()); - EXPECT_TRUE(spectrum->min_hold_visible()); - EXPECT_TRUE(spectrum->max_marker_visible()); - EXPECT_TRUE(spectrum->use_min_marker()); - EXPECT_TRUE(spectrum->sweep_region_visible()); - EXPECT_FALSE(spectrum->visible_range_only()); - EXPECT_EQ(spectrum->interpolation_mode(), Line_Interpolation_Mode::Cubic_Value); - EXPECT_EQ(spectrum->max_brush(), (Brush{Color{1, 2, 3, 80}, Brush_Style::Solid})); - EXPECT_EQ(spectrum->current_pen(), (Pen{Color{40, 50, 60, 255}, 2.5})); - EXPECT_EQ(spectrum->sweep_region_brush(), + spectrum->set<&Spectrum::Properties::max_brush>(Brush{Color{1, 2, 3, 80}, Brush_Style::Solid}); + spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{4, 5, 6, 90}, Brush_Style::Solid}); + spectrum->set<&Spectrum::Properties::min_brush>(Brush{Color{7, 8, 9, 100}, Brush_Style::Solid}); + spectrum->set<&Spectrum::Properties::max_pen>(Pen{Color{10, 20, 30, 255}, 2.0}); + spectrum->set<&Spectrum::Properties::current_pen>(Pen{Color{40, 50, 60, 255}, 2.5}); + spectrum->set<&Spectrum::Properties::min_pen>(Pen{Color{70, 80, 90, 255}, 3.0}); + spectrum->set<&Spectrum::Properties::selected_marker_pen>(Pen{Color{11, 22, 33, 255}, 4.0}); + spectrum->set<&Spectrum::Properties::marker_pen>(Pen{Color{44, 55, 66, 255}, 1.5}); + spectrum->set<&Spectrum::Properties::middle_frequency_pen>(Pen{Color{77, 88, 99, 255}, 1.25}); + spectrum->set<&Spectrum::Properties::sweep_region_brush>(Brush{Color{12, 34, 56, 70}, Brush_Style::Solid}); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::frequency_point_size>(), 4); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::frequency_range>(), (Range{90.0, 110.0})); + EXPECT_DOUBLE_EQ(spectrum->get<&Spectrum::Properties::center_frequency>(), 100.0); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::sweep_frequency_range>(), (Range{96.0, 104.0})); + EXPECT_TRUE(spectrum->get<&Spectrum::Properties::max_hold_visible>()); + EXPECT_TRUE(spectrum->get<&Spectrum::Properties::min_hold_visible>()); + EXPECT_TRUE(spectrum->get<&Spectrum::Properties::max_marker_visible>()); + EXPECT_TRUE(spectrum->get<&Spectrum::Properties::use_min_marker>()); + EXPECT_TRUE(spectrum->get<&Spectrum::Properties::sweep_region_visible>()); + EXPECT_FALSE(spectrum->get<&Spectrum::Properties::visible_range_only>()); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::interpolation_mode>(), Line_Interpolation_Mode::Cubic_Value); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::max_brush>(), (Brush{Color{1, 2, 3, 80}, Brush_Style::Solid})); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::current_pen>(), (Pen{Color{40, 50, 60, 255}, 2.5})); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::sweep_region_brush>(), (Brush{Color{12, 34, 56, 70}, Brush_Style::Solid})); std::pmr::vector samples{std::pmr::get_default_resource()}; samples.assign({-100.0, -80.0, -70.0, -50.0}); @@ -403,21 +401,19 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { const auto time = Time_Axis::Builder(root, Orientation::Vertical) .set_x(40).set_y(20).set_pixel_length(230) .set_visible_time_point_count(8).build(); - auto waterfall = Waterfall::Builder(root, frequency, time) - .set_frequency_range({0.0, 4.0}) - .set_power_range({-120.0, 0.0}) - .set_frequency_bin_count(4) - .set_visible_range_only(false) - .set_interpolation_mode(Image_Interpolation_Mode::Bicubic) - .build(); + auto waterfall = Waterfall::Builder{} + .set<&Waterfall::Properties::frequency_range>(Range{0.0, 4.0}) + .set<&Waterfall::Properties::power_range>(Range{-120.0, 0.0}) + .set<&Waterfall::Properties::frequency_bin_count>(4) + .set<&Waterfall::Properties::visible_range_only>(false) + .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bicubic) + .build(root, frequency, time); ASSERT_TRUE(waterfall); - EXPECT_EQ(waterfall->frequency_axis(), frequency); - EXPECT_EQ(waterfall->time_axis(), time); - EXPECT_EQ(waterfall->frequency_range(), (Range{0.0, 4.0})); - EXPECT_EQ(waterfall->power_range(), (Range{-120.0, 0.0})); - EXPECT_EQ(waterfall->frequency_bin_count(), 4); - EXPECT_FALSE(waterfall->visible_range_only()); - EXPECT_EQ(waterfall->interpolation_mode(), Image_Interpolation_Mode::Bicubic); + EXPECT_EQ(waterfall->get<&Waterfall::Properties::frequency_range>(), (Range{0.0, 4.0})); + EXPECT_EQ(waterfall->get<&Waterfall::Properties::power_range>(), (Range{-120.0, 0.0})); + EXPECT_EQ(waterfall->get<&Waterfall::Properties::frequency_bin_count>(), 4); + EXPECT_FALSE(waterfall->get<&Waterfall::Properties::visible_range_only>()); + EXPECT_EQ(waterfall->get<&Waterfall::Properties::interpolation_mode>(), Image_Interpolation_Mode::Bicubic); const std::array row1{-100.0, -80.0, -60.0, -40.0}; waterfall->append_row(Time_Of_Day{1000}, row1); std::pmr::vector row2{std::pmr::get_default_resource()}; @@ -426,23 +422,21 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { EXPECT_EQ(waterfall->row_count(), 2U); EXPECT_EQ(waterfall->stored_point_count(), 8U); EXPECT_EQ(waterfall->rendered_cell_count(), 8U); - auto afterglow = Afterglow::Builder(root, frequency, power) - .set_frequency_range({0.0, 4.0}) - .set_power_range({-120.0, 0.0}) - .set_frequency_bin_count(4) - .set_power_bin_count(8) - .set_interpolate_power_bins(false) - .set_decay_rate(0.35) - .build(); + auto afterglow = Afterglow::Builder{} + .set<&Afterglow::Properties::frequency_range>(Range{0.0, 4.0}) + .set<&Afterglow::Properties::power_range>(Range{-120.0, 0.0}) + .set<&Afterglow::Properties::frequency_point_size>(4) + .set<&Afterglow::Properties::power_point_size>(8) + .set<&Afterglow::Properties::interpolate>(false) + .set<&Afterglow::Properties::attenuation_rate>(0.35) + .build(root, frequency, power); ASSERT_TRUE(afterglow); - EXPECT_EQ(afterglow->frequency_axis(), frequency); - EXPECT_EQ(afterglow->power_axis(), power); - EXPECT_EQ(afterglow->frequency_range(), (Range{0.0, 4.0})); - EXPECT_EQ(afterglow->power_range(), (Range{-120.0, 0.0})); - EXPECT_EQ(afterglow->frequency_point_size(), 4); - EXPECT_EQ(afterglow->power_point_size(), 8); - EXPECT_FALSE(afterglow->interpolate()); - EXPECT_DOUBLE_EQ(afterglow->attenuation_rate(), 0.35); + EXPECT_EQ(afterglow->get<&Afterglow::Properties::frequency_range>(), (Range{0.0, 4.0})); + EXPECT_EQ(afterglow->get<&Afterglow::Properties::power_range>(), (Range{-120.0, 0.0})); + EXPECT_EQ(afterglow->get<&Afterglow::Properties::frequency_point_size>(), 4); + EXPECT_EQ(afterglow->get<&Afterglow::Properties::power_point_size>(), 8); + EXPECT_FALSE(afterglow->get<&Afterglow::Properties::interpolate>()); + EXPECT_DOUBLE_EQ(afterglow->get<&Afterglow::Properties::attenuation_rate>(), 0.35); afterglow->append_spectrum(row1); std::pmr::vector glow2{std::pmr::get_default_resource()}; glow2.assign({-95.0, -75.0, -55.0, -35.0}); @@ -450,25 +444,23 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { EXPECT_EQ(afterglow->history_count(), 2U); EXPECT_EQ(afterglow->latest_spectrum_point_count(), 4U); EXPECT_EQ(afterglow->rendered_cell_count(), 32U); - afterglow->set_attenuation_rate(2.0); - EXPECT_DOUBLE_EQ(afterglow->attenuation_rate(), 1.0); - auto sweep = Sweep_Spectrum::Builder(root, frequency, power) - .set_frequency_range({0.0, 8.0}) - .set_bins_per_block(4) - .set_block_num(2) - .set_pen({Color{1, 120, 220, 255}, 2.0}) - .set_current_frequency_pen({Color{240, 80, 20, 255}, 3.0}) - .set_visible_range_only(false) - .set_interpolation_mode(Line_Interpolation_Mode::Step_Right) - .build(); + afterglow->set<&Afterglow::Properties::attenuation_rate>(2.0); + EXPECT_DOUBLE_EQ(afterglow->get<&Afterglow::Properties::attenuation_rate>(), 1.0); + auto sweep = Sweep_Spectrum::Builder{} + .set<&Sweep_Spectrum::Properties::frequency_range>(Range{0.0, 8.0}) + .set<&Sweep_Spectrum::Properties::bins_per_block>(4) + .set<&Sweep_Spectrum::Properties::block_count>(2) + .set<&Sweep_Spectrum::Properties::pen>(Pen{Color{1, 120, 220, 255}, 2.0}) + .set<&Sweep_Spectrum::Properties::current_frequency_pen>(Pen{Color{240, 80, 20, 255}, 3.0}) + .set<&Sweep_Spectrum::Properties::visible_range_only>(false) + .set<&Sweep_Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Step_Right) + .build(root, frequency, power); ASSERT_TRUE(sweep); - EXPECT_EQ(sweep->frequency_axis(), frequency); - EXPECT_EQ(sweep->power_axis(), power); - EXPECT_EQ(sweep->frequency_range(), (Range{0.0, 8.0})); - EXPECT_EQ(sweep->bins_per_block(), 4); - EXPECT_EQ(sweep->block_count(), 2); - EXPECT_FALSE(sweep->visible_range_only()); - EXPECT_EQ(sweep->interpolation_mode(), Line_Interpolation_Mode::Step_Right); + EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::frequency_range>(), (Range{0.0, 8.0})); + EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::bins_per_block>(), 4); + EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::block_count>(), 2); + EXPECT_FALSE(sweep->get<&Sweep_Spectrum::Properties::visible_range_only>()); + EXPECT_EQ(sweep->get<&Sweep_Spectrum::Properties::interpolation_mode>(), Line_Interpolation_Mode::Step_Right); sweep->append_block(row1); sweep->append_block(row1); std::pmr::vector block3{std::pmr::get_default_resource()}; @@ -477,14 +469,12 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { EXPECT_EQ(sweep->stored_block_count(), 2U); EXPECT_EQ(sweep->stored_point_count(), 8U); EXPECT_GT(sweep->rendered_point_count(), 0U); - auto trace = Frequency_Trace::Builder(root, time, power) - .set_pen({Color{120, 240, 80, 255}, 2.0}) - .build(); + auto trace = Frequency_Trace::Builder{} + .set<&Frequency_Trace::Properties::pen>(Pen{Color{120, 240, 80, 255}, 2.0}) + .build(root, time, power); ASSERT_TRUE(trace); trace->append_sample(Time_Of_Day{3000}, -80.0); trace->append_sample(Time_Of_Day{4000}, -70.0); - EXPECT_EQ(trace->time_axis(), time); - EXPECT_EQ(trace->value_axis(), power); EXPECT_EQ(trace->sample_count(), 2U); EXPECT_EQ(trace->rendered_point_count(), 2U); EXPECT_TRUE(plot.render_frame(true)); @@ -501,18 +491,18 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa const auto vertical = Axis::Builder(root, Orientation::Vertical) .set_x(30).set_y(20).set_pixel_length(210) .set_coord_range({3.0, -3.0}).build(); - auto selection = Selection_Rectangle_Overlay::Builder(root, horizontal, vertical) - .set_font({14.0, 600, true}) - .set_font_pen({Color{20, 220, 180, 255}, 2.0}) - .set_rect_brush({Color{20, 80, 220, 60}, Brush_Style::Solid}) - .set_border_pen({Color{240, 200, 60, 255}, 2.0, Line_Style::Dash}) - .build(); + auto selection = Selection_Rectangle_Overlay::Builder{} + .set<&Selection_Rectangle_Overlay::Properties::label_font>(Font{14.0, 600, true}) + .set<&Selection_Rectangle_Overlay::Properties::label_pen>(Pen{Color{20, 220, 180, 255}, 2.0}) + .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{20, 80, 220, 60}, Brush_Style::Solid}) + .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{240, 200, 60, 255}, 2.0, Line_Style::Dash}) + .build(root, horizontal, vertical); ASSERT_TRUE(selection); - EXPECT_EQ(selection->label_font(), (Font{14.0, 600, true})); - EXPECT_EQ(selection->label_pen(), (Pen{Color{20, 220, 180, 255}, 2.0})); - EXPECT_EQ(selection->selection_brush(), + EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::label_font>(), (Font{14.0, 600, true})); + EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::label_pen>(), (Pen{Color{20, 220, 180, 255}, 2.0})); + EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::selection_brush>(), (Brush{Color{20, 80, 220, 60}, Brush_Style::Solid})); - EXPECT_EQ(selection->selection_border_pen(), + EXPECT_EQ(selection->get<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(), (Pen{Color{240, 200, 60, 255}, 2.0, Line_Style::Dash})); Pointer_Event press(Event_Type::Pointer_Press); press.position = {80.0, 70.0}; @@ -532,25 +522,21 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa EXPECT_FALSE(selection->selected_regions().front().empty()); selection->clear_selected_regions(); EXPECT_TRUE(selection->selected_regions().empty()); - selection->set_horizontal_axis(horizontal); - selection->set_vertical_axis(vertical); - auto constellation = Constellation_Diagram::Builder(root, horizontal, vertical) - .set_i_range({-2.0, 2.0}) - .set_q_range({-3.0, 3.0}) - .set_point_color({80, 220, 255, 255}) - .set_anchor_color({255, 180, 40, 255}) - .set_point_lifetime_ms(2000) - .set_type(Constellation_Diagram_Type::Psk16) - .set_phase_offset_radians(0.25) - .build(); + auto constellation = Constellation_Diagram::Builder{} + .set<&Constellation_Diagram::Properties::i_range>(Range{-2.0, 2.0}) + .set<&Constellation_Diagram::Properties::q_range>(Range{-3.0, 3.0}) + .set<&Constellation_Diagram::Properties::point_color>(Color{80, 220, 255, 255}) + .set<&Constellation_Diagram::Properties::anchor_color>(Color{255, 180, 40, 255}) + .set<&Constellation_Diagram::Properties::point_lifetime_ms>(2000) + .set<&Constellation_Diagram::Properties::type>(Constellation_Diagram_Type::Psk16) + .set<&Constellation_Diagram::Properties::phase_offset_radians>(0.25) + .build(root, horizontal, vertical); ASSERT_TRUE(constellation); - EXPECT_EQ(constellation->i_axis(), horizontal); - EXPECT_EQ(constellation->q_axis(), vertical); - EXPECT_EQ(constellation->i_range(), (Range{-2.0, 2.0})); - EXPECT_EQ(constellation->q_range(), (Range{-3.0, 3.0})); - EXPECT_EQ(constellation->point_color(), (Color{80, 220, 255, 255})); - EXPECT_EQ(constellation->anchor_color(), (Color{255, 180, 40, 255})); - EXPECT_EQ(constellation->point_lifetime_ms(), 2000); + EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::i_range>(), (Range{-2.0, 2.0})); + EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::q_range>(), (Range{-3.0, 3.0})); + EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::point_color>(), (Color{80, 220, 255, 255})); + EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::anchor_color>(), (Color{255, 180, 40, 255})); + EXPECT_EQ(constellation->get<&Constellation_Diagram::Properties::point_lifetime_ms>(), 2000); constellation->append_point({0.5, -0.5}); constellation->append_point({-0.5, 0.5}); EXPECT_EQ(constellation->point_count(), 2U); @@ -559,5 +545,23 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa EXPECT_EQ(vertical->coord_range(), (Range{3.0, -3.0})); EXPECT_TRUE(plot.render_frame(true)); } +TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { + Plot_Core plot; + plot.init(); + const auto root = plot.root_renderable(); + const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); + const auto power = Axis::Builder(root, Orientation::Vertical).build(); + const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); + ASSERT_TRUE(spectrum); + const auto state = std::dynamic_pointer_cast<::State_Strategy_Base>(spectrum); + ASSERT_TRUE(state); + EXPECT_EQ(state->state_revision(), 0U); + spectrum->set<&Spectrum::Properties::frequency_point_size>(128); + EXPECT_EQ(spectrum->get<&Spectrum::Properties::frequency_point_size>(), 128); + EXPECT_EQ(state->state_revision(), 0U); + ASSERT_TRUE(plot.prepare_frame()); + EXPECT_EQ(state->state_revision(), 1U); + ASSERT_TRUE(plot.render_prepared_frame()); +} } // namespace } // namespace renderive diff --git a/renderive_package.zip b/renderive_package.zip new file mode 100644 index 0000000..7354e60 Binary files /dev/null and b/renderive_package.zip differ diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index f0a9895..cc247b8 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -457,22 +457,12 @@ public: spectrum_->clear_marker_selection(); return "Marker 选择已清除"; } - if (request.id == "rebind_axes") { - spectrum_->set_frequency_axis(spectrum_->frequency_axis()); - spectrum_->set_power_axis(spectrum_->power_axis()); - return "Spectrum 的 shared_ptr 轴已回读并重新绑定"; - } } if (selection_) { if (request.id == "clear_selection") { selection_->clear_selected_regions(); return "框选区域已清空"; } - if (request.id == "rebind_selection_axes") { - selection_->set_horizontal_axis(numeric_domain_axis_); - selection_->set_vertical_axis(value_axis_); - return "Selection Overlay 的两根轴已重新绑定"; - } } if (waterfall_) { if (request.id == "append_row") { @@ -481,40 +471,22 @@ public: } if (request.id == "append_tick_row") { const int tick = time_axis_->append_time(current_time_of_day()); - waterfall_->append_row(tick, spectrum_values(waterfall_->frequency_bin_count(), - waterfall_->power_range())); + waterfall_->append_row(tick, spectrum_values(waterfall_->get<&Waterfall::Properties::frequency_bin_count>(), + waterfall_->get<&Waterfall::Properties::power_range>())); return "Waterfall 已通过 int tick 重载追加一行"; } - if (request.id == "rebind_axes") { - last_action_result_ = waterfall_->frequency_axis() && waterfall_->time_axis() - ? "frequency_axis/time_axis valid" - : "axis invalid"; - return "Waterfall 轴绑定已回读"; - } } if (afterglow_) { if (request.id == "append_spectrum") { push_afterglow_spectrum(); return "Afterglow 频谱已手动追加"; } - if (request.id == "rebind_axes") { - last_action_result_ = afterglow_->frequency_axis() && afterglow_->power_axis() - ? "frequency_axis/power_axis valid" - : "axis invalid"; - return "Afterglow 轴绑定已回读"; - } } if (sweep_) { if (request.id == "append_block") { push_sweep_block(); return "Sweep_Spectrum 扫频块已手动追加"; } - if (request.id == "rebind_axes") { - last_action_result_ = sweep_->frequency_axis() && sweep_->power_axis() - ? "frequency_axis/power_axis valid" - : "axis invalid"; - return "Sweep_Spectrum 轴绑定已回读"; - } } if (trace_) { if (request.id == "append_sample") { @@ -526,12 +498,6 @@ public: trace_->append_sample(tick, std::sin(frame_index_ * 0.12)); return "Frequency_Trace 已通过 int tick 重载追加样本"; } - if (request.id == "read_axes") { - last_action_result_ = trace_->time_axis() && trace_->value_axis() - ? "time_axis/value_axis valid" - : "axis invalid"; - return "Frequency_Trace 轴绑定已回读"; - } } if (constellation_) { if (request.id == "append_points") { @@ -542,12 +508,6 @@ public: constellation_->fit_square_to_axes(); return "Constellation 已按轴拟合正方形"; } - if (request.id == "read_axes") { - last_action_result_ = constellation_->i_axis() && constellation_->q_axis() - ? "i_axis/q_axis valid" - : "axis invalid"; - return "Constellation 轴绑定已回读"; - } } recognized = false; return {}; @@ -764,7 +724,7 @@ public: telemetry["spectrum"] = { {"selectable_line_markers", spectrum_->selectable_line_marker_count()}, {"selected_marker_index", spectrum_->selected_marker_index()}, - {"configured_frequency_points", spectrum_->frequency_point_size()}, + {"configured_frequency_points", spectrum_->get<&Spectrum::Properties::frequency_point_size>()}, {"input_sample_count", spectrum_->sample_count()}, {"rendered_point_count", spectrum_->rendered_point_count()} }; @@ -777,7 +737,7 @@ public: telemetry["selection_regions"] = selection_->selected_regions().size(); if (waterfall_) { telemetry["waterfall"] = { - {"configured_frequency_bins", waterfall_->frequency_bin_count()}, + {"configured_frequency_bins", waterfall_->get<&Waterfall::Properties::frequency_bin_count>()}, {"row_count", waterfall_->row_count()}, {"stored_point_count", waterfall_->stored_point_count()}, {"rendered_cell_count", waterfall_->rendered_cell_count()}, @@ -786,8 +746,8 @@ public: } if (afterglow_) { telemetry["afterglow"] = { - {"configured_frequency_points", afterglow_->frequency_point_size()}, - {"configured_power_points", afterglow_->power_point_size()}, + {"configured_frequency_points", afterglow_->get<&Afterglow::Properties::frequency_point_size>()}, + {"configured_power_points", afterglow_->get<&Afterglow::Properties::power_point_size>()}, {"history_frame_count", afterglow_->history_count()}, {"latest_input_point_count", afterglow_->latest_spectrum_point_count()}, {"rendered_cell_count", afterglow_->rendered_cell_count()} @@ -795,8 +755,8 @@ public: } if (sweep_) { telemetry["sweep_spectrum"] = { - {"configured_bins_per_block", sweep_->bins_per_block()}, - {"configured_block_count", sweep_->block_count()}, + {"configured_bins_per_block", sweep_->get<&Sweep_Spectrum::Properties::bins_per_block>()}, + {"configured_block_count", sweep_->get<&Sweep_Spectrum::Properties::block_count>()}, {"stored_block_count", sweep_->stored_block_count()}, {"stored_point_count", sweep_->stored_point_count()}, {"rendered_point_count", sweep_->rendered_point_count()} @@ -819,7 +779,7 @@ public: "rendered_point_count", constellation_->point_count() + static_cast(anchor_count) }, - {"point_lifetime_ms", constellation_->point_lifetime_ms()} + {"point_lifetime_ms", constellation_->get<&Constellation_Diagram::Properties::point_lifetime_ms>()} }; } if (case_id_ == "axis_lab") { @@ -1158,102 +1118,94 @@ private: return; } if (case_id_ == "spectrum" || case_id_ == "selection_overlay") { - spectrum_ = Spectrum::Builder(data_node_, - std::static_pointer_cast(numeric_domain_axis_), - value_axis_) - .set_frequency_range({ + spectrum_ = Spectrum::Builder{} + .set<&Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) - .set_frequency_point_size(integer_value(state_, "frequency_point_size")) - .set_center_frequency(number_value(state_, "center_frequency")) - .set_sweep_frequency_range({ + .set<&Spectrum::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")) + .set<&Spectrum::Properties::center_frequency>(number_value(state_, "center_frequency")) + .set<&Spectrum::Properties::sweep_frequency_range>(Range{ number_value(state_, "sweep_origin"), number_value(state_, "sweep_target") }) - .build(); + .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), value_axis_); primary_ = spectrum_; if (case_id_ == "selection_overlay") { - selection_ = Selection_Rectangle_Overlay::Builder( - overlay_node_, numeric_domain_axis_, value_axis_) - .build(); + selection_ = Selection_Rectangle_Overlay::Builder{}.build(overlay_node_, numeric_domain_axis_, value_axis_); primary_ = selection_; } return; } if (case_id_ == "waterfall") { - waterfall_ = Waterfall::Builder(data_node_, - std::static_pointer_cast(numeric_domain_axis_), - time_axis_) - .set_frequency_range({ + waterfall_ = Waterfall::Builder{} + .set<&Waterfall::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) - .set_power_range({ + .set<&Waterfall::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }) - .set_frequency_bin_count(integer_value(state_, "frequency_bin_count")) - .set_color_map(gallery_color_map(string_value(state_, "color_map"))) - .build(); + .set<&Waterfall::Properties::frequency_bin_count>(integer_value(state_, "frequency_bin_count")) + .set<&Waterfall::Properties::color_map>(gallery_color_map(string_value(state_, "color_map"))) + .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), time_axis_); primary_ = waterfall_; return; } if (case_id_ == "afterglow") { - afterglow_ = Afterglow::Builder(data_node_, - std::static_pointer_cast(numeric_domain_axis_), - value_axis_) - .set_frequency_range({ + afterglow_ = Afterglow::Builder{} + .set<&Afterglow::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) - .set_power_range({ + .set<&Afterglow::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }) - .set_frequency_bin_count(integer_value(state_, "frequency_point_size")) - .set_power_bin_count(integer_value(state_, "power_point_size")) - .set_color_map(gallery_color_map(string_value(state_, "color_map"))) - .build(); + .set<&Afterglow::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")) + .set<&Afterglow::Properties::power_point_size>(integer_value(state_, "power_point_size")) + .set<&Afterglow::Properties::color_map>(gallery_color_map(string_value(state_, "color_map"))) + .build(data_node_, std::static_pointer_cast(numeric_domain_axis_), value_axis_); primary_ = afterglow_; return; } if (case_id_ == "sweep_spectrum") { - sweep_ = Sweep_Spectrum::Builder(data_node_, numeric_domain_axis_, value_axis_) - .set_frequency_range({ + sweep_ = Sweep_Spectrum::Builder{} + .set<&Sweep_Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }) - .set_bins_per_block(integer_value(state_, "bins_per_block")) - .set_block_num(integer_value(state_, "block_count")) - .build(); + .set<&Sweep_Spectrum::Properties::bins_per_block>(integer_value(state_, "bins_per_block")) + .set<&Sweep_Spectrum::Properties::block_count>(integer_value(state_, "block_count")) + .build(data_node_, numeric_domain_axis_, value_axis_); primary_ = sweep_; return; } if (case_id_ == "frequency_trace") { - trace_ = Frequency_Trace::Builder(data_node_, time_axis_, value_axis_) - .set_pen({ + trace_ = Frequency_Trace::Builder{} + .set<&Frequency_Trace::Properties::pen>(Pen{ color_from_hex(string_value(state_, "trace_pen")), number_value(state_, "trace_pen_width"), Line_Style::Solid, Line_Cap::Round, Line_Join::Round }) - .build(); + .build(data_node_, time_axis_, value_axis_); primary_ = trace_; return; } if (case_id_ == "constellation") { - constellation_ = Constellation_Diagram::Builder(data_node_, numeric_domain_axis_, value_axis_) - .set_i_range({ + constellation_ = Constellation_Diagram::Builder{} + .set<&Constellation_Diagram::Properties::i_range>(Range{ number_value(state_, "i_origin"), number_value(state_, "i_target") }) - .set_q_range({ + .set<&Constellation_Diagram::Properties::q_range>(Range{ number_value(state_, "q_origin"), number_value(state_, "q_target") }) - .set_type(constellation_mode(string_value(state_, "constellation_type"))) - .set_phase_offset_radians(number_value(state_, "phase_offset")) - .build(); + .set<&Constellation_Diagram::Properties::type>(constellation_mode(string_value(state_, "constellation_type"))) + .set<&Constellation_Diagram::Properties::phase_offset_radians>(number_value(state_, "phase_offset")) + .build(data_node_, numeric_domain_axis_, value_axis_); primary_ = constellation_; } } @@ -1366,103 +1318,101 @@ private: } void apply_specific_state() { if (spectrum_) { - spectrum_->set_frequency_point_size(integer_value(state_, "frequency_point_size")); - spectrum_->set_frequency_range({ + spectrum_->set<&Spectrum::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")); + spectrum_->set<&Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); - spectrum_->set_center_frequency(number_value(state_, "center_frequency")); - spectrum_->set_sweep_frequency_range({ + spectrum_->set<&Spectrum::Properties::center_frequency>(number_value(state_, "center_frequency")); + spectrum_->set<&Spectrum::Properties::sweep_frequency_range>(Range{ number_value(state_, "sweep_origin"), number_value(state_, "sweep_target") }); - spectrum_->set_max_hold_visible(bool_value(state_, "max_hold_visible")); - spectrum_->set_min_hold_visible(bool_value(state_, "min_hold_visible")); - spectrum_->set_max_marker_visible(bool_value(state_, "max_marker_visible")); - spectrum_->set_use_min_marker(bool_value(state_, "use_min_marker")); - spectrum_->set_sweep_region_visible(bool_value(state_, "sweep_region_visible")); - spectrum_->set_visible_range_only(bool_value(state_, "visible_range_only")); - spectrum_->set_interpolation_mode(line_mode(string_value(state_, "line_interpolation"))); - spectrum_->set_max_brush({color_from_hex(string_value(state_, "max_brush"), 76), Brush_Style::Solid}); - spectrum_->set_current_brush({color_from_hex(string_value(state_, "current_brush"), 76), Brush_Style::Solid}); - spectrum_->set_min_brush({color_from_hex(string_value(state_, "min_brush"), 76), Brush_Style::Solid}); - spectrum_->set_max_pen({color_from_hex(string_value(state_, "max_pen")), 1.2}); - spectrum_->set_current_pen({ + spectrum_->set<&Spectrum::Properties::max_hold_visible>(bool_value(state_, "max_hold_visible")); + spectrum_->set<&Spectrum::Properties::min_hold_visible>(bool_value(state_, "min_hold_visible")); + spectrum_->set<&Spectrum::Properties::max_marker_visible>(bool_value(state_, "max_marker_visible")); + spectrum_->set<&Spectrum::Properties::use_min_marker>(bool_value(state_, "use_min_marker")); + spectrum_->set<&Spectrum::Properties::sweep_region_visible>(bool_value(state_, "sweep_region_visible")); + spectrum_->set<&Spectrum::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); + spectrum_->set<&Spectrum::Properties::interpolation_mode>(line_mode(string_value(state_, "line_interpolation"))); + spectrum_->set<&Spectrum::Properties::max_brush>(Brush{color_from_hex(string_value(state_, "max_brush"), 76), Brush_Style::Solid}); + spectrum_->set<&Spectrum::Properties::current_brush>(Brush{color_from_hex(string_value(state_, "current_brush"), 76), Brush_Style::Solid}); + spectrum_->set<&Spectrum::Properties::min_brush>(Brush{color_from_hex(string_value(state_, "min_brush"), 76), Brush_Style::Solid}); + spectrum_->set<&Spectrum::Properties::max_pen>(Pen{color_from_hex(string_value(state_, "max_pen")), 1.2}); + spectrum_->set<&Spectrum::Properties::current_pen>(Pen{ color_from_hex(string_value(state_, "current_pen")), 2.0, Line_Style::Solid, Line_Cap::Round, Line_Join::Round }); - spectrum_->set_min_pen({color_from_hex(string_value(state_, "min_pen")), 1.2}); - spectrum_->set_selected_marker_pen({color_from_hex(string_value(state_, "selected_marker_pen")), 2.0}); - spectrum_->set_marker_pen({color_from_hex(string_value(state_, "marker_pen")), 1.2}); - spectrum_->set_middle_frequency_pen({ + spectrum_->set<&Spectrum::Properties::min_pen>(Pen{color_from_hex(string_value(state_, "min_pen")), 1.2}); + spectrum_->set<&Spectrum::Properties::selected_marker_pen>(Pen{color_from_hex(string_value(state_, "selected_marker_pen")), 2.0}); + spectrum_->set<&Spectrum::Properties::marker_pen>(Pen{color_from_hex(string_value(state_, "marker_pen")), 1.2}); + spectrum_->set<&Spectrum::Properties::middle_frequency_pen>(Pen{ color_from_hex(string_value(state_, "middle_frequency_pen")), 1.0, Line_Style::Dash }); - spectrum_->set_sweep_region_brush({ + spectrum_->set<&Spectrum::Properties::sweep_region_brush>(Brush{ color_from_hex(string_value(state_, "sweep_region_brush"), 80), Brush_Style::Solid }); - spectrum_->set_use_hover_info(bool_value(state_, "hover_enabled")); - spectrum_->set_hover_tooltip_font({number_value(state_, "hover_font_size"), 500, false}); - spectrum_->set_tooltip_text_pen({color_from_hex(string_value(state_, "hover_text_color"))}); - spectrum_->set_hover_tooltip_background_brush( - {color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); + spectrum_->set<&Spectrum::Properties::tooltip_enabled>(bool_value(state_, "hover_enabled")); + spectrum_->set<&Spectrum::Properties::tooltip_font>(Font{number_value(state_, "hover_font_size"), 500, false}); + spectrum_->set<&Spectrum::Properties::tooltip_text_pen>(Pen{color_from_hex(string_value(state_, "hover_text_color"))}); + spectrum_->set<&Spectrum::Properties::tooltip_background_brush>(Brush{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); } if (selection_) { - selection_->set_label_font({number_value(state_, "selection_font_size"), 500, false}); - selection_->set_label_pen({color_from_hex(string_value(state_, "selection_label_pen"))}); - selection_->set_selection_brush({ + selection_->set<&Selection_Rectangle_Overlay::Properties::label_font>(Font{number_value(state_, "selection_font_size"), 500, false}); + selection_->set<&Selection_Rectangle_Overlay::Properties::label_pen>(Pen{color_from_hex(string_value(state_, "selection_label_pen"))}); + selection_->set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{ color_from_hex(string_value(state_, "selection_brush"), 70), Brush_Style::Solid }); - selection_->set_selection_border_pen({ + selection_->set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{ color_from_hex(string_value(state_, "selection_border")), 1.0, Line_Style::Dash }); } if (waterfall_) { - waterfall_->set_frequency_range({ + waterfall_->set<&Waterfall::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); - waterfall_->set_power_range({ + waterfall_->set<&Waterfall::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }); - waterfall_->set_frequency_bin_count(integer_value(state_, "frequency_bin_count")); - waterfall_->set_visible_range_only(bool_value(state_, "visible_range_only")); - waterfall_->set_interpolation_mode(image_mode(string_value(state_, "image_interpolation"))); - waterfall_->set_use_hover_info(bool_value(state_, "hover_enabled")); - waterfall_->set_hover_tooltip_font({number_value(state_, "hover_font_size"), 500, false}); - waterfall_->set_tooltip_text_pen({color_from_hex(string_value(state_, "hover_text_color"))}); - waterfall_->set_hover_tooltip_background_brush( - {color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); + waterfall_->set<&Waterfall::Properties::frequency_bin_count>(integer_value(state_, "frequency_bin_count")); + waterfall_->set<&Waterfall::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); + waterfall_->set<&Waterfall::Properties::interpolation_mode>(image_mode(string_value(state_, "image_interpolation"))); + waterfall_->set<&Waterfall::Properties::tooltip_enabled>(bool_value(state_, "hover_enabled")); + waterfall_->set<&Waterfall::Properties::tooltip_font>(Font{number_value(state_, "hover_font_size"), 500, false}); + waterfall_->set<&Waterfall::Properties::tooltip_text_pen>(Pen{color_from_hex(string_value(state_, "hover_text_color"))}); + waterfall_->set<&Waterfall::Properties::tooltip_background_brush>(Brush{color_from_hex(string_value(state_, "hover_background"), 230), Brush_Style::Solid}); } if (afterglow_) { - afterglow_->set_frequency_range({ + afterglow_->set<&Afterglow::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); - afterglow_->set_power_range({ + afterglow_->set<&Afterglow::Properties::power_range>(Range{ number_value(state_, "power_origin"), number_value(state_, "power_target") }); - afterglow_->set_frequency_point_size(integer_value(state_, "frequency_point_size")); - afterglow_->set_power_point_size(integer_value(state_, "power_point_size")); - afterglow_->set_interpolate(bool_value(state_, "interpolate_power")); - afterglow_->set_attenuation_rate(number_value(state_, "attenuation_rate")); + afterglow_->set<&Afterglow::Properties::frequency_point_size>(integer_value(state_, "frequency_point_size")); + afterglow_->set<&Afterglow::Properties::power_point_size>(integer_value(state_, "power_point_size")); + afterglow_->set<&Afterglow::Properties::interpolate>(bool_value(state_, "interpolate_power")); + afterglow_->set<&Afterglow::Properties::attenuation_rate>(number_value(state_, "attenuation_rate")); } if (sweep_) { - sweep_->set_frequency_range({ + sweep_->set<&Sweep_Spectrum::Properties::frequency_range>(Range{ number_value(state_, "frequency_origin"), number_value(state_, "frequency_target") }); - sweep_->set_bins_per_block(integer_value(state_, "bins_per_block")); - sweep_->set_block_count(integer_value(state_, "block_count")); - sweep_->set_pen({color_from_hex(string_value(state_, "sweep_pen")), 1.8}); - sweep_->set_cur_frequency_pen({color_from_hex(string_value(state_, "current_frequency_pen")), 2.0}); - sweep_->set_visible_range_only(bool_value(state_, "visible_range_only")); - sweep_->set_interpolation_mode(line_mode(string_value(state_, "line_interpolation"))); + sweep_->set<&Sweep_Spectrum::Properties::bins_per_block>(integer_value(state_, "bins_per_block")); + sweep_->set<&Sweep_Spectrum::Properties::block_count>(integer_value(state_, "block_count")); + sweep_->set<&Sweep_Spectrum::Properties::pen>(Pen{color_from_hex(string_value(state_, "sweep_pen")), 1.8}); + sweep_->set<&Sweep_Spectrum::Properties::current_frequency_pen>(Pen{color_from_hex(string_value(state_, "current_frequency_pen")), 2.0}); + sweep_->set<&Sweep_Spectrum::Properties::visible_range_only>(bool_value(state_, "visible_range_only")); + sweep_->set<&Sweep_Spectrum::Properties::interpolation_mode>(line_mode(string_value(state_, "line_interpolation"))); } if (trace_ && value_axis_) value_axis_->set_coord_range({ @@ -1470,11 +1420,11 @@ private: number_value(state_, "trace_value_max") }); if (constellation_) { - constellation_->set_i_range({number_value(state_, "i_origin"), number_value(state_, "i_target")}); - constellation_->set_q_range({number_value(state_, "q_origin"), number_value(state_, "q_target")}); - constellation_->set_point_color(color_from_hex(string_value(state_, "point_color"))); - constellation_->set_anchor_color(color_from_hex(string_value(state_, "anchor_color"))); - constellation_->set_point_lifetime_ms(integer_value(state_, "point_lifetime_ms")); + constellation_->set<&Constellation_Diagram::Properties::i_range>(Range{number_value(state_, "i_origin"), number_value(state_, "i_target")}); + constellation_->set<&Constellation_Diagram::Properties::q_range>(Range{number_value(state_, "q_origin"), number_value(state_, "q_target")}); + constellation_->set<&Constellation_Diagram::Properties::point_color>(color_from_hex(string_value(state_, "point_color"))); + constellation_->set<&Constellation_Diagram::Properties::anchor_color>(color_from_hex(string_value(state_, "anchor_color"))); + constellation_->set<&Constellation_Diagram::Properties::point_lifetime_ms>(integer_value(state_, "point_lifetime_ms")); if (numeric_domain_axis_) numeric_domain_axis_->set_coord_range({ number_value(state_, "i_origin"), @@ -1505,18 +1455,18 @@ private: return values; } void push_spectrum_samples() { - spectrum_->update_samples(spectrum_values(spectrum_->frequency_point_size())); + spectrum_->update_samples(spectrum_values(spectrum_->get<&Spectrum::Properties::frequency_point_size>())); } void push_waterfall_row() { waterfall_->append_row(current_time_of_day(), - spectrum_values(waterfall_->frequency_bin_count(), waterfall_->power_range())); + spectrum_values(waterfall_->get<&Waterfall::Properties::frequency_bin_count>(), waterfall_->get<&Waterfall::Properties::power_range>())); } void push_afterglow_spectrum() { - afterglow_->append_spectrum(spectrum_values(afterglow_->frequency_point_size(), - afterglow_->power_range())); + afterglow_->append_spectrum(spectrum_values(afterglow_->get<&Afterglow::Properties::frequency_point_size>(), + afterglow_->get<&Afterglow::Properties::power_range>())); } void push_sweep_block() { - const int count = sweep_->bins_per_block(); + const int count = sweep_->get<&Sweep_Spectrum::Properties::bins_per_block>(); std::vector block(static_cast(std::max(count, 1))); for (int index = 0; index < count; ++index) { const double x = static_cast(index) / std::max(1, count - 1); diff --git a/web_server/app/Gallery_Properties_Adminive.h b/web_server/app/Gallery_Properties_Adminive.h index 92ad1f4..0d04278 100644 --- a/web_server/app/Gallery_Properties_Adminive.h +++ b/web_server/app/Gallery_Properties_Adminive.h @@ -172,38 +172,38 @@ inline auto time_axis_fields() { inline auto hover_fields() { using T = Gallery_Hover_Properties; return std::tuple{ - boolean_property<&T::hover_enabled>("hover_enabled", "悬浮信息", "Hover_Tooltip_Mixin::set_use_hover_info"), - number_property<&T::hover_font_size>("hover_font_size", "悬浮字体", "Hover_Tooltip_Mixin::set_hover_tooltip_font"), - color_property<&T::hover_text_color>("hover_text_color", "悬浮文字", "Hover_Tooltip_Mixin::set_tooltip_text_pen"), - color_property<&T::hover_background>("hover_background", "悬浮背景", "Hover_Tooltip_Mixin::set_hover_tooltip_background_brush") + boolean_property<&T::hover_enabled>("hover_enabled", "悬浮信息", "Plottable_State::set<&Properties::tooltip_enabled>"), + number_property<&T::hover_font_size>("hover_font_size", "悬浮字体", "Plottable_State::set<&Properties::tooltip_font>"), + color_property<&T::hover_text_color>("hover_text_color", "悬浮文字", "Plottable_State::set<&Properties::tooltip_text_pen>"), + color_property<&T::hover_background>("hover_background", "悬浮背景", "Plottable_State::set<&Properties::tooltip_background_brush>") }; } inline auto spectrum_fields() { using T = Gallery_Spectrum_Properties; return std::tuple{ - number_property<&T::frequency_point_size>("frequency_point_size", "频率点数", "Spectrum::set_frequency_point_size"), - number_property<&T::frequency_origin>("frequency_origin", "数据频率起点", "Spectrum::set_frequency_range"), - number_property<&T::frequency_target>("frequency_target", "数据频率终点", "Spectrum::set_frequency_range"), - number_property<&T::center_frequency>("center_frequency", "中心频率", "Spectrum::set_center_frequency"), - number_property<&T::sweep_origin>("sweep_origin", "扫频区起点", "Spectrum::set_sweep_frequency_range"), - number_property<&T::sweep_target>("sweep_target", "扫频区终点", "Spectrum::set_sweep_frequency_range"), - boolean_property<&T::max_hold_visible>("max_hold_visible", "最大保持线", "Spectrum::set_max_hold_visible"), - boolean_property<&T::min_hold_visible>("min_hold_visible", "最小保持线", "Spectrum::set_min_hold_visible"), - boolean_property<&T::max_marker_visible>("max_marker_visible", "峰值 Marker", "Spectrum::set_max_marker_visible"), - boolean_property<&T::use_min_marker>("use_min_marker", "最小值 Marker", "Spectrum::set_use_min_marker"), - boolean_property<&T::sweep_region_visible>("sweep_region_visible", "扫频区域", "Spectrum::set_sweep_region_visible"), - boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Spectrum::set_visible_range_only"), - select_property<&T::line_interpolation>("line_interpolation", "线插值模式", "Spectrum::set_interpolation_mode"), - color_property<&T::max_brush>("max_brush", "最大保持填充", "Spectrum::set_max_brush"), - color_property<&T::current_brush>("current_brush", "当前曲线填充", "Spectrum::set_current_brush"), - color_property<&T::min_brush>("min_brush", "最小保持填充", "Spectrum::set_min_brush"), - color_property<&T::max_pen>("max_pen", "最大保持线颜色", "Spectrum::set_max_pen"), - color_property<&T::current_pen>("current_pen", "当前曲线颜色", "Spectrum::set_current_pen"), - color_property<&T::min_pen>("min_pen", "最小保持线颜色", "Spectrum::set_min_pen"), - color_property<&T::selected_marker_pen>("selected_marker_pen", "选中 Marker", "Spectrum::set_selected_marker_pen"), - color_property<&T::marker_pen>("marker_pen", "Marker 颜色", "Spectrum::set_marker_pen"), - color_property<&T::middle_frequency_pen>("middle_frequency_pen", "中心频率线", "Spectrum::set_middle_frequency_pen"), - color_property<&T::sweep_region_brush>("sweep_region_brush", "扫频区填充", "Spectrum::set_sweep_region_brush") + number_property<&T::frequency_point_size>("frequency_point_size", "频率点数", "Spectrum::set<&Properties::frequency_point_size>"), + number_property<&T::frequency_origin>("frequency_origin", "数据频率起点", "Spectrum::set<&Properties::frequency_range>"), + number_property<&T::frequency_target>("frequency_target", "数据频率终点", "Spectrum::set<&Properties::frequency_range>"), + number_property<&T::center_frequency>("center_frequency", "中心频率", "Spectrum::set<&Properties::center_frequency>"), + number_property<&T::sweep_origin>("sweep_origin", "扫频区起点", "Spectrum::set<&Properties::sweep_frequency_range>"), + number_property<&T::sweep_target>("sweep_target", "扫频区终点", "Spectrum::set<&Properties::sweep_frequency_range>"), + boolean_property<&T::max_hold_visible>("max_hold_visible", "最大保持线", "Spectrum::set<&Properties::max_hold_visible>"), + boolean_property<&T::min_hold_visible>("min_hold_visible", "最小保持线", "Spectrum::set<&Properties::min_hold_visible>"), + boolean_property<&T::max_marker_visible>("max_marker_visible", "峰值 Marker", "Spectrum::set<&Properties::max_marker_visible>"), + boolean_property<&T::use_min_marker>("use_min_marker", "最小值 Marker", "Spectrum::set<&Properties::use_min_marker>"), + boolean_property<&T::sweep_region_visible>("sweep_region_visible", "扫频区域", "Spectrum::set<&Properties::sweep_region_visible>"), + boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Spectrum::set<&Properties::visible_range_only>"), + select_property<&T::line_interpolation>("line_interpolation", "线插值模式", "Spectrum::set<&Properties::interpolation_mode>"), + color_property<&T::max_brush>("max_brush", "最大保持填充", "Spectrum::set<&Properties::max_brush>"), + color_property<&T::current_brush>("current_brush", "当前曲线填充", "Spectrum::set<&Properties::current_brush>"), + color_property<&T::min_brush>("min_brush", "最小保持填充", "Spectrum::set<&Properties::min_brush>"), + color_property<&T::max_pen>("max_pen", "最大保持线颜色", "Spectrum::set<&Properties::max_pen>"), + color_property<&T::current_pen>("current_pen", "当前曲线颜色", "Spectrum::set<&Properties::current_pen>"), + color_property<&T::min_pen>("min_pen", "最小保持线颜色", "Spectrum::set<&Properties::min_pen>"), + color_property<&T::selected_marker_pen>("selected_marker_pen", "选中 Marker", "Spectrum::set<&Properties::selected_marker_pen>"), + color_property<&T::marker_pen>("marker_pen", "Marker 颜色", "Spectrum::set<&Properties::marker_pen>"), + color_property<&T::middle_frequency_pen>("middle_frequency_pen", "中心频率线", "Spectrum::set<&Properties::middle_frequency_pen>"), + color_property<&T::sweep_region_brush>("sweep_region_brush", "扫频区填充", "Spectrum::set<&Properties::sweep_region_brush>") }; } template @@ -225,10 +225,10 @@ struct Property_Fields { static auto get() { using T = Gallery_Selection_Overlay_Properties; return std::tuple_cat(Property_Fields::get(), std::tuple{ - number_property<&T::selection_font_size>("selection_font_size", "框选标签字体", "Selection_Rectangle_Overlay::set_label_font"), - color_property<&T::selection_label_pen>("selection_label_pen", "框选标签颜色", "Selection_Rectangle_Overlay::set_label_pen"), - color_property<&T::selection_brush>("selection_brush", "框选填充", "Selection_Rectangle_Overlay::set_selection_brush"), - color_property<&T::selection_border>("selection_border", "框选边框", "Selection_Rectangle_Overlay::set_selection_border_pen") + number_property<&T::selection_font_size>("selection_font_size", "框选标签字体", "Selection_Rectangle_Overlay::set<&Properties::label_font>"), + color_property<&T::selection_label_pen>("selection_label_pen", "框选标签颜色", "Selection_Rectangle_Overlay::set<&Properties::label_pen>"), + color_property<&T::selection_brush>("selection_brush", "框选填充", "Selection_Rectangle_Overlay::set<&Properties::selection_brush>"), + color_property<&T::selection_border>("selection_border", "框选边框", "Selection_Rectangle_Overlay::set<&Properties::selection_border_pen>") }); } }; @@ -237,14 +237,14 @@ struct Property_Fields { static auto get() { using T = Gallery_Waterfall_Properties; return std::tuple_cat(common_fields(), axis_fields(), time_axis_fields(), std::tuple{ - number_property<&T::frequency_origin>("frequency_origin", "频率起点", "Waterfall::set_frequency_range"), - number_property<&T::frequency_target>("frequency_target", "频率终点", "Waterfall::set_frequency_range"), - number_property<&T::power_origin>("power_origin", "功率起点", "Waterfall::set_power_range"), - number_property<&T::power_target>("power_target", "功率终点", "Waterfall::set_power_range"), - number_property<&T::frequency_bin_count>("frequency_bin_count", "频率 Bin", "Waterfall::set_frequency_bin_count"), - boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Waterfall::set_visible_range_only"), - select_property<&T::image_interpolation>("image_interpolation", "图像插值(三模式)", "Waterfall::set_interpolation_mode"), - select_property<&T::color_map>("color_map", "颜色映射(重建 Builder)", "Waterfall::Builder::set_color_map / Color_Map::set_colors") + number_property<&T::frequency_origin>("frequency_origin", "频率起点", "Waterfall::set<&Properties::frequency_range>"), + number_property<&T::frequency_target>("frequency_target", "频率终点", "Waterfall::set<&Properties::frequency_range>"), + number_property<&T::power_origin>("power_origin", "功率起点", "Waterfall::set<&Properties::power_range>"), + number_property<&T::power_target>("power_target", "功率终点", "Waterfall::set<&Properties::power_range>"), + number_property<&T::frequency_bin_count>("frequency_bin_count", "频率 Bin", "Waterfall::set<&Properties::frequency_bin_count>"), + boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Waterfall::set<&Properties::visible_range_only>"), + select_property<&T::image_interpolation>("image_interpolation", "图像插值(三模式)", "Waterfall::set<&Properties::interpolation_mode>"), + select_property<&T::color_map>("color_map", "颜色映射", "Waterfall::set<&Properties::color_map>") }, hover_fields()); } }; @@ -253,15 +253,15 @@ struct Property_Fields { static auto get() { using T = Gallery_Afterglow_Properties; return std::tuple_cat(common_fields(), axis_fields(), std::tuple{ - number_property<&T::frequency_origin>("frequency_origin", "频率起点", "Afterglow::set_frequency_range"), - number_property<&T::frequency_target>("frequency_target", "频率终点", "Afterglow::set_frequency_range"), - number_property<&T::power_origin>("power_origin", "功率起点", "Afterglow::set_power_range"), - number_property<&T::power_target>("power_target", "功率终点", "Afterglow::set_power_range"), - number_property<&T::frequency_point_size>("frequency_point_size", "频率格点", "Afterglow::set_frequency_point_size"), - number_property<&T::power_point_size>("power_point_size", "功率格点", "Afterglow::set_power_point_size"), - boolean_property<&T::interpolate_power>("interpolate_power", "功率 Bin 插值", "Afterglow::set_interpolate"), - number_property<&T::attenuation_rate>("attenuation_rate", "衰减率", "Afterglow::set_attenuation_rate"), - select_property<&T::color_map>("color_map", "颜色映射(重建 Builder)", "Afterglow::Builder::set_color_map / Color_Map::set_colors") + number_property<&T::frequency_origin>("frequency_origin", "频率起点", "Afterglow::set<&Properties::frequency_range>"), + number_property<&T::frequency_target>("frequency_target", "频率终点", "Afterglow::set<&Properties::frequency_range>"), + number_property<&T::power_origin>("power_origin", "功率起点", "Afterglow::set<&Properties::power_range>"), + number_property<&T::power_target>("power_target", "功率终点", "Afterglow::set<&Properties::power_range>"), + number_property<&T::frequency_point_size>("frequency_point_size", "频率格点", "Afterglow::set<&Properties::frequency_point_size>"), + number_property<&T::power_point_size>("power_point_size", "功率格点", "Afterglow::set<&Properties::power_point_size>"), + boolean_property<&T::interpolate_power>("interpolate_power", "功率 Bin 插值", "Afterglow::set<&Properties::interpolate>"), + number_property<&T::attenuation_rate>("attenuation_rate", "衰减率", "Afterglow::set<&Properties::attenuation_rate>"), + select_property<&T::color_map>("color_map", "颜色映射", "Afterglow::set<&Properties::color_map>") }); } }; @@ -270,14 +270,14 @@ struct Property_Fields { static auto get() { using T = Gallery_Sweep_Spectrum_Properties; return std::tuple_cat(common_fields(), axis_fields(), std::tuple{ - number_property<&T::frequency_origin>("frequency_origin", "扫频起点", "Sweep_Spectrum::set_frequency_range"), - number_property<&T::frequency_target>("frequency_target", "扫频终点", "Sweep_Spectrum::set_frequency_range"), - number_property<&T::bins_per_block>("bins_per_block", "每块 Bin", "Sweep_Spectrum::set_bins_per_block"), - number_property<&T::block_count>("block_count", "块数量", "Sweep_Spectrum::set_block_count"), - color_property<&T::sweep_pen>("sweep_pen", "扫频曲线", "Sweep_Spectrum::set_pen"), - color_property<&T::current_frequency_pen>("current_frequency_pen", "当前频率游标", "Sweep_Spectrum::set_cur_frequency_pen"), - boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Sweep_Spectrum::set_visible_range_only"), - select_property<&T::line_interpolation>("line_interpolation", "线插值模式", "Sweep_Spectrum::set_interpolation_mode") + number_property<&T::frequency_origin>("frequency_origin", "扫频起点", "Sweep_Spectrum::set<&Properties::frequency_range>"), + number_property<&T::frequency_target>("frequency_target", "扫频终点", "Sweep_Spectrum::set<&Properties::frequency_range>"), + number_property<&T::bins_per_block>("bins_per_block", "每块 Bin", "Sweep_Spectrum::set<&Properties::bins_per_block>"), + number_property<&T::block_count>("block_count", "块数量", "Sweep_Spectrum::set<&Properties::block_count>"), + color_property<&T::sweep_pen>("sweep_pen", "扫频曲线", "Sweep_Spectrum::set<&Properties::pen>"), + color_property<&T::current_frequency_pen>("current_frequency_pen", "当前频率游标", "Sweep_Spectrum::set<&Properties::current_frequency_pen>"), + boolean_property<&T::visible_range_only>("visible_range_only", "仅绘制可见范围", "Sweep_Spectrum::set<&Properties::visible_range_only>"), + select_property<&T::line_interpolation>("line_interpolation", "线插值模式", "Sweep_Spectrum::set<&Properties::interpolation_mode>") }); } }; @@ -286,8 +286,8 @@ struct Property_Fields { static auto get() { using T = Gallery_Frequency_Trace_Properties; return std::tuple_cat(common_fields(), axis_fields(), time_axis_fields(), std::tuple{ - color_property<&T::trace_pen>("trace_pen", "轨迹颜色(重建 Builder)", "Frequency_Trace::Builder::set_pen"), - number_property<&T::trace_pen_width>("trace_pen_width", "轨迹宽度(重建 Builder)", "Frequency_Trace::Builder::set_pen"), + color_property<&T::trace_pen>("trace_pen", "轨迹颜色", "Frequency_Trace::set<&Properties::pen>"), + number_property<&T::trace_pen_width>("trace_pen_width", "轨迹宽度", "Frequency_Trace::set<&Properties::pen>"), number_property<&T::trace_value_min>("trace_value_min", "数值下限", "Axis::set_coord_range"), number_property<&T::trace_value_max>("trace_value_max", "数值上限", "Axis::set_coord_range") }); @@ -298,15 +298,15 @@ struct Property_Fields { static auto get() { using T = Gallery_Constellation_Properties; return std::tuple_cat(common_fields(), axis_fields(), std::tuple{ - number_property<&T::i_origin>("i_origin", "I 起点", "Constellation_Diagram::set_i_range"), - number_property<&T::i_target>("i_target", "I 终点", "Constellation_Diagram::set_i_range"), - number_property<&T::q_origin>("q_origin", "Q 起点", "Constellation_Diagram::set_q_range"), - number_property<&T::q_target>("q_target", "Q 终点", "Constellation_Diagram::set_q_range"), - color_property<&T::point_color>("point_color", "采样点颜色", "Constellation_Diagram::set_point_color"), - color_property<&T::anchor_color>("anchor_color", "锚点颜色", "Constellation_Diagram::set_anchor_color"), - number_property<&T::point_lifetime_ms>("point_lifetime_ms", "采样点寿命", "Constellation_Diagram::set_point_lifetime_ms"), - select_property<&T::constellation_type>("constellation_type", "星座模式(重建 Builder)", "Constellation_Diagram::Builder::set_type"), - number_property<&T::phase_offset>("phase_offset", "相位偏移(重建 Builder)", "Constellation_Diagram::Builder::set_phase_offset_radians") + number_property<&T::i_origin>("i_origin", "I 起点", "Constellation_Diagram::set<&Properties::i_range>"), + number_property<&T::i_target>("i_target", "I 终点", "Constellation_Diagram::set<&Properties::i_range>"), + number_property<&T::q_origin>("q_origin", "Q 起点", "Constellation_Diagram::set<&Properties::q_range>"), + number_property<&T::q_target>("q_target", "Q 终点", "Constellation_Diagram::set<&Properties::q_range>"), + color_property<&T::point_color>("point_color", "采样点颜色", "Constellation_Diagram::set<&Properties::point_color>"), + color_property<&T::anchor_color>("anchor_color", "锚点颜色", "Constellation_Diagram::set<&Properties::anchor_color>"), + number_property<&T::point_lifetime_ms>("point_lifetime_ms", "采样点寿命", "Constellation_Diagram::set<&Properties::point_lifetime_ms>"), + select_property<&T::constellation_type>("constellation_type", "星座模式", "Constellation_Diagram::set<&Properties::type>"), + number_property<&T::phase_offset>("phase_offset", "相位偏移", "Constellation_Diagram::set<&Properties::phase_offset_radians>") }); } }; diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 841147c..56cb3fa 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -142,34 +142,27 @@ std::vector actions(std::string_view case_id, result.push_back(action("set_marker_frequency", "修改选中 Marker 频率", "Spectrum::set_marker_frequency / set_current_marker_frequency", "Marker", {}, "number", "频率", 99e6)); - result.push_back(action("rebind_axes", "重新绑定频率轴和功率轴", "Spectrum::set_frequency_axis / set_power_axis", "Spectrum")); result.push_back(action("read_data_shape", "读取输入与绘制点数", "Spectrum::sample_count / rendered_point_count", "观察者")); if (case_id == "selection_overlay") { result.push_back(action("clear_selection", "清空框选区域", "Selection_Rectangle_Overlay::clear_selected_regions", "Selection Overlay")); - result.push_back(action("rebind_selection_axes", "重新绑定框选轴", "Selection_Rectangle_Overlay::set_horizontal_axis / set_vertical_axis", "Selection Overlay")); } } else if (case_id == "waterfall") { result.push_back(action("append_row", "追加一行", "Waterfall::append_row", "Waterfall")); result.push_back(action("append_tick_row", "按 tick 追加一行", "Time_Axis::append_time / Waterfall::append_row(int, span)", "Waterfall")); - result.push_back(action("rebind_axes", "读取并重新绑定轴", "Waterfall::frequency_axis / time_axis", "Waterfall")); result.push_back(action("read_data_shape", "读取行、点与渲染单元", "Waterfall::row_count / stored_point_count / rendered_cell_count", "观察者")); } else if (case_id == "afterglow") { result.push_back(action("append_spectrum", "追加一帧频谱", "Afterglow::append_spectrum", "Afterglow")); - result.push_back(action("rebind_axes", "读取轴绑定", "Afterglow::frequency_axis / power_axis", "Afterglow")); result.push_back(action("read_data_shape", "读取历史帧与渲染网格", "Afterglow::history_count / latest_spectrum_point_count / rendered_cell_count", "观察者")); } else if (case_id == "sweep_spectrum") { result.push_back(action("append_block", "追加一个扫频块", "Sweep_Spectrum::append_block", "Sweep Spectrum")); - result.push_back(action("rebind_axes", "读取轴绑定", "Sweep_Spectrum::frequency_axis / power_axis", "Sweep Spectrum")); result.push_back(action("read_data_shape", "读取块与绘制点数", "Sweep_Spectrum::stored_block_count / stored_point_count / rendered_point_count", "观察者")); } else if (case_id == "frequency_trace") { result.push_back(action("append_sample", "追加一个样本", "Frequency_Trace::append_sample", "Frequency Trace")); result.push_back(action("append_tick_sample", "按 tick 追加样本", "Time_Axis::append_time / Frequency_Trace::append_sample(int, double)", "Frequency Trace")); - result.push_back(action("read_axes", "读取时间轴与值轴", "Frequency_Trace::time_axis / value_axis", "Frequency Trace")); result.push_back(action("read_data_shape", "读取轨迹点数", "Frequency_Trace::sample_count / rendered_point_count / Time_Axis::time_point_count", "观察者")); } else if (case_id == "constellation") { result.push_back(action("append_points", "追加一组 IQ 点", "Constellation_Diagram::append_point", "Constellation")); result.push_back(action("fit_square", "按轴拟合正方形", "Constellation_Diagram::fit_square_to_axes", "Constellation")); - result.push_back(action("read_axes", "读取 I/Q 轴", "Constellation_Diagram::i_axis / q_axis", "Constellation")); result.push_back(action("read_data_shape", "读取星座点与锚点数", "Constellation_Diagram::point_count", "观察者")); } return result; diff --git a/web_server/app/Web_Plot_Session.cpp b/web_server/app/Web_Plot_Session.cpp index c6852fb..697a8b6 100644 --- a/web_server/app/Web_Plot_Session.cpp +++ b/web_server/app/Web_Plot_Session.cpp @@ -111,37 +111,36 @@ struct Web_Plot_Session::Impl { .set_sub_tick_length(-4) .set_color(axis_color) .build(); - spectrum = Spectrum::Builder(data, spectrum_frequency_axis, spectrum_power_axis) - .set_frequency_range(initial_frequency) - .set_frequency_point_size(768) - .set_center_frequency(102'500'000.0) - .set_sweep_frequency_range({102'200'000.0, 102'800'000.0}) - .set_max_marker_visible(true) - .set_sweep_region_visible(true) - .set_interpolation_mode(Line_Interpolation_Mode::Cubic_Value) - .build(); - spectrum->set_current_pen({ + spectrum = Spectrum::Builder{} + .set<&Spectrum::Properties::frequency_range>(initial_frequency) + .set<&Spectrum::Properties::frequency_point_size>(768) + .set<&Spectrum::Properties::center_frequency>(102'500'000.0) + .set<&Spectrum::Properties::sweep_frequency_range>(Range{102'200'000.0, 102'800'000.0}) + .set<&Spectrum::Properties::max_marker_visible>(true) + .set<&Spectrum::Properties::sweep_region_visible>(true) + .set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value) + .build(data, spectrum_frequency_axis, spectrum_power_axis); + spectrum->set<&Spectrum::Properties::current_pen>(Pen{ Color{57, 224, 177, 255}, 2.0, Line_Style::Solid, Line_Cap::Round, Line_Join::Round }); - spectrum->set_current_brush({Color{31, 174, 145, 32}, Brush_Style::Solid}); - spectrum->set_max_pen({Color{250, 204, 21, 210}, 1.0}); - spectrum->set_middle_frequency_pen({ + spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{31, 174, 145, 32}, Brush_Style::Solid}); + spectrum->set<&Spectrum::Properties::max_pen>(Pen{Color{250, 204, 21, 210}, 1.0}); + spectrum->set<&Spectrum::Properties::middle_frequency_pen>(Pen{ Color{56, 189, 248, 220}, 1.0, Line_Style::Dash }); - waterfall = Waterfall::Builder(data, waterfall_frequency_axis, waterfall_time_axis) - .set_frequency_range(initial_frequency) - .set_power_range({-120.0, -20.0}) - .set_frequency_bin_count(768) - .set_interpolation_mode(Image_Interpolation_Mode::Bilinear) - .set_color_map(radio_color_map()) - .build(); - selection = Selection_Rectangle_Overlay::Builder( - overlay, spectrum_frequency_axis, spectrum_power_axis) - .set_rect_brush({Color{56, 189, 248, 36}, Brush_Style::Solid}) - .set_border_pen({Color{125, 211, 252, 230}, 1.0, Line_Style::Dash}) - .build(); + waterfall = Waterfall::Builder{} + .set<&Waterfall::Properties::frequency_range>(initial_frequency) + .set<&Waterfall::Properties::power_range>(Range{-120.0, -20.0}) + .set<&Waterfall::Properties::frequency_bin_count>(768) + .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bilinear) + .set<&Waterfall::Properties::color_map>(radio_color_map()) + .build(data, waterfall_frequency_axis, waterfall_time_axis); + selection = Selection_Rectangle_Overlay::Builder{} + .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{56, 189, 248, 36}, Brush_Style::Solid}) + .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{125, 211, 252, 230}, 1.0, Line_Style::Dash}) + .build(overlay, spectrum_frequency_axis, spectrum_power_axis); apply_layout(plot.viewport_size()); plot.activate_view(); update_model(); @@ -219,26 +218,26 @@ struct Web_Plot_Session::Impl { const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; spectrum_frequency_axis->set_coord_range(range); waterfall_frequency_axis->set_coord_range(range); - spectrum->set_frequency_range(range); - spectrum->set_center_frequency(center); - spectrum->set_sweep_frequency_range({ + spectrum->set<&Spectrum::Properties::frequency_range>(range); + spectrum->set<&Spectrum::Properties::center_frequency>(center); + spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ center - bandwidth * 0.125, center + bandwidth * 0.125 }); - waterfall->set_frequency_range(range); + waterfall->set<&Waterfall::Properties::frequency_range>(range); } void set_bandwidth(double kilohertz) { - const double center = spectrum->center_frequency(); + const double center = spectrum->get<&Spectrum::Properties::center_frequency>(); const double bandwidth = kilohertz * 1000.0; const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; spectrum_frequency_axis->set_coord_range(range); waterfall_frequency_axis->set_coord_range(range); - spectrum->set_frequency_range(range); - spectrum->set_sweep_frequency_range({ + spectrum->set<&Spectrum::Properties::frequency_range>(range); + spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ center - bandwidth * 0.125, center + bandwidth * 0.125 }); - waterfall->set_frequency_range(range); + waterfall->set<&Waterfall::Properties::frequency_range>(range); } std::optional render_pixels() { if (!plot.view_active()) @@ -297,14 +296,14 @@ struct Web_Plot_Session::Impl { plot.notify_model_dirty(); } else if constexpr (std::is_same_v) { - spectrum->set_max_hold_visible(value.enabled); + spectrum->set<&Spectrum::Properties::max_hold_visible>(value.enabled); } else if constexpr (std::is_same_v) { - spectrum->set_interpolation_mode( + spectrum->set<&Spectrum::Properties::interpolation_mode>( value.enabled ? Line_Interpolation_Mode::Cubic_Value : Line_Interpolation_Mode::Nearest_Sample); - waterfall->set_interpolation_mode( + waterfall->set<&Waterfall::Properties::interpolation_mode>( value.enabled ? Image_Interpolation_Mode::Bilinear : Image_Interpolation_Mode::Nearest); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index c25db10..7b4cbc8 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -265,7 +265,7 @@ TEST(RenderiveWebGallery, BackendMenuMapsRetainedControlApis) { api_text += action.at("api").get() + '\n'; } } - constexpr std::array required{ + constexpr std::array required{ "Plot_Core::set_background_color", "Plot_Core::set_max_render_fps", "Plot_Core::activate_view", "Plot_Core::remove_renderable", "Renderable::set_visible", "Renderable::set_cache_mode", "Renderable::set_object_name", @@ -273,16 +273,16 @@ TEST(RenderiveWebGallery, BackendMenuMapsRetainedControlApis) { "Abs_Axis::set_pixel_length", "Abs_Axis::set_locale", "Abs_Axis::set_unit_text_font", "Axis::set_coord_range", "Axis::set_coord_start", "Axis::set_coord_length", "Axis::set_use_wheel", "Axis::set_use_drag", "Time_Axis::set_time_format", - "Time_Axis::append_time", "Spectrum::set_frequency_axis", "Spectrum::set_frequency_point_size", - "Spectrum::set_interpolation_mode", "Spectrum::set_current_pen", "Spectrum::set_max_brush", + "Time_Axis::append_time", "Spectrum::set<&Properties::frequency_point_size>", + "Spectrum::set<&Properties::interpolation_mode>", "Spectrum::set<&Properties::current_pen>", "Spectrum::set<&Properties::max_brush>", "Spectrum::update_samples", "Spectrum::power_at", "Spectrum::add_custom_marker", - "Spectrum::set_marker_frequency", "Waterfall::set_frequency_range", - "Waterfall::set_interpolation_mode", "Waterfall::append_row", "Color_Map::set_colors", - "Afterglow::set_attenuation_rate", "Afterglow::append_spectrum", - "Sweep_Spectrum::set_bins_per_block", "Sweep_Spectrum::append_block", - "Frequency_Trace::Builder::set_pen", "Selection_Rectangle_Overlay::set_selection_brush", + "Spectrum::set_marker_frequency", "Waterfall::set<&Properties::frequency_range>", + "Waterfall::set<&Properties::interpolation_mode>", "Waterfall::append_row", "Waterfall::set<&Properties::color_map>", + "Afterglow::set<&Properties::attenuation_rate>", "Afterglow::append_spectrum", + "Sweep_Spectrum::set<&Properties::bins_per_block>", "Sweep_Spectrum::append_block", + "Frequency_Trace::set<&Properties::pen>", "Selection_Rectangle_Overlay::set<&Properties::selection_brush>", "Selection_Rectangle_Overlay::clear_selected_regions", - "Constellation_Diagram::Builder::set_type", "Constellation_Diagram::fit_square_to_axes" + "Constellation_Diagram::set<&Properties::type>", "Constellation_Diagram::fit_square_to_axes" }; for (const auto api : required) EXPECT_NE(api_text.find(api), std::string::npos) << api; @@ -595,9 +595,6 @@ TEST(RenderiveWebGallery, CanvasSpecificActionsProduceObservableStateChanges) { telemetry = invoke_action(session, "append_tick_row").at("telemetry"); EXPECT_EQ(telemetry.at("waterfall").at("row_count").get(), before_rows + 2); - EXPECT_NE(invoke_action(session, "rebind_axes").at("telemetry") - .at("last_action_result").get().find("valid"), - std::string::npos); } { Gallery_Plot_Session session; @@ -641,9 +638,6 @@ TEST(RenderiveWebGallery, CanvasSpecificActionsProduceObservableStateChanges) { const auto telemetry = invoke_action(session, "append_points").at("telemetry"); EXPECT_EQ(telemetry.at("constellation").at("point_count").get(), before + 24); - EXPECT_NE(invoke_action(session, "read_axes").at("telemetry") - .at("last_action_result").get().find("valid"), - std::string::npos); } } @@ -712,9 +706,6 @@ TEST(RenderiveWebGallery, SelectionOverlayPointerLifecycleCreatesAndClearsRegion EXPECT_EQ(observe_telemetry(session).at("selection_regions"), 1); EXPECT_EQ(invoke_action(session, "clear_selection").at("telemetry") .at("selection_regions"), 0); - const auto rebound = invoke_action(session, "rebind_selection_axes").at("telemetry"); - EXPECT_NE(rebound.at("last_action_result").get().find("Selection Overlay"), - std::string::npos); } TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRendersPixels) {