#include "Plottables.h" #include "Curve_Sampling.h" #include "../plot/Plot_Core.h" #include "../render/Blend2D_Cache.h" #include #include #include #include namespace renderive { namespace { RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; } } // namespace Sweep_Spectrum::Sweep_Spectrum(Plot_Core& plot, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : Renderable(plot) { state_.frequency_axis = std::move(frequency_axis); state_.power_axis = std::move(power_axis); } void Sweep_Spectrum::append_block(std::span values) { { std::lock_guard lock(mutex_); state_.data.emplace_back(values.begin(), values.end()); if (state_.bins <= 0) state_.bins = static_cast(values.size()); const int limit = std::max(1, state_.blocks); while (state_.data.size() > static_cast(limit)) state_.data.pop_front(); } changed(); } void Sweep_Spectrum::append_block(std::pmr::vector&& values) { append_block(std::span(values.data(), values.size())); } std::shared_ptr Sweep_Spectrum::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } std::shared_ptr Sweep_Spectrum::power_axis() const { std::lock_guard lock(mutex_); return state_.power_axis; } #define RENDERIVE_SWEEP_PROPERTY(Type, Method, Field) \ Type Sweep_Spectrum::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ void Sweep_Spectrum::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } RENDERIVE_SWEEP_PROPERTY(Range, frequency_range, frequency_range) RENDERIVE_SWEEP_PROPERTY(Pen, pen, pen) RENDERIVE_SWEEP_PROPERTY(Pen, cur_frequency_pen, current_pen) RENDERIVE_SWEEP_PROPERTY(bool, visible_range_only, visible_only) RENDERIVE_SWEEP_PROPERTY(Line_Interpolation_Mode, interpolation_mode, interpolation) #undef RENDERIVE_SWEEP_PROPERTY int Sweep_Spectrum::bins_per_block() const { std::lock_guard lock(mutex_); return state_.bins; } void Sweep_Spectrum::set_bins_per_block(int value) { { std::lock_guard lock(mutex_); state_.bins = std::max(0, value); } changed(); } int Sweep_Spectrum::block_count() const { std::lock_guard lock(mutex_); return state_.blocks; } void Sweep_Spectrum::set_block_count(int value) { { std::lock_guard lock(mutex_); state_.blocks = std::max(1, value); while (state_.data.size() > static_cast(state_.blocks)) state_.data.pop_front(); } changed(); } std::size_t Sweep_Spectrum::stored_block_count() const { std::lock_guard lock(mutex_); return state_.data.size(); } std::size_t Sweep_Spectrum::stored_point_count() const { std::lock_guard lock(mutex_); std::size_t count{}; for (const auto& block : state_.data) count += block.size(); return count; } std::size_t Sweep_Spectrum::rendered_point_count() const { const State state = state_snapshot(); if (!state.frequency_axis || !state.power_axis) return 0; std::vector values; for (const auto& block : state.data) values.insert(values.end(), block.begin(), block.end()); return detail::curve_points(values, state.frequency_range, state.frequency_axis->transform(), state.power_axis->transform(), state.visible_only, state.interpolation) .size(); } Sweep_Spectrum::State Sweep_Spectrum::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } void Sweep_Spectrum::paint(detail::Painter& painter) { const State state = state_snapshot(); if (!state.frequency_axis || !state.power_axis || state.data.empty()) return; std::vector values; for (const auto& block : state.data) values.insert(values.end(), block.begin(), block.end()); if (values.size() < 2) return; const Axis_Transform x = state.frequency_axis->transform(); const Axis_Transform y = state.power_axis->transform(); auto points = detail::curve_points(values, state.frequency_range, x, y, state.visible_only, state.interpolation); painter.polyline(points, state.pen); const double completed = state.blocks > 0 ? std::min(1.0, static_cast(state.data.size()) / state.blocks) : 1.0; const double frequency = state.frequency_range.origin + state.frequency_range.length() * completed; const RectF content = axis_content_rect(x, y); const double marker_x = x.coord_to_pixel(frequency); painter.line({marker_x, content.y}, {marker_x, content.bottom()}, state.current_pen); } Sweep_Spectrum::Builder::Builder(std::shared_ptr parent, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), power_axis_(std::move(power_axis)) {} std::shared_ptr Sweep_Spectrum::Builder::build() { if (!parent_ || !frequency_axis_ || !power_axis_) return {}; auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); result->set_frequency_range(frequency_range_); result->set_bins_per_block(bins_); result->set_block_count(blocks_); result->set_pen(pen_); result->set_cur_frequency_pen(current_pen_); result->set_visible_range_only(visible_only_); result->set_interpolation_mode(interpolation_); return result; } Selection_Rectangle_Overlay::Selection_Rectangle_Overlay( Plot_Core& plot, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis) : Renderable(plot), horizontal_axis_(std::move(horizontal_axis)), vertical_axis_(std::move(vertical_axis)) {} void Selection_Rectangle_Overlay::set_horizontal_axis(std::shared_ptr axis) { { std::lock_guard lock(mutex_); horizontal_axis_ = std::move(axis); } changed(); } void Selection_Rectangle_Overlay::set_vertical_axis(std::shared_ptr axis) { { std::lock_guard lock(mutex_); vertical_axis_ = std::move(axis); } changed(); } #define RENDERIVE_SELECTION_PROPERTY(Type, Method, Field) \ Type Selection_Rectangle_Overlay::Method() const { std::lock_guard lock(mutex_); return Field; } \ void Selection_Rectangle_Overlay::set_##Method(Type value) { { std::lock_guard lock(mutex_); Field = std::move(value); } changed(); } RENDERIVE_SELECTION_PROPERTY(Font, label_font, font_) RENDERIVE_SELECTION_PROPERTY(Pen, label_pen, label_pen_) RENDERIVE_SELECTION_PROPERTY(Brush, selection_brush, brush_) RENDERIVE_SELECTION_PROPERTY(Pen, selection_border_pen, border_) #undef RENDERIVE_SELECTION_PROPERTY std::vector Selection_Rectangle_Overlay::selected_regions() const { std::lock_guard lock(mutex_); return regions_; } void Selection_Rectangle_Overlay::clear_selected_regions() { { std::lock_guard lock(mutex_); regions_.clear(); selecting_ = false; } changed(); } void Selection_Rectangle_Overlay::handle_event(const Event& event) { std::shared_ptr horizontal; std::shared_ptr vertical; { std::lock_guard lock(mutex_); horizontal = horizontal_axis_; vertical = vertical_axis_; } if (!horizontal || !vertical) return; const RectF content = axis_content_rect(horizontal->transform(), vertical->transform()); if (event.type == Event_Type::Pointer_Press) { const auto& pointer = static_cast(event); if (pointer.button != Mouse_Button::Left || !content.contains(pointer.position)) return; { std::lock_guard lock(mutex_); selecting_ = true; selection_start_ = selection_current_ = pointer.position; } event.accept(); changed(); } else if (event.type == Event_Type::Pointer_Move) { const auto& pointer = static_cast(event); { std::lock_guard lock(mutex_); if (!selecting_) return; selection_current_ = pointer.position; } event.accept(); changed(); } else if (event.type == Event_Type::Pointer_Release) { const auto& pointer = static_cast(event); PointF start; { std::lock_guard lock(mutex_); if (!selecting_) return; selecting_ = false; start = selection_start_; selection_current_ = pointer.position; } const double x1 = horizontal->pixel_to_coord(start.x); const double x2 = horizontal->pixel_to_coord(pointer.position.x); const double y1 = vertical->pixel_to_coord(start.y); const double y2 = vertical->pixel_to_coord(pointer.position.y); const RectF region{x1, y1, x2 - x1, y2 - y1}; if (std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9) { std::lock_guard lock(mutex_); regions_.push_back(region.normalized()); } event.accept(); changed(); } } void Selection_Rectangle_Overlay::paint(detail::Painter& painter) { std::shared_ptr horizontal; std::shared_ptr vertical; std::vector regions; Font font; Pen label_pen; Brush brush; Pen border; bool selecting{}; PointF start; PointF current; { std::lock_guard lock(mutex_); horizontal = horizontal_axis_; vertical = vertical_axis_; regions = regions_; font = font_; label_pen = label_pen_; brush = brush_; border = border_; selecting = selecting_; start = selection_start_; current = selection_current_; } if (!horizontal || !vertical) return; for (const RectF& region : regions) { const double x1 = horizontal->coord_to_pixel(region.x); const double x2 = horizontal->coord_to_pixel(region.right()); const double y1 = vertical->coord_to_pixel(region.y); const double y2 = vertical->coord_to_pixel(region.bottom()); RectF pixels{x1, y1, x2 - x1, y2 - y1}; painter.rect(pixels, border, brush); std::ostringstream text; text << region.width << " x " << region.height; const RectF normalized = pixels.normalized(); painter.text({normalized.x + 3.0, normalized.y + 3.0}, text.str(), font, label_pen); } if (selecting) painter.rect({start.x, start.y, current.x - start.x, current.y - start.y}, border, brush); } Selection_Rectangle_Overlay::Builder::Builder( std::shared_ptr parent, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis) : parent_(std::move(parent)), horizontal_axis_(std::move(horizontal_axis)), vertical_axis_(std::move(vertical_axis)) {} std::shared_ptr Selection_Rectangle_Overlay::Builder::build() { if (!parent_ || !horizontal_axis_ || !vertical_axis_) return {}; auto result = parent_->plot().make_renderable( parent_, horizontal_axis_, vertical_axis_); result->set_label_font(font_); result->set_label_pen(label_pen_); result->set_selection_brush(brush_); result->set_selection_border_pen(border_); return result; } Constellation_Diagram::Constellation_Diagram(Plot_Core& plot, std::shared_ptr i_axis, std::shared_ptr q_axis) : Renderable(plot) { state_.i_axis = std::move(i_axis); state_.q_axis = std::move(q_axis); } void Constellation_Diagram::append_point(PointF point) { const auto now = std::chrono::steady_clock::now(); { std::lock_guard lock(mutex_); state_.points.push_back({point, now}); const auto cutoff = now - std::chrono::milliseconds(std::max(0, state_.lifetime_ms)); while (!state_.points.empty() && state_.points.front().time < cutoff) state_.points.pop_front(); } changed(); } std::shared_ptr Constellation_Diagram::i_axis() const { std::lock_guard lock(mutex_); return state_.i_axis; } std::shared_ptr Constellation_Diagram::q_axis() const { std::lock_guard lock(mutex_); return state_.q_axis; } #define RENDERIVE_CONSTELLATION_PROPERTY(Type, Method, Field) \ Type Constellation_Diagram::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ void Constellation_Diagram::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } RENDERIVE_CONSTELLATION_PROPERTY(Range, i_range, i_range) RENDERIVE_CONSTELLATION_PROPERTY(Range, q_range, q_range) RENDERIVE_CONSTELLATION_PROPERTY(Color, point_color, point_color) RENDERIVE_CONSTELLATION_PROPERTY(Color, anchor_color, anchor_color) #undef RENDERIVE_CONSTELLATION_PROPERTY int Constellation_Diagram::point_lifetime_ms() const { std::lock_guard lock(mutex_); return state_.lifetime_ms; } void Constellation_Diagram::set_point_lifetime_ms(int value) { { std::lock_guard lock(mutex_); state_.lifetime_ms = std::max(0, value); } changed(); } std::size_t Constellation_Diagram::point_count() const { std::lock_guard lock(mutex_); return state_.points.size(); } void Constellation_Diagram::fit_square_to_axes() { auto horizontal = i_axis(); auto vertical = q_axis(); if (!horizontal || !vertical) return; const Range i = i_range(); const Range q = q_range(); const double side = std::max(i.size(), q.size()); horizontal->set_coord_range({i.center() - side * 0.5, i.center() + side * 0.5}); vertical->set_coord_range({q.center() + side * 0.5, q.center() - side * 0.5}); } Constellation_Diagram::State Constellation_Diagram::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } void Constellation_Diagram::paint(detail::Painter& painter) { const State state = state_snapshot(); if (!state.i_axis || !state.q_axis) return; const Axis_Transform x = state.i_axis->transform(); const Axis_Transform y = state.q_axis->transform(); const int count = static_cast(state.type); const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for (int index = 0; index < count; ++index) { const double angle = state.phase + 2.0 * std::numbers::pi * index / count; const PointF point{state.i_range.center() + std::cos(angle) * radius, state.q_range.center() + std::sin(angle) * radius}; painter.circle({x.coord_to_pixel(point.x), y.coord_to_pixel(point.y)}, 3.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::Solid}); } const auto cutoff = std::chrono::steady_clock::now() - std::chrono::milliseconds(std::max(0, state.lifetime_ms)); for (const Timed_Point& value : state.points) { if (value.time < cutoff) continue; painter.circle({x.coord_to_pixel(value.point.x), y.coord_to_pixel(value.point.y)}, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::Solid}); } } Constellation_Diagram::Builder::Builder(std::shared_ptr parent, std::shared_ptr i_axis, std::shared_ptr q_axis) : parent_(std::move(parent)), i_axis_(std::move(i_axis)), q_axis_(std::move(q_axis)) {} std::shared_ptr Constellation_Diagram::Builder::build() { if (!parent_ || !i_axis_ || !q_axis_) return {}; auto result = parent_->plot().make_renderable(parent_, i_axis_, q_axis_); result->set_i_range(i_range_); result->set_q_range(q_range_); result->set_point_color(point_color_); result->set_anchor_color(anchor_color_); result->set_point_lifetime_ms(lifetime_); { std::lock_guard lock(result->mutex_); result->state_.type = type_; result->state_.phase = phase_; } return result; } } // namespace renderive