435 lines
17 KiB
C++
435 lines
17 KiB
C++
#include "Plottables.h"
|
|
|
|
#include "Curve_Sampling.h"
|
|
#include "../plot/Plot_Core.h"
|
|
#include "../render/Blend2D_Cache.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <numbers>
|
|
#include <sstream>
|
|
|
|
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<Axis> frequency_axis,
|
|
std::shared_ptr<Axis> 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<const double> values) {
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
state_.data.emplace_back(values.begin(), values.end());
|
|
if (state_.bins <= 0)
|
|
state_.bins = static_cast<int>(values.size());
|
|
const int limit = std::max(1, state_.blocks);
|
|
while (state_.data.size() > static_cast<std::size_t>(limit))
|
|
state_.data.pop_front();
|
|
}
|
|
changed();
|
|
}
|
|
|
|
void Sweep_Spectrum::append_block(std::pmr::vector<double>&& values) {
|
|
append_block(std::span<const double>(values.data(), values.size()));
|
|
}
|
|
|
|
std::shared_ptr<Axis> Sweep_Spectrum::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; }
|
|
std::shared_ptr<Axis> 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<std::size_t>(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<double> 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<double> 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<double>(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<Renderable> parent,
|
|
std::shared_ptr<Axis> frequency_axis,
|
|
std::shared_ptr<Axis> power_axis)
|
|
: parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)),
|
|
power_axis_(std::move(power_axis)) {}
|
|
|
|
std::shared_ptr<Sweep_Spectrum> Sweep_Spectrum::Builder::build() {
|
|
if (!parent_ || !frequency_axis_ || !power_axis_)
|
|
return {};
|
|
auto result = parent_->plot().make_renderable<Sweep_Spectrum>(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<Abs_Axis> horizontal_axis,
|
|
std::shared_ptr<Abs_Axis> 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<Abs_Axis> axis) {
|
|
{ std::lock_guard lock(mutex_); horizontal_axis_ = std::move(axis); }
|
|
changed();
|
|
}
|
|
|
|
void Selection_Rectangle_Overlay::set_vertical_axis(std::shared_ptr<Abs_Axis> 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<RectF> 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<Abs_Axis> horizontal;
|
|
std::shared_ptr<Abs_Axis> 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<const Pointer_Event&>(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<const Pointer_Event&>(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<const Pointer_Event&>(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<Abs_Axis> horizontal;
|
|
std::shared_ptr<Abs_Axis> vertical;
|
|
std::vector<RectF> 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<Renderable> parent,
|
|
std::shared_ptr<Abs_Axis> horizontal_axis,
|
|
std::shared_ptr<Abs_Axis> vertical_axis)
|
|
: parent_(std::move(parent)), horizontal_axis_(std::move(horizontal_axis)),
|
|
vertical_axis_(std::move(vertical_axis)) {}
|
|
|
|
std::shared_ptr<Selection_Rectangle_Overlay> Selection_Rectangle_Overlay::Builder::build() {
|
|
if (!parent_ || !horizontal_axis_ || !vertical_axis_)
|
|
return {};
|
|
auto result = parent_->plot().make_renderable<Selection_Rectangle_Overlay>(
|
|
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<Axis> i_axis,
|
|
std::shared_ptr<Axis> 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<Axis> Constellation_Diagram::i_axis() const { std::lock_guard lock(mutex_); return state_.i_axis; }
|
|
std::shared_ptr<Axis> 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<int>(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<Renderable> parent,
|
|
std::shared_ptr<Axis> i_axis,
|
|
std::shared_ptr<Axis> q_axis)
|
|
: parent_(std::move(parent)), i_axis_(std::move(i_axis)), q_axis_(std::move(q_axis)) {}
|
|
|
|
std::shared_ptr<Constellation_Diagram> Constellation_Diagram::Builder::build() {
|
|
if (!parent_ || !i_axis_ || !q_axis_)
|
|
return {};
|
|
auto result = parent_->plot().make_renderable<Constellation_Diagram>(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
|