diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp index fbe0e71..f1fe96a 100644 --- a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -26,7 +27,9 @@ public: History_Real_Time_Data(const History_Real_Time_Data&) = delete; History_Real_Time_Data& operator=(const History_Real_Time_Data&) = delete; void update(Value value); + void update(Value value, std::size_t retain_latest_count); void clear(); + std::size_t retain_latest(std::size_t count); Container snapshot() const; std::size_t size() const; std::uint64_t revision() const; diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl index fdcbcc7..972521b 100644 --- a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl @@ -39,6 +39,10 @@ void History_Real_Time_Data::reserve_upd } template void History_Real_Time_Data::update(Value value) { + update(std::move(value), std::numeric_limits::max()); +} +template +void History_Real_Time_Data::update(Value value, std::size_t retain_latest_count) { std::lock_guard mutation_lock(mutation_mutex_); Real_Time_Data_Observation observation; { @@ -47,6 +51,12 @@ void History_Real_Time_Data::update(Valu values_.push_back(std::move(value)); const std::uint64_t update_time_ns = observer_.now_ns(); update_times_[update_times_size_++] = update_time_ns; + if(values_.size() > retain_latest_count) { + const std::size_t discarded = values_.size() - retain_latest_count; + values_.erase(values_.begin(), std::next(values_.begin(), static_cast(discarded))); + std::move(update_times_ + discarded, update_times_ + update_times_size_, update_times_); + update_times_size_ -= discarded; + } ++revision_; ++total_update_count_; last_update_time_ns_ = update_time_ns; @@ -69,6 +79,25 @@ void History_Real_Time_Data::clear() { observer_.observe(observation); } template +std::size_t History_Real_Time_Data::retain_latest(std::size_t count) { + std::lock_guard mutation_lock(mutation_mutex_); + Real_Time_Data_Observation observation; + std::size_t discarded{}; + { + std::lock_guard lock(mutex_); + if(values_.size() <= count) + return 0; + discarded = values_.size() - count; + values_.erase(values_.begin(), std::next(values_.begin(), static_cast(discarded))); + std::move(update_times_ + discarded, update_times_ + update_times_size_, update_times_); + update_times_size_ -= discarded; + ++revision_; + observation = {Real_Time_Data_Observation_Event::discarded, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}}; + } + observer_.observe(observation); + return discarded; +} +template auto History_Real_Time_Data::snapshot() const -> Container { std::lock_guard lock(mutex_); return values_; diff --git a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp index 18041a1..f6364f3 100644 --- a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp +++ b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp @@ -45,6 +45,18 @@ TEST(history_real_time_data_test, retains_all_values_until_explicit_discard) { EXPECT_EQ(data.discard_before_time_ns(15), 2); EXPECT_EQ(data.snapshot(), (std::vector{3})); } +TEST(history_real_time_data_test, retains_latest_requested_value_count) { + History_Real_Time_Data data; + data.update(1); + data.update(2, 2); + data.update(3, 2); + EXPECT_EQ(data.snapshot(), (std::vector{2, 3})); + EXPECT_EQ(data.revision(), 3); + EXPECT_EQ(data.retain_latest(2), 0); + data.update(4); + EXPECT_EQ(data.retain_latest(2), 1); + EXPECT_EQ(data.snapshot(), (std::vector{3, 4})); +} TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy) { Scene2D_Context<> scene; auto latest = std::make_shared(); diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index 9304398..de34771 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -1,49 +1,44 @@ #include "Afterglow.h" #include "Heatmap_Utils.h" +#include "Plottable_Real_Time_Data.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; +using Afterglow_History = Plottable_History_Real_Time_Data, std::deque>>; } struct Afterglow_Control::Impl { - Impl(std::shared_ptr frequency, std::shared_ptr power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {} + Impl(Afterglow_Control& owner, std::shared_ptr frequency, std::shared_ptr power) + : frequency_axis(std::move(frequency)), power_axis(std::move(power)), history(observe_real_time_data(owner)) {} std::shared_ptr frequency_axis; std::shared_ptr power_axis; - Afterglow_Runtime_State runtime; + Afterglow_History history; }; 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))) {} + : Plottable_State(plot, properties), impl_(std::make_unique(*this, 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(); }); + return impl_->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(); }); + const auto history = impl_->history.snapshot(); + return history.empty() ? 0 : 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()) + const auto history = impl_->history.snapshot(); + if(history.empty()) return 0; - const int width = std::min(state.frequency_point_size.get(), static_cast(runtime.history.back().size())); + const int width = std::min(state.frequency_point_size.get(), static_cast(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(); - }); + impl_->history.update({values.begin(), values.end()}, 64); changed(); } void Afterglow_Control::append_spectrum(std::pmr::vector&& values) { @@ -51,21 +46,20 @@ void Afterglow_Control::append_spectrum(std::pmr::vector&& values) { } 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()) + const auto history = impl_->history.snapshot(); + if(history.empty()) return; - const int width = std::min(state.frequency_point_size.get(), static_cast(runtime.history.back().size())); + const int width = std::min(state.frequency_point_size.get(), static_cast(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) { + for(auto iterator = history.rbegin(); iterator != 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); diff --git a/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/plottable/Constellation_Diagram.cpp index 0e05a01..91071aa 100644 --- a/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/plottable/Constellation_Diagram.cpp @@ -1,4 +1,5 @@ #include "Constellation_Diagram.h" +#include "Plottable_Real_Time_Data.h" #include "../render/Blend2D_Cache.h" #include #include @@ -12,34 +13,29 @@ 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; +using Constellation_History = Plottable_History_Real_Time_Data>; } struct Constellation_Diagram_Control::Impl { - Impl(std::shared_ptr i, std::shared_ptr q) : i_axis(std::move(i)), q_axis(std::move(q)) {} + Impl(Constellation_Diagram_Control& owner, std::shared_ptr i, std::shared_ptr q) + : i_axis(std::move(i)), q_axis(std::move(q)), points(observe_real_time_data(owner)) {} std::shared_ptr i_axis; std::shared_ptr q_axis; - Constellation_Runtime_State runtime; + Constellation_History points; }; 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))) {} + : Plottable_State(plot, properties), impl_(std::make_unique(*this, 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(); - }); + const auto cutoff = now - std::chrono::milliseconds(lifetime); + const auto cutoff_ns = std::chrono::duration_cast(cutoff.time_since_epoch()).count(); + impl_->points.update({point, now}); + impl_->points.discard_before_time_ns(static_cast(cutoff_ns)); changed(); } std::size_t Constellation_Diagram_Control::point_count() const { - return impl_->runtime.read([](const Constellation_Runtime& runtime) { return runtime.points.size(); }); + return impl_->points.size(); } void Constellation_Diagram_Control::fit_square_to_axes() { const auto state = properties(); @@ -49,11 +45,10 @@ void Constellation_Diagram_Control::fit_square_to_axes() { } 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 auto points = impl_->points.snapshot(); const Axis_Transform x = impl_->i_axis->transform(); const Axis_Transform y = impl_->q_axis->transform(); const int count = static_cast(state.type); @@ -64,7 +59,7 @@ void Constellation_Diagram_Control::paint(Painter& painter) { 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) { + for(const auto& value : 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/Frequency_Trace.cpp b/render_2D/plottable/Frequency_Trace.cpp index f32f986..0d4151c 100644 --- a/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/plottable/Frequency_Trace.cpp @@ -1,4 +1,5 @@ #include "Frequency_Trace.h" +#include "Plottable_Real_Time_Data.h" #include "../render/Blend2D_Cache.h" #include #include @@ -6,53 +7,46 @@ 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; +using Frequency_Trace_History = Plottable_History_Real_Time_Data, std::deque>>; } struct Frequency_Trace_Control::Impl { - Impl(std::shared_ptr time, std::shared_ptr value) : time_axis(std::move(time)), value_axis(std::move(value)) {} + Impl(Frequency_Trace_Control& owner, std::shared_ptr time, std::shared_ptr value) + : time_axis(std::move(time)), value_axis(std::move(value)), samples(observe_real_time_data(owner)) {} std::shared_ptr time_axis; std::shared_ptr value_axis; - Frequency_Trace_Runtime_State runtime; + Frequency_Trace_History samples; }; 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))) {} + : Plottable_State(plot, properties), impl_(std::make_unique(*this, 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(); - }); + impl_->samples.update({tick, value}, static_cast(limit)); 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(); }); + return impl_->samples.size(); } std::size_t Frequency_Trace_Control::rendered_point_count() const { - return sample_count() >= 2 ? sample_count() : 0; + const std::size_t count = sample_count(); + return count >= 2 ? 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) + const auto samples = impl_->samples.snapshot(); + if(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.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, state.pen); } diff --git a/render_2D/plottable/Plottable_Real_Time_Data.h b/render_2D/plottable/Plottable_Real_Time_Data.h new file mode 100644 index 0000000..d015001 --- /dev/null +++ b/render_2D/plottable/Plottable_Real_Time_Data.h @@ -0,0 +1,17 @@ +#pragma once +#include "../renderable/Renderable.h" +#include +#include +#include +#include +#include +namespace renderive::detail { +using Plottable_Real_Time_Data_Observer = Observer_State; +inline Plottable_Real_Time_Data_Observer observe_real_time_data(Renderable& renderable) { + return Plottable_Real_Time_Data_Observer(Frame_Strategy_Real_Time_Data_Observer(renderable)); +} +template +using Plottable_Latest_Real_Time_Data = Latest_Real_Time_Data; +template +using Plottable_History_Real_Time_Data = History_Real_Time_Data; +} diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/plottable/Selection_Rectangle_Overlay.cpp index fff33b7..158ab74 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -1,4 +1,5 @@ #include "Selection_Rectangle_Overlay.h" +#include "Plottable_Real_Time_Data.h" #include "../render/Blend2D_Cache.h" #include #include @@ -6,14 +7,13 @@ namespace renderive { namespace detail { namespace { -struct Selection_Runtime_Base {}; -struct Selection_Runtime { - std::vector regions; +struct Selection_Interaction_Base {}; +struct Selection_Interaction { bool selecting{}; PointF selection_start{}; PointF selection_current{}; }; -using Selection_Runtime_State = Double_State_Strategy; +using Selection_Interaction_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); @@ -23,21 +23,23 @@ RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& } } 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)) {} + Impl(Selection_Rectangle_Overlay_Control& owner, std::shared_ptr horizontal, std::shared_ptr vertical) + : horizontal_axis(std::move(horizontal)), vertical_axis(std::move(vertical)), regions(observe_real_time_data(owner)) {} std::shared_ptr horizontal_axis; std::shared_ptr vertical_axis; - Selection_Runtime_State runtime; + Plottable_History_Real_Time_Data> regions; + Selection_Interaction_State interaction; }; 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))) {} + : Plottable_State(plot, properties), impl_(std::make_unique(*this, 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>(); + return impl_->regions.snapshot(); } void Selection_Rectangle_Overlay_Control::clear_selected_regions() { - impl_->runtime.update([](Selection_Runtime& runtime) { - runtime.regions.clear(); - runtime.selecting = false; + impl_->regions.clear(); + impl_->interaction.update([](Selection_Interaction& interaction) { + interaction.selecting = false; }); changed(); } @@ -47,9 +49,9 @@ void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) { 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; + impl_->interaction.update([&](Selection_Interaction& interaction) { + interaction.selecting = true; + interaction.selection_start = interaction.selection_current = pointer.position; }); event.accept(); changed(); @@ -58,10 +60,10 @@ void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) { if(event.type == Event_Type::Pointer_Move) { const auto& pointer = static_cast(event); bool selecting{}; - impl_->runtime.update([&](Selection_Runtime& runtime) { - selecting = runtime.selecting; + impl_->interaction.update([&](Selection_Interaction& interaction) { + selecting = interaction.selecting; if(selecting) - runtime.selection_current = pointer.position; + interaction.selection_current = pointer.position; }); if(!selecting) return; @@ -74,30 +76,30 @@ void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) { const auto& pointer = static_cast(event); PointF start; bool selecting{}; - impl_->runtime.update([&](Selection_Runtime& runtime) { - selecting = runtime.selecting; + impl_->interaction.update([&](Selection_Interaction& interaction) { + selecting = interaction.selecting; if(!selecting) return; - runtime.selecting = false; - start = runtime.selection_start; - runtime.selection_current = pointer.position; + interaction.selecting = false; + start = interaction.selection_start; + interaction.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()); }); + impl_->regions.update(region.normalized()); event.accept(); changed(); } void Selection_Rectangle_Overlay_Control::publish() { publish_properties(); - impl_->runtime.publish(); + impl_->interaction.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) { + const auto interaction = impl_->interaction.render_use_state(); + for(const RectF& region : impl_->regions.snapshot()) { 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; @@ -105,8 +107,8 @@ void Selection_Rectangle_Overlay_Control::paint(Painter& painter) { 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); + if(interaction.selecting) + painter.rect({interaction.selection_start.x, interaction.selection_start.y, interaction.selection_current.x - interaction.selection_start.x, interaction.selection_current.y - interaction.selection_start.y}, state.selection_border_pen, state.selection_brush); } } } diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index 1421841..efaf7d8 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -1,5 +1,6 @@ #include "Spectrum.h" #include "Curve_Sampling.h" +#include "Plottable_Real_Time_Data.h" #include #include #include @@ -7,16 +8,18 @@ namespace renderive { namespace detail { namespace { -struct Spectrum_Runtime_Base {}; -struct Spectrum_Runtime { +struct Spectrum_Frame { std::vector samples; std::vector maxima; std::vector minima; +}; +struct Spectrum_Interaction_Base {}; +struct Spectrum_Interaction { std::vector markers; int selected_marker = -1; Hover_Tooltip_Runtime tooltip; }; -using Spectrum_Runtime_State = Double_State_Strategy; +using Spectrum_Interaction_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); @@ -38,80 +41,86 @@ void draw_curve(Painter& painter, std::span values, Range domain, } painter.polyline(points, pen); } -double spectrum_power_at(const Spectrum_Properties& properties, const Spectrum_Runtime& runtime, double frequency, bool& ok) { +double spectrum_power_at(const Spectrum_Properties& properties, const Spectrum_Frame& frame, double frequency, bool& ok) { ok = false; - if(runtime.samples.empty() || !properties.frequency_range.contains(frequency) || properties.frequency_range.length() == 0.0) + if(frame.samples.empty() || !properties.frequency_range.contains(frequency) || properties.frequency_range.length() == 0.0) return 0.0; 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 double position = std::clamp(normalized, 0.0, 1.0) * static_cast(frame.samples.size() - 1); const auto lower = static_cast(std::floor(position)); - const auto upper = std::min(lower + 1, runtime.samples.size() - 1); + const auto upper = std::min(lower + 1, frame.samples.size() - 1); const double fraction = position - static_cast(lower); ok = true; - return runtime.samples[lower] * (1.0 - fraction) + runtime.samples[upper] * fraction; + return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction; } } struct Spectrum_Control::Impl { - Impl(std::shared_ptr frequency, std::shared_ptr power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {} + Impl(Spectrum_Control& owner, std::shared_ptr frequency, std::shared_ptr power) + : frequency_axis(std::move(frequency)), power_axis(std::move(power)), frame(observe_real_time_data(owner)) {} std::shared_ptr frequency_axis; std::shared_ptr power_axis; - Spectrum_Runtime_State runtime; + Plottable_Latest_Real_Time_Data frame; + Spectrum_Interaction_State interaction; + std::mutex frame_update_mutex; }; 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))) {} + : Plottable_State(plot, properties), impl_(std::make_unique(*this, 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]); - }); + std::lock_guard lock(impl_->frame_update_mutex); + Spectrum_Frame frame = impl_->frame.snapshot().value_or(Spectrum_Frame{}); + frame.samples.assign(values.begin(), values.end()); + if(frame.maxima.size() != values.size()) + frame.maxima.assign(values.begin(), values.end()); + else + for(std::size_t index = 0; index < values.size(); ++index) + frame.maxima[index] = std::max(frame.maxima[index], values[index]); + if(frame.minima.size() != values.size()) + frame.minima.assign(values.begin(), values.end()); + else + for(std::size_t index = 0; index < values.size(); ++index) + frame.minima[index] = std::min(frame.minima[index], values[index]); + impl_->frame.update(std::move(frame)); changed(); } 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(); }); + const auto frame = impl_->frame.snapshot(); + return frame ? frame->samples.size() : 0; } 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(); + const auto frame = impl_->frame.snapshot(); + return frame ? curve_points(frame->samples, state.frequency_range, impl_->frequency_axis->transform(), impl_->power_axis->transform(), state.visible_range_only, state.interpolation_mode).size() : 0; } 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); }); + const auto frame = impl_->frame.snapshot(); + return frame ? spectrum_power_at(state, *frame, frequency, ok) : (ok = false, 0.0); } 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); }); + impl_->interaction.update([frequency](Spectrum_Interaction& interaction) { interaction.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()) + impl_->interaction.update([&](Spectrum_Interaction& interaction) { + if(interaction.markers.empty()) return; - 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; + auto closest = std::min_element(interaction.markers.begin(), interaction.markers.end(), [frequency](double left, double right) { return std::abs(left - frequency) < std::abs(right - frequency); }); + const int removed_index = static_cast(std::distance(interaction.markers.begin(), closest)); + interaction.markers.erase(closest); + if(interaction.selected_marker == removed_index) + interaction.selected_marker = -1; + else if(interaction.selected_marker > removed_index) + --interaction.selected_marker; removed = true; }); if(removed) @@ -119,48 +128,48 @@ void Spectrum_Control::remove_custom_marker(double frequency) { } 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())) + impl_->interaction.update([&](Spectrum_Interaction& interaction) { + if(interaction.selected_marker < 0 || interaction.selected_marker >= static_cast(interaction.markers.size())) return; - runtime.markers.erase(runtime.markers.begin() + runtime.selected_marker); - runtime.selected_marker = -1; + interaction.markers.erase(interaction.markers.begin() + interaction.selected_marker); + interaction.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; + impl_->interaction.update([](Spectrum_Interaction& interaction) { + interaction.markers.clear(); + interaction.selected_marker = -1; }); changed(); } int Spectrum_Control::selectable_line_marker_count() const { - return impl_->runtime.read([](const Spectrum_Runtime& runtime) { return static_cast(runtime.markers.size()); }); + return impl_->interaction.read([](const Spectrum_Interaction& interaction) { return static_cast(interaction.markers.size()); }); } int Spectrum_Control::selected_marker_index() const { - return impl_->runtime.get<&Spectrum_Runtime::selected_marker>(); + return impl_->interaction.get<&Spectrum_Interaction::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; }); + impl_->interaction.update([index](Spectrum_Interaction& interaction) { interaction.selected_marker = index >= 0 && index < static_cast(interaction.markers.size()) ? index : -1; }); changed(); } void Spectrum_Control::select_next_marker() { - impl_->runtime.update([](Spectrum_Runtime& runtime) { - if(runtime.markers.empty()) - runtime.selected_marker = -1; + impl_->interaction.update([](Spectrum_Interaction& interaction) { + if(interaction.markers.empty()) + interaction.selected_marker = -1; else - runtime.selected_marker = (runtime.selected_marker + 1) % static_cast(runtime.markers.size()); + interaction.selected_marker = (interaction.selected_marker + 1) % static_cast(interaction.markers.size()); }); changed(); } void Spectrum_Control::select_previous_marker() { - impl_->runtime.update([](Spectrum_Runtime& runtime) { - if(runtime.markers.empty()) - runtime.selected_marker = -1; + impl_->interaction.update([](Spectrum_Interaction& interaction) { + if(interaction.markers.empty()) + interaction.selected_marker = -1; else - runtime.selected_marker = (runtime.selected_marker <= 0 ? static_cast(runtime.markers.size()) : runtime.selected_marker) - 1; + interaction.selected_marker = (interaction.selected_marker <= 0 ? static_cast(interaction.markers.size()) : interaction.selected_marker) - 1; }); changed(); } @@ -168,14 +177,14 @@ void Spectrum_Control::clear_marker_selection() { set_selected_marker_index(-1); } 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; }); + return impl_->interaction.read([index](const Spectrum_Interaction& interaction) { return index >= 0 && index < static_cast(interaction.markers.size()) ? interaction.markers[index] : 0.0; }); } 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())) + impl_->interaction.update([&](Spectrum_Interaction& interaction) { + if(index < 0 || index >= static_cast(interaction.markers.size())) return; - runtime.markers[index] = frequency; + interaction.markers[index] = frequency; updated = true; }); if(updated) @@ -188,17 +197,18 @@ void Spectrum_Control::set_current_marker_frequency(double frequency) { } void Spectrum_Control::handle_event(const Event& event) { bool updated{}; - impl_->runtime.update([&](Spectrum_Runtime& runtime) { updated = update_hover_tooltip(runtime.tooltip, event); }); + impl_->interaction.update([&](Spectrum_Interaction& interaction) { updated = update_hover_tooltip(interaction.tooltip, event); }); if(updated) changed(); } void Spectrum_Control::publish() { publish_properties(); - impl_->runtime.publish(); + impl_->interaction.publish(); } void Spectrum_Control::paint(Painter& painter) { const auto state = render_properties(); - const auto runtime = impl_->runtime.render_use_state(); + const Spectrum_Frame frame = impl_->frame.snapshot().value_or(Spectrum_Frame{}); + const auto interaction = impl_->interaction.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); @@ -208,23 +218,23 @@ void Spectrum_Control::paint(Painter& painter) { 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_visible) - draw_curve(painter, runtime.maxima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.max_pen, state.max_brush); + draw_curve(painter, frame.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); + draw_curve(painter, frame.minima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.min_pen, state.min_brush); + draw_curve(painter, frame.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_frequency_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); + for(std::size_t index = 0; index < interaction.markers.size(); ++index) { + const double x = horizontal.coord_to_pixel(interaction.markers[index]); + painter.line({x, content.y}, {x, content.bottom()}, static_cast(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen); } - if(!runtime.samples.empty() && (state.max_marker_visible || state.use_min_marker)) { + if(!frame.samples.empty() && (state.max_marker_visible || state.use_min_marker)) { const auto draw_extreme = [&](bool maximum) { - 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; + auto iterator = maximum ? std::max_element(frame.samples.begin(), frame.samples.end()) : std::min_element(frame.samples.begin(), frame.samples.end()); + const std::size_t index = static_cast(std::distance(frame.samples.begin(), iterator)); + const double denominator = frame.samples.size() > 1 ? frame.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; @@ -235,14 +245,14 @@ void Spectrum_Control::paint(Painter& painter) { if(state.use_min_marker) draw_extreme(false); } - if(state.tooltip_enabled && runtime.tooltip.active && content.contains(runtime.tooltip.position)) { - const double frequency = horizontal.pixel_to_coord(runtime.tooltip.position.x); + if(state.tooltip_enabled && interaction.tooltip.active && content.contains(interaction.tooltip.position)) { + const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x); bool ok{}; - const double power = spectrum_power_at(state, runtime, frequency, ok); + const double power = spectrum_power_at(state, frame, frequency, ok); if(ok) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency << " Hz " << power; - const RectF box{runtime.tooltip.position.x + 8.0, runtime.tooltip.position.y + 8.0, 170.0, 24.0}; + const RectF box{interaction.tooltip.position.x + 8.0, interaction.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); } diff --git a/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/plottable/Sweep_Spectrum.cpp index de15c29..e13b2ae 100644 --- a/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/plottable/Sweep_Spectrum.cpp @@ -1,5 +1,6 @@ #include "Sweep_Spectrum.h" #include "Curve_Sampling.h" +#include "Plottable_Real_Time_Data.h" #include "../render/Blend2D_Cache.h" #include #include @@ -7,11 +8,7 @@ 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; +using Sweep_Spectrum_History = Plottable_History_Real_Time_Data, std::deque>>; 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); @@ -19,72 +16,63 @@ RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& 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 flatten(const std::deque>& blocks) { std::vector values; - for(const auto& block : runtime.blocks) + for(const auto& block : 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)) {} + Impl(Sweep_Spectrum_Control& owner, std::shared_ptr frequency, std::shared_ptr power) + : frequency_axis(std::move(frequency)), power_axis(std::move(power)), blocks(observe_real_time_data(owner)) {} std::shared_ptr frequency_axis; std::shared_ptr power_axis; - Sweep_Spectrum_Runtime_State runtime; + Sweep_Spectrum_History blocks; }; 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))) {} + : Plottable_State(plot, properties), impl_(std::make_unique(*this, 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(); - }); + impl_->blocks.update({values.begin(), values.end()}, static_cast(limit)); 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(); }); + return impl_->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; - }); + const auto blocks = impl_->blocks.snapshot(); + std::size_t count{}; + for(const auto& block : 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); + const auto values = flatten(impl_->blocks.snapshot()); 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(); + impl_->blocks.retain_latest(static_cast(limit)); } 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); + const auto blocks = impl_->blocks.snapshot(); + const auto values = flatten(blocks); 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 completed = std::min(1.0, static_cast(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); diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp index 650ef5b..7b8cc90 100644 --- a/render_2D/plottable/Waterfall.cpp +++ b/render_2D/plottable/Waterfall.cpp @@ -1,5 +1,6 @@ #include "Waterfall.h" #include "Heatmap_Utils.h" +#include "Plottable_Real_Time_Data.h" #include #include #include @@ -11,31 +12,29 @@ struct Waterfall_Row { int tick{}; std::vector values; }; -struct Waterfall_Runtime_Base {}; -struct Waterfall_Runtime { - std::deque rows; +struct Waterfall_Interaction_Base {}; +struct Waterfall_Interaction { Hover_Tooltip_Runtime tooltip; }; -using Waterfall_Runtime_State = Double_State_Strategy; +using Waterfall_History = Plottable_History_Real_Time_Data>; +using Waterfall_Interaction_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)) {} + Impl(Waterfall_Control& owner, std::shared_ptr frequency, std::shared_ptr time) + : frequency_axis(std::move(frequency)), time_axis(std::move(time)), rows(observe_real_time_data(owner)) {} std::shared_ptr frequency_axis; std::shared_ptr time_axis; - Waterfall_Runtime_State runtime; + Waterfall_History rows; + Waterfall_Interaction_State interaction; }; 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))) {} + : Plottable_State(plot, properties), impl_(std::make_unique(*this, 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(); - }); + impl_->rows.update({tick, {values.begin(), values.end()}}, limit); changed(); } void Waterfall_Control::append_row(int tick, std::pmr::vector&& values) { @@ -48,44 +47,44 @@ void Waterfall_Control::append_row(Time_Of_Day time, std::pmr::vector&& 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(); }); + return impl_->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; - }); + const auto rows = impl_->rows.snapshot(); + std::size_t count{}; + for(const auto& row : 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()) + const auto rows = impl_->rows.snapshot(); + if(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())); + const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(rows.begin(), 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; + return columns ? static_cast(columns->last - columns->first + 1) * 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); }); + impl_->interaction.update([&](Waterfall_Interaction& interaction) { updated = update_hover_tooltip(interaction.tooltip, event); }); if(updated) changed(); } void Waterfall_Control::publish() { publish_properties(); - impl_->runtime.publish(); + impl_->interaction.publish(); } void Waterfall_Control::paint(Painter& painter) { const auto state = render_properties(); - const auto runtime = impl_->runtime.render_use_state(); - if(runtime.rows.empty()) + const auto rows = impl_->rows.snapshot(); + const auto interaction = impl_->interaction.render_use_state(); + if(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()); + const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); + const int height = static_cast(rows.size()); if(source_width <= 0 || height <= 0) return; const Axis_Transform horizontal = impl_->frequency_axis->transform(); @@ -95,21 +94,21 @@ void Waterfall_Control::paint(Painter& painter) { 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; + const auto& row = 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)}; + const Range time_range{static_cast(rows.front().tick), static_cast(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); + if(state.tooltip_enabled && interaction.tooltip.active && target.contains(interaction.tooltip.position)) { + const double frequency = horizontal.pixel_to_coord(interaction.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}; + const RectF box{interaction.tooltip.position.x + 8.0, interaction.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/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 0cd689f..7cd20b5 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -563,5 +563,26 @@ TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { EXPECT_EQ(state->state_revision(), 1U); ASSERT_TRUE(plot.render_prepared_frame()); } +TEST(Renderive_Core2, PlottableDataUpdatesReachKernelRealTimeDataStrategy) { + 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); + const auto afterglow = Afterglow::Builder{}.build(root, frequency, power); + ASSERT_TRUE(spectrum); + ASSERT_TRUE(afterglow); + const auto before = plot.frame_observer_snapshot().observation_count; + const std::array samples{-80.0, -70.0, -60.0, -50.0}; + spectrum->update_samples(samples); + const auto latest = plot.frame_observer_snapshot(); + EXPECT_EQ(latest.last_event, "real_time_data_updated"); + EXPECT_GT(latest.observation_count, before); + afterglow->append_spectrum(samples); + const auto history = plot.frame_observer_snapshot(); + EXPECT_EQ(history.last_event, "real_time_data_updated"); + EXPECT_GT(history.observation_count, latest.observation_count); +} } // namespace } // namespace renderive diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index cc247b8..7b260ac 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -1823,7 +1823,7 @@ struct Gallery_Plot_Session::Impl { Web_Response_Type::Json, Gallery_Protocol::case_json(id, scene->state(), scene->telemetry_json(), - "Renderable 已移除并通过 Builder 重建", mode) + "Renderable 已移除并按属性重建", mode) }; } bool recognized{}; diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 56cb3fa..098ac3c 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -59,11 +59,11 @@ const std::vector& cases() { {"waterfall", "瀑布图", "Waterfall", "热力图控件", "逐行时频热力图;Nearest、Bilinear、Bicubic 三种图像模式均可切换。", 50}, {"frequency_trace", "时间轨迹", "Frequency_Trace", "曲线控件", - "Time_Axis 驱动的连续轨迹,覆盖时间格式和 Builder 笔刷 API。", 60}, + "Time_Axis 驱动的连续轨迹,覆盖时间格式和属性化笔刷 API。", 60}, {"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件", - "鼠标框选、多区域保留、标注样式、清空与轴重新绑定。", 70}, + "鼠标框选、多区域保留、标注样式和清空。", 70}, {"constellation", "星座图", "Constellation_Diagram", "点图控件", - "PSK4/PSK8/PSK16 Builder 模式、相位、寿命、坐标范围和方形拟合。", 80} + "PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80} }; return value; } diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 7b4cbc8..2529b78 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -259,8 +259,8 @@ TEST(RenderiveWebGallery, BackendMenuMapsRetainedControlApis) { for (const auto mode : modes) { const auto contract = parse_json(Gallery_Protocol::case_json( case_id, Gallery_Protocol::default_state(case_id, mode), "{}", {}, mode)); - for (const auto& control : contract.at("controls").at("data")) - api_text += control.at("api").get() + '\n'; + for (const auto& control : contract.at("controls").at("descriptor").at("fields")) + api_text += control.at("presentation").at("description").get() + '\n'; for (const auto& action : contract.at("actions").at("data")) api_text += action.at("api").get() + '\n'; } @@ -497,57 +497,53 @@ TEST(RenderiveWebGallery, EveryPublishedControlAppliesToTheLiveCore2Scene) { auto state = response_json(session.handle(gallery_request( Gallery_Request_Kind::Open, open_message(case_id, mode)))); ASSERT_EQ(state.at("type"), "case_state"); - const auto published_controls = state.at("controls").at("data"); + const auto published_controls = state.at("controls").at("descriptor").at("fields"); for (const auto& published : published_controls) { ++applied_control_instances; - const std::string id = published.at("id"); + const std::string id = published.at("name"); SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode) + "/" + id); - const std::string input = published.at("input"); + const std::string input = published.at("presentation").at("control"); + const auto& current_value = state.at("controls").at("data").at(id); nlohmann::json alternative; if (input == "boolean") { - alternative = !published.at("value").get(); + alternative = !current_value.get(); } else if (input == "number") { const double minimum = published.at("minimum"); const double maximum = published.at("maximum"); - const double current = published.at("value"); - const double step = std::max(published.at("step").get(), 1e-9); + const double current = current_value; + const double step = std::max(published.at("multiple_of").get(), 1e-9); double value = current + step; if (value > maximum) value = current - step; if (value < minimum || value == current) value = current == minimum ? maximum : minimum; - if (published.at("integer").get()) + const bool integer = published.at("value_type") == "integer"; + if (integer) value = std::round(value); - alternative = value; + alternative = integer ? nlohmann::json(static_cast(std::llround(value))) : nlohmann::json(value); } else if (input == "select") { - const auto& options = published.at("options"); - const std::string current = published.at("value"); + const auto& options = published.at("presentation").at("options"); + const std::string current = current_value; const auto selected = std::find_if( options.begin(), options.end(), [¤t](const auto& option) { - return option.template get() != current; + return option.at("value").template get() != current; }); ASSERT_NE(selected, options.end()); - alternative = *selected; + alternative = selected->at("value"); } else if (input == "color") { - alternative = published.at("value") == "#123456" ? "#654321" : "#123456"; + alternative = current_value == "#123456" ? "#654321" : "#123456"; } else { ASSERT_EQ(input, "text"); - alternative = published.at("value").get() + "_live"; + alternative = current_value.get() + "_live"; } nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"}, {"patch", {{id, alternative}}}}; state = response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, request.dump()))); - ASSERT_EQ(state.at("type"), "case_state"); + ASSERT_EQ(state.at("type"), "case_state") << state.dump(); EXPECT_EQ(state.at("frame_mode").at("id").get(), mode); - const auto& returned = state.at("controls").at("data"); - const auto found = std::find_if(returned.begin(), returned.end(), - [&id](const auto& item) { - return item.at("id") == id; - }); - ASSERT_NE(found, returned.end()); - EXPECT_EQ(found->at("value"), alternative); + EXPECT_EQ(state.at("controls").at("data").at(id), alternative); EXPECT_TRUE(state.at("telemetry").contains("kernel_observer")); } } @@ -648,7 +644,7 @@ TEST(RenderiveWebGallery, CommonControlsAndViewLifecycleProduceObservableEffects "case_state"); const auto background_state = patch_controls(session, {{"background_color", "#123456"}}); - EXPECT_EQ(background_state.at("controls").at("data").at(0).at("value"), "#123456"); + EXPECT_EQ(background_state.at("controls").at("data").at("background_color"), "#123456"); ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state"); const auto background_frame = session.handle(Frame_Request{}); ASSERT_TRUE(background_frame.has_value()); @@ -1443,7 +1439,7 @@ TEST(RenderiveWebGallery, ConstellationLowLatencyPixelsChange) { expect_low_latency_canvas_to_change("constellation"); } -TEST(RenderiveWebGallery, BuilderOnlyControlsRebuildAndAllImageModesRender) { +TEST(RenderiveWebGallery, PropertyChangesRebuildAndAllImageModesRender) { Gallery_Plot_Session constellation; ASSERT_TRUE(constellation.handle(gallery_request( Gallery_Request_Kind::Open, @@ -1453,12 +1449,7 @@ TEST(RenderiveWebGallery, BuilderOnlyControlsRebuildAndAllImageModesRender) { R"({"category":"event","type":"gallery_patch","patch":{"constellation_type":"PSK16","phase_offset":0.75}})")); ASSERT_TRUE(rebuilt.has_value()); const auto rebuilt_json = parse_json(rebuilt->payload); - const auto& rebuilt_controls = rebuilt_json.at("controls").at("data"); - const auto type = std::find_if(rebuilt_controls.begin(), rebuilt_controls.end(), [](const auto& item) { - return item.at("id") == "constellation_type"; - }); - ASSERT_NE(type, rebuilt_controls.end()); - EXPECT_EQ(type->at("value"), "PSK16"); + EXPECT_EQ(rebuilt_json.at("controls").at("data").at("constellation_type"), "PSK16"); ASSERT_TRUE(constellation.handle(Frame_Request{}).has_value()); Gallery_Plot_Session waterfall;