420 lines
20 KiB
C++
420 lines
20 KiB
C++
#include "Spectrum.h"
|
|
#include "Curve_Sampling.h"
|
|
#include "Plottable_Real_Time_Data.h"
|
|
#include "../render/Blend2D_Cache.h"
|
|
#include "../renderable/Render_Partition.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
namespace renderive {
|
|
namespace detail {
|
|
namespace {
|
|
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_Interaction_State = Double_State_Strategy<Spectrum_Interaction_Base, Spectrum_Interaction>;
|
|
struct Spectrum_Render_Frame {
|
|
Adaptive_Render_Partitioner partitioner;
|
|
int active_partitions{1};
|
|
std::size_t work_size{};
|
|
bool valid{};
|
|
};
|
|
|
|
struct Curve_Partition {
|
|
std::span<const double> values;
|
|
Range domain;
|
|
Range clip_domain;
|
|
};
|
|
|
|
Curve_Partition curve_partition(std::span<const double> values, Range domain,
|
|
int partition_index, int partition_count) {
|
|
if (values.size() < 2 || partition_count <= 0)
|
|
return {};
|
|
const std::size_t segment_count = values.size() - 1;
|
|
const auto core = render_partition_range(segment_count, partition_index, partition_count);
|
|
if (core.first == core.last)
|
|
return {};
|
|
const std::size_t first = core.first == 0 ? 0 : core.first - 1;
|
|
const std::size_t last = std::min(segment_count, core.last + 1);
|
|
const auto coordinate = [domain, segment_count](std::size_t index) {
|
|
return domain.origin + domain.length() * static_cast<double>(index) /
|
|
static_cast<double>(segment_count);
|
|
};
|
|
return {
|
|
values.subspan(first, last - first + 1),
|
|
{coordinate(first), coordinate(last)},
|
|
{coordinate(core.first), coordinate(core.last)}
|
|
};
|
|
}
|
|
void draw_curve(Painter& painter, std::span<const double> values, Range domain,
|
|
const Axis_Transform& frequency_axis, const Axis_Transform& power_axis,
|
|
bool visible_only, Line_Interpolation_Mode interpolation,
|
|
const Pen& pen, const Brush& brush) {
|
|
auto points = curve_points(values, domain, frequency_axis, power_axis,
|
|
visible_only, interpolation);
|
|
if(points.size() < 2)
|
|
return;
|
|
if(brush.enabled()) {
|
|
std::vector<PointF> polygon;
|
|
polygon.reserve(points.size() + 2);
|
|
polygon.push_back(mapped_point(frequency_axis, domain.origin, power_axis,
|
|
power_axis.coordinate_range.target));
|
|
polygon.insert(polygon.end(), points.begin(), points.end());
|
|
polygon.push_back(mapped_point(frequency_axis, domain.target, power_axis,
|
|
power_axis.coordinate_range.target));
|
|
painter.polygon(polygon, Pen{.style = Line_Style::None}, brush);
|
|
}
|
|
painter.polyline(points, pen);
|
|
}
|
|
double spectrum_power_at(const Spectrum_Properties& properties, const Spectrum_Frame& frame, double frequency, bool& ok) {
|
|
ok = false;
|
|
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>(frame.samples.size() - 1);
|
|
const auto lower = static_cast<std::size_t>(std::floor(position));
|
|
const auto upper = std::min(lower + 1, frame.samples.size() - 1);
|
|
const double fraction = position - static_cast<double>(lower);
|
|
ok = true;
|
|
return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction;
|
|
}
|
|
}
|
|
struct Spectrum_Control::Impl {
|
|
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(owner) {}
|
|
std::shared_ptr<Frequency_Axis> frequency_axis;
|
|
std::shared_ptr<Axis> power_axis;
|
|
Plottable_Latest_Real_Time_Data<Spectrum_Frame> frame;
|
|
Spectrum_Interaction_State interaction;
|
|
std::mutex frame_update_mutex;
|
|
Spectrum_Render_Frame render_frame;
|
|
};
|
|
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>(*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()));
|
|
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 {
|
|
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 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();
|
|
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_->interaction.update([frequency](Spectrum_Interaction& interaction) { interaction.markers.push_back(frequency); });
|
|
changed();
|
|
}
|
|
void Spectrum_Control::remove_custom_marker(double frequency) {
|
|
bool removed{};
|
|
impl_->interaction.update([&](Spectrum_Interaction& interaction) {
|
|
if(interaction.markers.empty())
|
|
return;
|
|
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)
|
|
changed();
|
|
}
|
|
void Spectrum_Control::remove_selected_marker() {
|
|
bool removed{};
|
|
impl_->interaction.update([&](Spectrum_Interaction& interaction) {
|
|
if(interaction.selected_marker < 0 || interaction.selected_marker >= static_cast<int>(interaction.markers.size()))
|
|
return;
|
|
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_->interaction.update([](Spectrum_Interaction& interaction) {
|
|
interaction.markers.clear();
|
|
interaction.selected_marker = -1;
|
|
});
|
|
changed();
|
|
}
|
|
int Spectrum_Control::selectable_line_marker_count() const {
|
|
return impl_->interaction.read([](const Spectrum_Interaction& interaction) { return static_cast<int>(interaction.markers.size()); });
|
|
}
|
|
int Spectrum_Control::selected_marker_index() const {
|
|
return impl_->interaction.get<&Spectrum_Interaction::selected_marker>();
|
|
}
|
|
void Spectrum_Control::set_selected_marker_index(int index) {
|
|
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_->interaction.update([](Spectrum_Interaction& interaction) {
|
|
if(interaction.markers.empty())
|
|
interaction.selected_marker = -1;
|
|
else
|
|
interaction.selected_marker = (interaction.selected_marker + 1) % static_cast<int>(interaction.markers.size());
|
|
});
|
|
changed();
|
|
}
|
|
void Spectrum_Control::select_previous_marker() {
|
|
impl_->interaction.update([](Spectrum_Interaction& interaction) {
|
|
if(interaction.markers.empty())
|
|
interaction.selected_marker = -1;
|
|
else
|
|
interaction.selected_marker = (interaction.selected_marker <= 0 ? static_cast<int>(interaction.markers.size()) : interaction.selected_marker) - 1;
|
|
});
|
|
changed();
|
|
}
|
|
void Spectrum_Control::clear_marker_selection() {
|
|
set_selected_marker_index(-1);
|
|
}
|
|
double Spectrum_Control::marker_frequency(int index) const {
|
|
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_->interaction.update([&](Spectrum_Interaction& interaction) {
|
|
if(index < 0 || index >= static_cast<int>(interaction.markers.size()))
|
|
return;
|
|
interaction.markers[index] = frequency;
|
|
updated = true;
|
|
});
|
|
if(updated)
|
|
changed();
|
|
}
|
|
void Spectrum_Control::set_current_marker_frequency(double frequency) {
|
|
const int index = selected_marker_index();
|
|
if(index >= 0)
|
|
set_marker_frequency(index, frequency);
|
|
}
|
|
void Spectrum_Control::handle_event(const Event& event) {
|
|
bool updated{};
|
|
impl_->interaction.update([&](Spectrum_Interaction& interaction) { updated = update_hover_tooltip(interaction.tooltip, event); });
|
|
if(updated)
|
|
changed();
|
|
}
|
|
void Spectrum_Control::publish() {
|
|
publish_properties();
|
|
impl_->interaction.publish();
|
|
}
|
|
|
|
void Spectrum_Control::build_paint_task_graph(Renderable_Task_Graph& graph) {
|
|
auto& output = impl_->render_frame;
|
|
const auto view = render_state_view();
|
|
const auto& state = render_properties(view);
|
|
const auto& published_frame = view.get(impl_->frame);
|
|
const std::size_t work_size = published_frame ? published_frame->samples.size() : 0;
|
|
const int partition_count = output.partitioner.graph_partition_count(
|
|
state.partition_mode, state.partition_count.get(),
|
|
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 128);
|
|
const auto prepare = add_paint_task(
|
|
graph, "prepare spectrum and paint background",
|
|
[this, partition_count](Painter& painter, const Render_State_View& frame_view,
|
|
const Scene_Render_Context&) {
|
|
prepare_render_frame(frame_view, partition_count);
|
|
paint_background(painter, frame_view);
|
|
});
|
|
std::vector<Renderable_Task_Graph::Task> partitions;
|
|
partitions.reserve(static_cast<std::size_t>(partition_count));
|
|
for (int index = 0; index < partition_count; ++index) {
|
|
const auto partition = add_paint_task(
|
|
graph, "paint spectrum partition " + std::to_string(index + 1),
|
|
[this, index](Painter& painter, const Render_State_View& frame_view,
|
|
const Scene_Render_Context&) {
|
|
render_partition(painter, frame_view, index);
|
|
});
|
|
graph.precede(prepare, partition);
|
|
partitions.push_back(partition);
|
|
}
|
|
const auto overlay = add_paint_task(
|
|
graph, "paint spectrum overlay",
|
|
[this](Painter& painter, const Render_State_View& frame_view,
|
|
const Scene_Render_Context& context) {
|
|
paint_overlay(painter, frame_view,
|
|
context.frame_control_state.next_refresh_interval_ns);
|
|
});
|
|
for (const auto task : partitions)
|
|
graph.precede(task, overlay);
|
|
}
|
|
|
|
void Spectrum_Control::prepare_render_frame(const Render_State_View& view,
|
|
int graph_partition_count) {
|
|
const auto& published_frame = view.get(impl_->frame);
|
|
auto& output = impl_->render_frame;
|
|
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
|
const Axis_Transform power_axis = impl_->power_axis->transform(view);
|
|
output.valid = axes_are_orthogonal(frequency_axis, power_axis);
|
|
output.work_size = published_frame ? published_frame->samples.size() : 0;
|
|
output.active_partitions = output.partitioner.begin(graph_partition_count,
|
|
output.work_size);
|
|
}
|
|
|
|
void Spectrum_Control::render_partition(Painter& painter, const Render_State_View& view,
|
|
int partition_index) {
|
|
auto& output = impl_->render_frame;
|
|
if (!output.valid || partition_index >= output.active_partitions)
|
|
return;
|
|
const auto& state = render_properties(view);
|
|
const auto& published_frame = view.get(impl_->frame);
|
|
if (!published_frame)
|
|
return;
|
|
const Spectrum_Frame& frame = *published_frame;
|
|
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
|
const Axis_Transform power_axis = impl_->power_axis->transform(view);
|
|
const auto current = curve_partition(frame.samples, state.frequency_range,
|
|
partition_index, output.active_partitions);
|
|
if (current.values.empty())
|
|
return;
|
|
|
|
painter.clip(mapped_rect(frequency_axis, power_axis, current.clip_domain,
|
|
power_axis.coordinate_range));
|
|
if (state.max_hold_visible) {
|
|
const auto maximum = curve_partition(frame.maxima, state.frequency_range,
|
|
partition_index, output.active_partitions);
|
|
draw_curve(painter, maximum.values, maximum.domain, frequency_axis, power_axis,
|
|
state.visible_range_only, state.interpolation_mode,
|
|
state.max_pen, state.max_brush);
|
|
}
|
|
if (state.min_hold_visible) {
|
|
const auto minimum = curve_partition(frame.minima, state.frequency_range,
|
|
partition_index, output.active_partitions);
|
|
draw_curve(painter, minimum.values, minimum.domain, frequency_axis, power_axis,
|
|
state.visible_range_only, state.interpolation_mode,
|
|
state.min_pen, state.min_brush);
|
|
}
|
|
draw_curve(painter, current.values, current.domain, frequency_axis, power_axis,
|
|
state.visible_range_only, state.interpolation_mode,
|
|
state.current_pen, state.current_brush);
|
|
}
|
|
|
|
void Spectrum_Control::paint_background(Painter& painter, const Render_State_View& view) {
|
|
const auto& state = render_properties(view);
|
|
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
|
const Axis_Transform power_axis = impl_->power_axis->transform(view);
|
|
const RectF content = mapped_rect(frequency_axis, power_axis,
|
|
frequency_axis.coordinate_range,
|
|
power_axis.coordinate_range);
|
|
if (content.empty())
|
|
return;
|
|
if(state.sweep_region_visible) {
|
|
painter.rect(mapped_rect(frequency_axis, power_axis, state.sweep_frequency_range,
|
|
power_axis.coordinate_range),
|
|
Pen{.style = Line_Style::None}, state.sweep_region_brush);
|
|
}
|
|
}
|
|
|
|
void Spectrum_Control::paint_overlay(Painter& painter, const Render_State_View& view,
|
|
std::uint64_t frame_interval_ns) {
|
|
const auto& state = render_properties(view);
|
|
const auto& published_frame = view.get(impl_->frame);
|
|
const Spectrum_Frame empty_frame;
|
|
const Spectrum_Frame& frame = published_frame ? *published_frame : empty_frame;
|
|
const auto& interaction = view.get(impl_->interaction);
|
|
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
|
|
const Axis_Transform power_axis = impl_->power_axis->transform(view);
|
|
const RectF content = mapped_rect(frequency_axis, power_axis,
|
|
frequency_axis.coordinate_range,
|
|
power_axis.coordinate_range);
|
|
if (content.empty())
|
|
return;
|
|
auto& output = impl_->render_frame;
|
|
if(state.middle_frequency_pen.enabled()) {
|
|
painter.line(mapped_point(frequency_axis, state.center_frequency, power_axis,
|
|
power_axis.coordinate_range.origin),
|
|
mapped_point(frequency_axis, state.center_frequency, power_axis,
|
|
power_axis.coordinate_range.target),
|
|
state.middle_frequency_pen);
|
|
}
|
|
for(std::size_t index = 0; index < interaction.markers.size(); ++index) {
|
|
painter.line(mapped_point(frequency_axis, interaction.markers[index], power_axis,
|
|
power_axis.coordinate_range.origin),
|
|
mapped_point(frequency_axis, interaction.markers[index], power_axis,
|
|
power_axis.coordinate_range.target),
|
|
static_cast<int>(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen);
|
|
}
|
|
if(!frame.samples.empty() && (state.max_marker_visible || state.use_min_marker)) {
|
|
const auto draw_extreme = [&](bool maximum) {
|
|
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 = mapped_point(frequency_axis, frequency, power_axis, *iterator);
|
|
const Pen& pen = maximum ? state.max_pen : state.min_pen;
|
|
painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid});
|
|
};
|
|
if(state.max_marker_visible)
|
|
draw_extreme(true);
|
|
if(state.use_min_marker)
|
|
draw_extreme(false);
|
|
}
|
|
if(state.tooltip_enabled && interaction.tooltip.active && content.contains(interaction.tooltip.position)) {
|
|
const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position);
|
|
bool 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{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);
|
|
}
|
|
}
|
|
if (output.partitioner.finish(
|
|
state.partition_mode, output.active_partitions, frame_interval_ns,
|
|
static_cast<int>(Scene_Base::task_executor_worker_count()),
|
|
output.work_size, 128))
|
|
task_graph_changed();
|
|
}
|
|
|
|
void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
|
|
prepare_render_frame(view, 1);
|
|
paint_background(painter, view);
|
|
render_partition(painter, view, 0);
|
|
paint_overlay(painter, view, 0);
|
|
}
|
|
}
|
|
}
|