#include "Plottables.h" #include "Curve_Sampling.h" #include "../plot/Plot_Core.h" #include "../render/Blend2D_Cache.h" #include #include #include #include namespace renderive { namespace { RectF 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); const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; } void draw_curve(detail::Painter& painter, std::span values, Range domain, const Axis_Transform& x_axis, const Axis_Transform& y_axis, bool visible_only, Line_Interpolation_Mode interpolation, const Pen& pen, const Brush& brush) { auto points = detail::curve_points(values, domain, x_axis, y_axis, visible_only, interpolation); if (points.size() < 2) return; if (brush.enabled()) { std::vector polygon; polygon.reserve(points.size() + 2); polygon.push_back({points.front().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)}); polygon.insert(polygon.end(), points.begin(), points.end()); polygon.push_back({points.back().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)}); painter.polygon(polygon, Pen{.style = Line_Style::None}, brush); } painter.polyline(points, pen); } } // namespace Spectrum::Spectrum(Plot_Core& plot, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : Renderable(plot), state_{} { state_.frequency_axis = std::move(frequency_axis); state_.power_axis = std::move(power_axis); } #define RENDERIVE_SPECTRUM_PROPERTY(Type, Method, Field) \ Type Spectrum::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ void Spectrum::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } RENDERIVE_SPECTRUM_PROPERTY(std::shared_ptr, frequency_axis, frequency_axis) RENDERIVE_SPECTRUM_PROPERTY(std::shared_ptr, power_axis, power_axis) RENDERIVE_SPECTRUM_PROPERTY(Range, frequency_range, frequency_range) RENDERIVE_SPECTRUM_PROPERTY(double, center_frequency, center_frequency) RENDERIVE_SPECTRUM_PROPERTY(Range, sweep_frequency_range, sweep_range) RENDERIVE_SPECTRUM_PROPERTY(bool, max_hold_visible, max_hold) RENDERIVE_SPECTRUM_PROPERTY(bool, min_hold_visible, min_hold) RENDERIVE_SPECTRUM_PROPERTY(bool, max_marker_visible, max_marker) RENDERIVE_SPECTRUM_PROPERTY(bool, use_min_marker, min_marker) RENDERIVE_SPECTRUM_PROPERTY(bool, sweep_region_visible, sweep_visible) RENDERIVE_SPECTRUM_PROPERTY(bool, visible_range_only, visible_only) RENDERIVE_SPECTRUM_PROPERTY(Line_Interpolation_Mode, interpolation_mode, interpolation) RENDERIVE_SPECTRUM_PROPERTY(Brush, max_brush, max_brush) RENDERIVE_SPECTRUM_PROPERTY(Brush, current_brush, current_brush) RENDERIVE_SPECTRUM_PROPERTY(Brush, min_brush, min_brush) RENDERIVE_SPECTRUM_PROPERTY(Pen, max_pen, max_pen) RENDERIVE_SPECTRUM_PROPERTY(Pen, current_pen, current_pen) RENDERIVE_SPECTRUM_PROPERTY(Pen, min_pen, min_pen) RENDERIVE_SPECTRUM_PROPERTY(Pen, selected_marker_pen, selected_marker_pen) RENDERIVE_SPECTRUM_PROPERTY(Pen, marker_pen, marker_pen) RENDERIVE_SPECTRUM_PROPERTY(Pen, middle_frequency_pen, middle_pen) RENDERIVE_SPECTRUM_PROPERTY(Brush, sweep_region_brush, sweep_brush) #undef RENDERIVE_SPECTRUM_PROPERTY int Spectrum::frequency_point_size() const { std::lock_guard lock(mutex_); return state_.point_count; } void Spectrum::set_frequency_point_size(int value) { { std::lock_guard lock(mutex_); state_.point_count = std::max(0, value); } changed(); } void Spectrum::update_samples(std::span values) { { std::lock_guard lock(mutex_); state_.samples.assign(values.begin(), values.end()); if (state_.point_count <= 0) state_.point_count = static_cast(values.size()); if (state_.maxima.size() != values.size()) state_.maxima.assign(values.begin(), values.end()); else for (std::size_t index = 0; index < values.size(); ++index) state_.maxima[index] = std::max(state_.maxima[index], values[index]); if (state_.minima.size() != values.size()) state_.minima.assign(values.begin(), values.end()); else for (std::size_t index = 0; index < values.size(); ++index) state_.minima[index] = std::min(state_.minima[index], values[index]); } changed(); } void Spectrum::update_samples(std::pmr::vector&& values) { update_samples(std::span(values.data(), values.size())); } std::size_t Spectrum::sample_count() const { std::lock_guard lock(mutex_); return state_.samples.size(); } std::size_t Spectrum::rendered_point_count() const { const State state = state_snapshot(); if (!state.frequency_axis || !state.power_axis) return 0; return detail::curve_points(state.samples, state.frequency_range, state.frequency_axis->transform(), state.power_axis->transform(), state.visible_only, state.interpolation) .size(); } double Spectrum::power_at(double frequency, bool& ok) const { std::lock_guard lock(mutex_); ok = false; if (state_.samples.empty() || !state_.frequency_range.contains(frequency) || state_.frequency_range.length() == 0.0) return 0.0; const double normalized = (frequency - state_.frequency_range.origin) / state_.frequency_range.length(); const double position = std::clamp(normalized, 0.0, 1.0) * static_cast(state_.samples.size() - 1); const auto lower = static_cast(std::floor(position)); const auto upper = std::min(lower + 1, state_.samples.size() - 1); const double fraction = position - static_cast(lower); ok = true; return state_.samples[lower] * (1.0 - fraction) + state_.samples[upper] * fraction; } void Spectrum::add_custom_marker(double frequency) { add_custom_line_marker(frequency); } void Spectrum::add_custom_line_marker(double frequency) { { std::lock_guard lock(mutex_); state_.markers.push_back(frequency); } changed(); } void Spectrum::remove_custom_marker(double frequency) { { std::lock_guard lock(mutex_); if (state_.markers.empty()) return; auto closest = std::min_element(state_.markers.begin(), state_.markers.end(), [frequency](double left, double right) { return std::abs(left - frequency) < std::abs(right - frequency); }); const int removed = static_cast(std::distance(state_.markers.begin(), closest)); state_.markers.erase(closest); if (state_.selected_marker == removed) state_.selected_marker = -1; else if (state_.selected_marker > removed) --state_.selected_marker; } changed(); } void Spectrum::remove_selected_marker() { { std::lock_guard lock(mutex_); if (state_.selected_marker < 0 || state_.selected_marker >= static_cast(state_.markers.size())) return; state_.markers.erase(state_.markers.begin() + state_.selected_marker); state_.selected_marker = -1; } changed(); } void Spectrum::clear_custom_markers() { { std::lock_guard lock(mutex_); state_.markers.clear(); state_.selected_marker = -1; } changed(); } int Spectrum::selectable_line_marker_count() const { std::lock_guard lock(mutex_); return static_cast(state_.markers.size()); } int Spectrum::selected_marker_index() const { std::lock_guard lock(mutex_); return state_.selected_marker; } void Spectrum::set_selected_marker_index(int index) { { std::lock_guard lock(mutex_); state_.selected_marker = index >= 0 && index < static_cast(state_.markers.size()) ? index : -1; } changed(); } void Spectrum::select_next_marker() { std::lock_guard lock(mutex_); if (state_.markers.empty()) state_.selected_marker = -1; else state_.selected_marker = (state_.selected_marker + 1) % static_cast(state_.markers.size()); invalidate_cache(); plot().notify_model_dirty(); } void Spectrum::select_previous_marker() { std::lock_guard lock(mutex_); if (state_.markers.empty()) state_.selected_marker = -1; else state_.selected_marker = (state_.selected_marker <= 0 ? static_cast(state_.markers.size()) : state_.selected_marker) - 1; invalidate_cache(); plot().notify_model_dirty(); } void Spectrum::clear_marker_selection() { set_selected_marker_index(-1); } double Spectrum::marker_frequency(int index) const { std::lock_guard lock(mutex_); return index >= 0 && index < static_cast(state_.markers.size()) ? state_.markers[index] : 0.0; } void Spectrum::set_marker_frequency(int index, double frequency) { { std::lock_guard lock(mutex_); if (index < 0 || index >= static_cast(state_.markers.size())) return; state_.markers[index] = frequency; } changed(); } void Spectrum::set_current_marker_frequency(double frequency) { const int index = selected_marker_index(); if (index >= 0) set_marker_frequency(index, frequency); } Spectrum::State Spectrum::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } void Spectrum::handle_event(const Event& event) { update_hover(event); } void Spectrum::paint(detail::Painter& painter) { const State state = state_snapshot(); if (!state.frequency_axis || !state.power_axis) return; const Axis_Transform horizontal = state.frequency_axis->transform(); const Axis_Transform vertical = state.power_axis->transform(); const RectF content = axes_rect(horizontal, vertical); if (state.sweep_visible) { const double first = horizontal.coord_to_pixel(state.sweep_range.origin); const double last = horizontal.coord_to_pixel(state.sweep_range.target); painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, Pen{.style = Line_Style::None}, state.sweep_brush); } if (state.max_hold) draw_curve(painter, state.maxima, state.frequency_range, horizontal, vertical, state.visible_only, state.interpolation, state.max_pen, state.max_brush); if (state.min_hold) draw_curve(painter, state.minima, state.frequency_range, horizontal, vertical, state.visible_only, state.interpolation, state.min_pen, state.min_brush); draw_curve(painter, state.samples, state.frequency_range, horizontal, vertical, state.visible_only, state.interpolation, state.current_pen, state.current_brush); if (state.middle_pen.enabled()) { const double x = horizontal.coord_to_pixel(state.center_frequency); painter.line({x, content.y}, {x, content.bottom()}, state.middle_pen); } for (std::size_t index = 0; index < state.markers.size(); ++index) { const double frequency = state.markers[index]; const double x = horizontal.coord_to_pixel(frequency); const Pen& pen = static_cast(index) == state.selected_marker ? state.selected_marker_pen : state.marker_pen; painter.line({x, content.y}, {x, content.bottom()}, pen); } if (!state.samples.empty() && (state.max_marker || state.min_marker)) { const auto draw_extreme = [&](bool maximum) { auto iterator = maximum ? std::max_element(state.samples.begin(), state.samples.end()) : std::min_element(state.samples.begin(), state.samples.end()); const std::size_t index = static_cast(std::distance(state.samples.begin(), iterator)); const double denominator = state.samples.size() > 1 ? state.samples.size() - 1.0 : 1.0; const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator; const PointF point{horizontal.coord_to_pixel(frequency), vertical.coord_to_pixel(*iterator)}; const Pen& pen = maximum ? state.max_pen : state.min_pen; painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid}); }; if (state.max_marker) draw_extreme(true); if (state.min_marker) draw_extreme(false); } const Hover_Tooltip_Snapshot tooltip = tooltip_snapshot(); if (tooltip.enabled && tooltip.active && content.contains(tooltip.position)) { const double frequency = horizontal.pixel_to_coord(tooltip.position.x); bool ok{}; const double power = power_at(frequency, ok); if (ok) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency << " Hz " << power; const RectF box{tooltip.position.x + 8.0, tooltip.position.y + 8.0, 170.0, 24.0}; painter.rect(box, Pen{tooltip.text_pen.color}, tooltip.background); painter.text({box.x + 4.0, box.y + 3.0}, text.str(), tooltip.font, tooltip.text_pen); } } } Spectrum::Builder::Builder(std::shared_ptr parent, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), power_axis_(std::move(power_axis)) {} std::shared_ptr Spectrum::Builder::build() { if (!parent_ || !frequency_axis_ || !power_axis_) return {}; auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); result->set_frequency_range(frequency_range_); result->set_frequency_point_size(point_count_); result->set_center_frequency(center_); result->set_sweep_frequency_range(sweep_range_); result->set_max_hold_visible(max_hold_); result->set_min_hold_visible(min_hold_); result->set_max_marker_visible(max_marker_); result->set_use_min_marker(min_marker_); result->set_sweep_region_visible(sweep_visible_); result->set_visible_range_only(visible_only_); result->set_interpolation_mode(interpolation_); return result; } } // namespace renderive