加上实时数据和历史数据目标到私有类里

This commit is contained in:
2026-08-11 18:11:28 +08:00
parent 0327d71536
commit c89a678245
15 changed files with 323 additions and 268 deletions
@@ -3,6 +3,7 @@
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <limits>
#include <memory_resource>
#include <mutex>
#include <utility>
@@ -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;
@@ -39,6 +39,10 @@ void History_Real_Time_Data<Value_Type, Container, Mutex, Observer>::reserve_upd
}
template <class Value_Type, class Container, Mutex_Type Mutex, class Observer>
void History_Real_Time_Data<Value_Type, Container, Mutex, Observer>::update(Value value) {
update(std::move(value), std::numeric_limits<std::size_t>::max());
}
template <class Value_Type, class Container, Mutex_Type Mutex, class Observer>
void History_Real_Time_Data<Value_Type, Container, Mutex, Observer>::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<Value_Type, Container, Mutex, Observer>::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<std::ptrdiff_t>(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<Value_Type, Container, Mutex, Observer>::clear() {
observer_.observe(observation);
}
template <class Value_Type, class Container, Mutex_Type Mutex, class Observer>
std::size_t History_Real_Time_Data<Value_Type, Container, Mutex, Observer>::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<Mutex> lock(mutex_);
if(values_.size() <= count)
return 0;
discarded = values_.size() - count;
values_.erase(values_.begin(), std::next(values_.begin(), static_cast<std::ptrdiff_t>(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 <class Value_Type, class Container, Mutex_Type Mutex, class Observer>
auto History_Real_Time_Data<Value_Type, Container, Mutex, Observer>::snapshot() const -> Container {
std::lock_guard<Mutex> lock(mutex_);
return values_;
@@ -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<int>{3}));
}
TEST(history_real_time_data_test, retains_latest_requested_value_count) {
History_Real_Time_Data<int> data;
data.update(1);
data.update(2, 2);
data.update(3, 2);
EXPECT_EQ(data.snapshot(), (std::vector<int>{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<int>{3, 4}));
}
TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy) {
Scene2D_Context<> scene;
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
+17 -23
View File
@@ -1,49 +1,44 @@
#include "Afterglow.h"
#include "Heatmap_Utils.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <deque>
namespace renderive {
namespace detail {
namespace {
struct Afterglow_Runtime_Base {};
struct Afterglow_Runtime {
std::deque<std::vector<double>> history;
};
using Afterglow_Runtime_State = Double_State_Strategy<Afterglow_Runtime_Base, Afterglow_Runtime>;
using Afterglow_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
}
struct Afterglow_Control::Impl {
Impl(std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Axis> power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {}
Impl(Afterglow_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Axis> power)
: frequency_axis(std::move(frequency)), power_axis(std::move(power)), history(observe_real_time_data(owner)) {}
std::shared_ptr<Frequency_Axis> frequency_axis;
std::shared_ptr<Axis> 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> frequency_axis, std::shared_ptr<Axis> power_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(std::move(frequency_axis), std::move(power_axis))) {}
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*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<int>(runtime.history.back().size()));
const int width = std::min(state.frequency_point_size.get(), static_cast<int>(history.back().size()));
const int height = state.power_point_size.get() > 0 ? state.power_point_size.get() : std::max(1, static_cast<int>(impl_->power_axis->pixel_length()));
return width > 0 && height > 0 ? static_cast<std::size_t>(width) * static_cast<std::size_t>(height) : 0;
}
void Afterglow_Control::append_spectrum(std::span<const double> values) {
if(get<&Afterglow_Properties::frequency_point_size>() <= 0)
set<&Afterglow_Properties::frequency_point_size>(static_cast<int>(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<double>&& values) {
@@ -51,21 +46,20 @@ void Afterglow_Control::append_spectrum(std::pmr::vector<double>&& 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<int>(runtime.history.back().size()));
const int width = std::min(state.frequency_point_size.get(), static_cast<int>(history.back().size()));
const int height = state.power_point_size.get() > 0 ? state.power_point_size.get() : std::max(1, static_cast<int>(impl_->power_axis->pixel_length()));
if(width <= 0 || height <= 0)
return;
std::vector<double> intensity(static_cast<std::size_t>(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<int>(iterator->size()));
for(int x = 0; x < count; ++x) {
const double normalized = normalized_value((*iterator)[static_cast<std::size_t>(x)], state.power_range);
+13 -18
View File
@@ -1,4 +1,5 @@
#include "Constellation_Diagram.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <chrono>
@@ -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<Timed_Point> points;
};
using Constellation_Runtime_State = Double_State_Strategy<Constellation_Runtime_Base, Constellation_Runtime>;
using Constellation_History = Plottable_History_Real_Time_Data<Timed_Point, std::deque<Timed_Point>>;
}
struct Constellation_Diagram_Control::Impl {
Impl(std::shared_ptr<Axis> i, std::shared_ptr<Axis> q) : i_axis(std::move(i)), q_axis(std::move(q)) {}
Impl(Constellation_Diagram_Control& owner, std::shared_ptr<Axis> i, std::shared_ptr<Axis> q)
: i_axis(std::move(i)), q_axis(std::move(q)), points(observe_real_time_data(owner)) {}
std::shared_ptr<Axis> i_axis;
std::shared_ptr<Axis> 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<Axis> i_axis, std::shared_ptr<Axis> q_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(std::move(i_axis), std::move(q_axis))) {}
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*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<std::chrono::nanoseconds>(cutoff.time_since_epoch()).count();
impl_->points.update({point, now});
impl_->points.discard_before_time_ns(static_cast<std::uint64_t>(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<int>(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});
+14 -20
View File
@@ -1,4 +1,5 @@
#include "Frequency_Trace.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <deque>
@@ -6,53 +7,46 @@
namespace renderive {
namespace detail {
namespace {
struct Frequency_Trace_Runtime_Base {};
struct Frequency_Trace_Runtime {
std::deque<std::pair<int, double>> samples;
};
using Frequency_Trace_Runtime_State = Double_State_Strategy<Frequency_Trace_Runtime_Base, Frequency_Trace_Runtime>;
using Frequency_Trace_History = Plottable_History_Real_Time_Data<std::pair<int, double>, std::deque<std::pair<int, double>>>;
}
struct Frequency_Trace_Control::Impl {
Impl(std::shared_ptr<Time_Axis> time, std::shared_ptr<Axis> value) : time_axis(std::move(time)), value_axis(std::move(value)) {}
Impl(Frequency_Trace_Control& owner, std::shared_ptr<Time_Axis> time, std::shared_ptr<Axis> value)
: time_axis(std::move(time)), value_axis(std::move(value)), samples(observe_real_time_data(owner)) {}
std::shared_ptr<Time_Axis> time_axis;
std::shared_ptr<Axis> 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> time_axis, std::shared_ptr<Axis> value_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(std::move(time_axis), std::move(value_axis))) {}
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*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<std::size_t>(limit))
runtime.samples.pop_front();
});
impl_->samples.update({tick, value}, static_cast<std::size_t>(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<PointF> 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);
}
@@ -0,0 +1,17 @@
#pragma once
#include "../renderable/Renderable.h"
#include <renderive/base/observer/Observer.hpp>
#include <renderive/real_time_data/Frame_Strategy_Observer.hpp>
#include <renderive/real_time_data/History_Real_Time_Data.hpp>
#include <renderive/real_time_data/Latest_Real_Time_Data.hpp>
#include <mutex>
namespace renderive::detail {
using Plottable_Real_Time_Data_Observer = Observer_State<Frame_Strategy_Real_Time_Data_Observer>;
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 <class Value>
using Plottable_Latest_Real_Time_Data = Latest_Real_Time_Data<Value, std::mutex, Plottable_Real_Time_Data_Observer>;
template <class Value, class Container>
using Plottable_History_Real_Time_Data = History_Real_Time_Data<Value, Container, std::mutex, Plottable_Real_Time_Data_Observer>;
}
@@ -1,4 +1,5 @@
#include "Selection_Rectangle_Overlay.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <cmath>
@@ -6,14 +7,13 @@
namespace renderive {
namespace detail {
namespace {
struct Selection_Runtime_Base {};
struct Selection_Runtime {
std::vector<RectF> regions;
struct Selection_Interaction_Base {};
struct Selection_Interaction {
bool selecting{};
PointF selection_start{};
PointF selection_current{};
};
using Selection_Runtime_State = Double_State_Strategy<Selection_Runtime_Base, Selection_Runtime>;
using Selection_Interaction_State = Double_State_Strategy<Selection_Interaction_Base, Selection_Interaction>;
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<Abs_Axis> horizontal, std::shared_ptr<Abs_Axis> vertical) : horizontal_axis(std::move(horizontal)), vertical_axis(std::move(vertical)) {}
Impl(Selection_Rectangle_Overlay_Control& owner, std::shared_ptr<Abs_Axis> horizontal, std::shared_ptr<Abs_Axis> vertical)
: horizontal_axis(std::move(horizontal)), vertical_axis(std::move(vertical)), regions(observe_real_time_data(owner)) {}
std::shared_ptr<Abs_Axis> horizontal_axis;
std::shared_ptr<Abs_Axis> vertical_axis;
Selection_Runtime_State runtime;
Plottable_History_Real_Time_Data<RectF, std::vector<RectF>> 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<Abs_Axis> horizontal_axis, std::shared_ptr<Abs_Axis> vertical_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(std::move(horizontal_axis), std::move(vertical_axis))) {}
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(horizontal_axis), std::move(vertical_axis))) {}
Selection_Rectangle_Overlay_Control::~Selection_Rectangle_Overlay_Control() = default;
std::vector<RectF> 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<const Pointer_Event&>(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<const Pointer_Event&>(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<const Pointer_Event&>(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);
}
}
}
+87 -77
View File
@@ -1,5 +1,6 @@
#include "Spectrum.h"
#include "Curve_Sampling.h"
#include "Plottable_Real_Time_Data.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
@@ -7,16 +8,18 @@
namespace renderive {
namespace detail {
namespace {
struct Spectrum_Runtime_Base {};
struct Spectrum_Runtime {
struct Spectrum_Frame {
std::vector<double> samples;
std::vector<double> maxima;
std::vector<double> minima;
};
struct Spectrum_Interaction_Base {};
struct Spectrum_Interaction {
std::vector<double> markers;
int selected_marker = -1;
Hover_Tooltip_Runtime tooltip;
};
using Spectrum_Runtime_State = Double_State_Strategy<Spectrum_Runtime_Base, Spectrum_Runtime>;
using Spectrum_Interaction_State = Double_State_Strategy<Spectrum_Interaction_Base, Spectrum_Interaction>;
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<const double> 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<double>(runtime.samples.size() - 1);
const double position = std::clamp(normalized, 0.0, 1.0) * static_cast<double>(frame.samples.size() - 1);
const auto lower = static_cast<std::size_t>(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<double>(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_Axis> frequency, std::shared_ptr<Axis> power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {}
Impl(Spectrum_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Axis> power)
: frequency_axis(std::move(frequency)), power_axis(std::move(power)), frame(observe_real_time_data(owner)) {}
std::shared_ptr<Frequency_Axis> frequency_axis;
std::shared_ptr<Axis> power_axis;
Spectrum_Runtime_State runtime;
Plottable_Latest_Real_Time_Data<Spectrum_Frame> 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> frequency_axis, std::shared_ptr<Axis> power_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(std::move(frequency_axis), std::move(power_axis))) {}
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(power_axis))) {}
Spectrum_Control::~Spectrum_Control() = default;
void Spectrum_Control::update_samples(std::span<const double> values) {
if(get<&Spectrum_Properties::frequency_point_size>() <= 0)
set<&Spectrum_Properties::frequency_point_size>(static_cast<int>(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<double>&& values) {
update_samples(std::span<const double>(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<int>(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<int>(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<int>(runtime.markers.size()))
impl_->interaction.update([&](Spectrum_Interaction& interaction) {
if(interaction.selected_marker < 0 || interaction.selected_marker >= static_cast<int>(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<int>(runtime.markers.size()); });
return impl_->interaction.read([](const Spectrum_Interaction& interaction) { return static_cast<int>(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<int>(runtime.markers.size()) ? index : -1; });
impl_->interaction.update([index](Spectrum_Interaction& interaction) { interaction.selected_marker = index >= 0 && index < static_cast<int>(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<int>(runtime.markers.size());
interaction.selected_marker = (interaction.selected_marker + 1) % static_cast<int>(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<int>(runtime.markers.size()) : runtime.selected_marker) - 1;
interaction.selected_marker = (interaction.selected_marker <= 0 ? static_cast<int>(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<int>(runtime.markers.size()) ? runtime.markers[index] : 0.0; });
return impl_->interaction.read([index](const Spectrum_Interaction& interaction) { return index >= 0 && index < static_cast<int>(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<int>(runtime.markers.size()))
impl_->interaction.update([&](Spectrum_Interaction& interaction) {
if(index < 0 || index >= static_cast<int>(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<int>(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<int>(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::size_t>(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::size_t>(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);
}
+20 -32
View File
@@ -1,5 +1,6 @@
#include "Sweep_Spectrum.h"
#include "Curve_Sampling.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <cmath>
@@ -7,11 +8,7 @@
namespace renderive {
namespace detail {
namespace {
struct Sweep_Spectrum_Runtime_Base {};
struct Sweep_Spectrum_Runtime {
std::deque<std::vector<double>> blocks;
};
using Sweep_Spectrum_Runtime_State = Double_State_Strategy<Sweep_Spectrum_Runtime_Base, Sweep_Spectrum_Runtime>;
using Sweep_Spectrum_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
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<double> flatten(const Sweep_Spectrum_Runtime& runtime) {
std::vector<double> flatten(const std::deque<std::vector<double>>& blocks) {
std::vector<double> 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<Axis> frequency, std::shared_ptr<Axis> power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)) {}
Impl(Sweep_Spectrum_Control& owner, std::shared_ptr<Axis> frequency, std::shared_ptr<Axis> power)
: frequency_axis(std::move(frequency)), power_axis(std::move(power)), blocks(observe_real_time_data(owner)) {}
std::shared_ptr<Axis> frequency_axis;
std::shared_ptr<Axis> 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<Axis> frequency_axis, std::shared_ptr<Axis> power_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(std::move(frequency_axis), std::move(power_axis))) {}
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(power_axis))) {}
Sweep_Spectrum_Control::~Sweep_Spectrum_Control() = default;
void Sweep_Spectrum_Control::append_block(std::span<const double> values) {
if(get<&Sweep_Spectrum_Properties::bins_per_block>() <= 0)
set<&Sweep_Spectrum_Properties::bins_per_block>(static_cast<int>(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<std::size_t>(limit))
runtime.blocks.pop_front();
});
impl_->blocks.update({values.begin(), values.end()}, static_cast<std::size_t>(limit));
changed();
}
void Sweep_Spectrum_Control::append_block(std::pmr::vector<double>&& values) {
append_block(std::span<const double>(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<std::size_t>(limit))
runtime.blocks.pop_front();
});
impl_->runtime.publish();
impl_->blocks.retain_latest(static_cast<std::size_t>(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<double>(runtime.blocks.size()) / state.block_count.get());
const double completed = std::min(1.0, static_cast<double>(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);
+33 -34
View File
@@ -1,5 +1,6 @@
#include "Waterfall.h"
#include "Heatmap_Utils.h"
#include "Plottable_Real_Time_Data.h"
#include <algorithm>
#include <deque>
#include <iomanip>
@@ -11,31 +12,29 @@ struct Waterfall_Row {
int tick{};
std::vector<double> values;
};
struct Waterfall_Runtime_Base {};
struct Waterfall_Runtime {
std::deque<Waterfall_Row> rows;
struct Waterfall_Interaction_Base {};
struct Waterfall_Interaction {
Hover_Tooltip_Runtime tooltip;
};
using Waterfall_Runtime_State = Double_State_Strategy<Waterfall_Runtime_Base, Waterfall_Runtime>;
using Waterfall_History = Plottable_History_Real_Time_Data<Waterfall_Row, std::deque<Waterfall_Row>>;
using Waterfall_Interaction_State = Double_State_Strategy<Waterfall_Interaction_Base, Waterfall_Interaction>;
}
struct Waterfall_Control::Impl {
Impl(std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Time_Axis> time) : frequency_axis(std::move(frequency)), time_axis(std::move(time)) {}
Impl(Waterfall_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Time_Axis> time)
: frequency_axis(std::move(frequency)), time_axis(std::move(time)), rows(observe_real_time_data(owner)) {}
std::shared_ptr<Frequency_Axis> frequency_axis;
std::shared_ptr<Time_Axis> 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> frequency_axis, std::shared_ptr<Time_Axis> time_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(std::move(frequency_axis), std::move(time_axis))) {}
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(time_axis))) {}
Waterfall_Control::~Waterfall_Control() = default;
void Waterfall_Control::append_row(int tick, std::span<const double> values) {
const std::size_t limit = static_cast<std::size_t>(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<int>(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<double>&& values) {
@@ -48,44 +47,44 @@ void Waterfall_Control::append_row(Time_Of_Day time, std::pmr::vector<double>&&
append_row(time, std::span<const double>(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<int>(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<int>(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<std::size_t>(columns->last - columns->first + 1) * runtime.rows.size() : 0;
return columns ? static_cast<std::size_t>(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<int>(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<int>(runtime.rows.size());
const int source_width = std::min(state.frequency_bin_count.get(), static_cast<int>(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<int>(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<Pixel> pixels(static_cast<std::size_t>(width) * height);
for(int y = 0; y < height; ++y) {
const auto& row = runtime.rows[static_cast<std::size_t>(y)].values;
const auto& row = rows[static_cast<std::size_t>(y)].values;
for(int x = 0; x < width; ++x)
pixels[static_cast<std::size_t>(y) * width + x] = state.color_map.at_normalized(normalized_value(row[static_cast<std::size_t>(columns->first + x)], state.power_range));
}
const Axis_Transform vertical = impl_->time_axis->transform();
const Range time_range{static_cast<double>(runtime.rows.front().tick), static_cast<double>(runtime.rows.back().tick)};
const Range time_range{static_cast<double>(rows.front().tick), static_cast<double>(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<double>(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);
}
@@ -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
+1 -1
View File
@@ -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{};
+3 -3
View File
@@ -59,11 +59,11 @@ const std::vector<Case_Model>& 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;
}
+23 -32
View File
@@ -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<std::string>() + '\n';
for (const auto& control : contract.at("controls").at("descriptor").at("fields"))
api_text += control.at("presentation").at("description").get<std::string>() + '\n';
for (const auto& action : contract.at("actions").at("data"))
api_text += action.at("api").get<std::string>() + '\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<bool>();
alternative = !current_value.get<bool>();
} 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<double>(), 1e-9);
const double current = current_value;
const double step = std::max(published.at("multiple_of").get<double>(), 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<bool>())
const bool integer = published.at("value_type") == "integer";
if (integer)
value = std::round(value);
alternative = value;
alternative = integer ? nlohmann::json(static_cast<std::int64_t>(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(), [&current](const auto& option) {
return option.template get<std::string>() != current;
return option.at("value").template get<std::string>() != 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<std::string>() + "_live";
alternative = current_value.get<std::string>() + "_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<std::string>(), 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;