Files
Renderive/render_2D/plottable/Overlays.cpp
T
2026-08-11 13:49:53 +08:00

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