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

407 lines
17 KiB
C++

#include "Axis.h"
#include "../plot/Plot_Core.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <sstream>
namespace renderive {
namespace {
bool valid_range(Range range) {
return std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0;
}
std::string fixed_number(double value, int precision) {
std::ostringstream stream;
stream << std::fixed << std::setprecision(std::clamp(precision, 0, 12)) << value;
std::string result = stream.str();
if (result.find('.') != std::string::npos) {
while (!result.empty() && result.back() == '0')
result.pop_back();
if (!result.empty() && result.back() == '.')
result.pop_back();
}
return result;
}
std::string localized_number(double value, int precision, Number_Locale locale) {
std::string result = fixed_number(value, precision);
if (locale.decimal_point != '.')
std::replace(result.begin(), result.end(), '.', locale.decimal_point);
return result;
}
std::string formatted_time(Time_Of_Day time, std::string_view format) {
const auto total = time.milliseconds;
const int hours = static_cast<int>((total / 3'600'000) % 24);
const int minutes = static_cast<int>((total / 60'000) % 60);
const int seconds = static_cast<int>((total / 1'000) % 60);
const int milliseconds = static_cast<int>(total % 1'000);
const auto digits = [](int value, int width) {
std::ostringstream stream;
stream << std::setfill('0') << std::setw(width) << value;
return stream.str();
};
std::string result;
for (std::size_t index = 0; index < format.size();) {
const std::string_view rest = format.substr(index);
if (rest.starts_with("zzz")) {
result += digits(milliseconds, 3);
index += 3;
} else if (rest.starts_with("hh") || rest.starts_with("HH")) {
result += digits(hours, 2);
index += 2;
} else if (rest.starts_with("mm")) {
result += digits(minutes, 2);
index += 2;
} else if (rest.starts_with("ss")) {
result += digits(seconds, 2);
index += 2;
} else {
result.push_back(format[index++]);
}
}
return result;
}
} // namespace
Abs_Axis::Abs_Axis(Plot_Core& plot, Orientation orientation) : Renderable(plot, true) {
axis_state_.orientation = orientation;
}
Abs_Axis::~Abs_Axis() = default;
#define RENDERIVE_AXIS_PROPERTY(Type, Name) \
Type Abs_Axis::Name() const { std::lock_guard lock(axis_mutex_); return axis_state_.Name; } \
void Abs_Axis::set_##Name(Type value) { \
{ std::lock_guard lock(axis_mutex_); if (axis_state_.Name == value) return; axis_state_.Name = std::move(value); } \
changed(); \
}
RENDERIVE_AXIS_PROPERTY(int, x)
RENDERIVE_AXIS_PROPERTY(int, y)
RENDERIVE_AXIS_PROPERTY(Orientation, orientation)
RENDERIVE_AXIS_PROPERTY(std::size_t, pixel_length)
RENDERIVE_AXIS_PROPERTY(int, tick_length)
RENDERIVE_AXIS_PROPERTY(int, sub_tick_length)
RENDERIVE_AXIS_PROPERTY(Color, color)
RENDERIVE_AXIS_PROPERTY(Number_Locale, locale)
RENDERIVE_AXIS_PROPERTY(std::string, unit_text)
RENDERIVE_AXIS_PROPERTY(Font, unit_text_font)
RENDERIVE_AXIS_PROPERTY(Pen, unit_text_pen)
RENDERIVE_AXIS_PROPERTY(Brush, unit_text_background_brush)
RENDERIVE_AXIS_PROPERTY(int, label_rotation_degrees)
#undef RENDERIVE_AXIS_PROPERTY
Abs_Axis::State Abs_Axis::axis_state() const {
std::lock_guard lock(axis_mutex_);
return axis_state_;
}
Axis_Transform Abs_Axis::transform() const {
const State state = axis_state();
return {
coord_range(),
state.orientation == Orientation::Horizontal ? static_cast<double>(state.x)
: static_cast<double>(state.y),
static_cast<double>(state.pixel_length)
};
}
double Abs_Axis::pixel_to_coord(double pixel) const { return transform().pixel_to_coord(pixel); }
double Abs_Axis::coord_to_pixel(double coordinate) const { return transform().coord_to_pixel(coordinate); }
double Abs_Axis::start_coord() const { return coord_range().origin; }
double Abs_Axis::end_coord() const { return coord_range().target; }
int Abs_Axis::pixel_sample_count(Range range) const {
const Axis_Transform value = transform();
const double first = value.coord_to_pixel(range.origin);
const double last = value.coord_to_pixel(range.target);
return std::max(0, static_cast<int>(std::abs(last - first)) + 1);
}
int Abs_Axis::pixel_sample_count() const {
return static_cast<int>(pixel_length()) + (pixel_length() > 0 ? 1 : 0);
}
double Abs_Axis::tick_step(Range range) const {
const double raw = range.size() / 5.0;
if (!(raw > 0.0) || !std::isfinite(raw))
return 1.0;
const double scale = std::pow(10.0, std::floor(std::log10(raw)));
const double normalized = raw / scale;
const double nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 : normalized <= 5.0 ? 5.0 : 10.0;
return nice * scale;
}
int Abs_Axis::sub_tick_count(double) const { return 4; }
std::string Abs_Axis::tick_label(double tick) const {
return localized_number(tick, 2, locale());
}
void Abs_Axis::paint(detail::Painter& painter) {
const State state = axis_state();
if (state.pixel_length == 0)
return;
const Range coordinates = coord_range();
const double step = tick_step(coordinates);
if (!(step > 0.0))
return;
const Pen axis_pen{state.color, 1.0};
const PointF first{static_cast<double>(state.x), static_cast<double>(state.y)};
const PointF last = state.orientation == Orientation::Horizontal
? PointF{first.x + state.pixel_length, first.y}
: PointF{first.x, first.y + state.pixel_length};
painter.line(first, last, axis_pen);
const auto [low, high] = std::minmax(coordinates.origin, coordinates.target);
const double initial = std::ceil(low / step) * step;
int tick_index{};
for (double tick = initial; tick <= high + step * 1e-6 && tick_index < 1000;
tick += step, ++tick_index) {
const double pixel = coord_to_pixel(tick);
PointF tick_start;
PointF tick_end;
PointF label;
if (state.orientation == Orientation::Horizontal) {
tick_start = {pixel, static_cast<double>(state.y)};
tick_end = {pixel, static_cast<double>(state.y + state.tick_length)};
label = {pixel + 2.0, static_cast<double>(state.y + state.tick_length + 2)};
} else {
tick_start = {static_cast<double>(state.x), pixel};
tick_end = {static_cast<double>(state.x + state.tick_length), pixel};
label = {static_cast<double>(state.x + state.tick_length + 2), pixel - 7.0};
}
painter.line(tick_start, tick_end, axis_pen);
painter.text(label, tick_label(tick), state.unit_text_font, state.unit_text_pen,
state.label_rotation_degrees);
const int subdivisions = std::max(0, sub_tick_count(step));
for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) {
const double sub_tick = tick + step * sub_index / (subdivisions + 1.0);
if (sub_tick >= high)
break;
const double sub_pixel = coord_to_pixel(sub_tick);
if (state.orientation == Orientation::Horizontal) {
painter.line({sub_pixel, static_cast<double>(state.y)},
{sub_pixel, static_cast<double>(state.y + state.sub_tick_length)},
axis_pen);
} else {
painter.line({static_cast<double>(state.x), sub_pixel},
{static_cast<double>(state.x + state.sub_tick_length), sub_pixel},
axis_pen);
}
}
}
if (!state.unit_text.empty()) {
const PointF position{last.x + 4.0, last.y + 4.0};
const double estimated_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65);
painter.rect({position.x - 2.0, position.y - 2.0,
estimated_width + 4.0, state.unit_text_font.size * 1.5 + 4.0},
Pen{.style = Line_Style::None}, state.unit_text_background_brush);
painter.text(position, state.unit_text,
state.unit_text_font, state.unit_text_pen);
}
}
Axis::Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {}
Range Axis::coord_range() const { std::lock_guard lock(interaction_mutex_); return coordinates_; }
int Axis::label_precision() const { std::lock_guard lock(interaction_mutex_); return precision_; }
void Axis::set_label_precision(int value) { { std::lock_guard lock(interaction_mutex_); precision_ = std::clamp(value, 0, 12); } changed(); }
double Axis::coord_start() const { return coord_range().origin; }
void Axis::set_coord_start(double value) { auto range = coord_range(); set_coord_range({value, value + range.length()}); }
double Axis::coord_length() const { return coord_range().length(); }
void Axis::set_coord_length(double value) { auto range = coord_range(); set_coord_range({range.origin, range.origin + value}); }
void Axis::set_coord_range(Range range) {
if (!valid_range(range))
return;
{
std::lock_guard lock(interaction_mutex_);
if (coordinates_ == range)
return;
coordinates_ = range;
}
changed();
}
void Axis::set_use_wheel(bool value) { std::lock_guard lock(interaction_mutex_); wheel_enabled_ = value; }
void Axis::set_use_drag(bool value) { std::lock_guard lock(interaction_mutex_); drag_enabled_ = value; }
bool Axis::use_wheel() const { std::lock_guard lock(interaction_mutex_); return wheel_enabled_; }
bool Axis::use_drag() const { std::lock_guard lock(interaction_mutex_); return drag_enabled_; }
void Axis::handle_event(const Event& event) {
if (event.type == Event_Type::Wheel && use_wheel()) {
const auto& wheel = static_cast<const Wheel_Event&>(event);
Range range = coord_range();
const double anchor_pixel = orientation() == Orientation::Horizontal ? wheel.position.x : wheel.position.y;
const double anchor = pixel_to_coord(anchor_pixel);
const double factor = wheel.angle_delta_y >= 0.0 ? 0.9 : 1.1;
set_coord_range({anchor + (range.origin - anchor) * factor,
anchor + (range.target - anchor) * factor});
event.accept();
return;
}
if (!use_drag())
return;
if (event.type == Event_Type::Pointer_Press) {
const auto& pointer = static_cast<const Pointer_Event&>(event);
if (pointer.button == Mouse_Button::Left) {
std::lock_guard lock(interaction_mutex_);
dragging_ = true;
last_pointer_ = pointer.position;
event.accept();
}
} else if (event.type == Event_Type::Pointer_Move) {
const auto& pointer = static_cast<const Pointer_Event&>(event);
PointF previous;
{
std::lock_guard lock(interaction_mutex_);
if (!dragging_)
return;
previous = last_pointer_;
last_pointer_ = pointer.position;
}
const double delta = orientation() == Orientation::Horizontal
? pointer.position.x - previous.x
: pointer.position.y - previous.y;
Range range = coord_range();
const double shift = pixel_length() == 0 ? 0.0 : -delta * range.length() / pixel_length();
set_coord_range({range.origin + shift, range.target + shift});
event.accept();
} else if (event.type == Event_Type::Pointer_Release) {
std::lock_guard lock(interaction_mutex_);
if (dragging_) {
dragging_ = false;
event.accept();
}
}
}
std::string Axis::tick_label(double tick) const {
return localized_number(tick, label_precision(), locale());
}
Axis::Builder::Builder(std::shared_ptr<Renderable> parent, Orientation orientation)
: Axis_Builder_Base(std::move(parent), orientation) {}
std::shared_ptr<Axis> Axis::Builder::build() {
if (!parent_)
return {};
auto result = parent_->plot().make_renderable<Axis>(parent_, orientation_);
apply(*result);
result->set_coord_range(coordinates_);
result->set_label_precision(precision_);
result->set_use_wheel(wheel_);
result->set_use_drag(drag_);
return result;
}
Frequency_Axis::Frequency_Axis(Plot_Core& plot, Orientation orientation) : Axis(plot, orientation) {}
std::string Frequency_Axis::tick_label(double tick) const {
const double absolute = std::abs(tick);
if (absolute >= 1'000'000.0)
return localized_number(tick / 1'000'000.0, label_precision(), locale()) + " MHz";
if (absolute >= 1'000.0)
return localized_number(tick / 1'000.0, label_precision(), locale()) + " kHz";
return localized_number(tick, label_precision(), locale()) + " Hz";
}
Frequency_Axis::Builder::Builder(std::shared_ptr<Renderable> parent, Orientation orientation)
: Axis_Builder_Base(std::move(parent), orientation) {}
std::shared_ptr<Frequency_Axis> Frequency_Axis::Builder::build() {
if (!parent_)
return {};
auto result = parent_->plot().make_renderable<Frequency_Axis>(parent_, orientation_);
apply(*result);
result->set_coord_range(coordinates_);
result->set_label_precision(precision_);
result->set_use_wheel(wheel_);
result->set_use_drag(drag_);
return result;
}
Time_Axis::Time_Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {}
int Time_Axis::visible_time_point_count() const { std::lock_guard lock(time_mutex_); return time_state_.visible_count; }
void Time_Axis::set_visible_time_point_count(int value) { { std::lock_guard lock(time_mutex_); time_state_.visible_count = std::max(2, value); } changed(); }
int Time_Axis::tick_label_spacing_px() const { std::lock_guard lock(time_mutex_); return time_state_.tick_label_spacing_px; }
void Time_Axis::set_tick_label_spacing_px(int value) { { std::lock_guard lock(time_mutex_); time_state_.tick_label_spacing_px = std::max(0, value); } changed(); }
std::string Time_Axis::time_format() const { std::lock_guard lock(time_mutex_); return time_state_.format; }
void Time_Axis::set_time_format(std::string value) { { std::lock_guard lock(time_mutex_); time_state_.format = std::move(value); } changed(); }
Font Time_Axis::font() const { return unit_text_font(); }
void Time_Axis::set_font(Font value) { set_unit_text_font(value); }
bool Time_Axis::newest_at_axis_start() const { std::lock_guard lock(time_mutex_); return time_state_.newest_at_start; }
void Time_Axis::set_newest_at_axis_start(bool value) { { std::lock_guard lock(time_mutex_); time_state_.newest_at_start = value; } changed(); }
std::size_t Time_Axis::time_point_count() const { std::lock_guard lock(time_mutex_); return time_state_.samples.size(); }
int Time_Axis::append_time(Time_Of_Day time) {
int tick{};
{
std::lock_guard lock(time_mutex_);
tick = time_state_.next_tick++;
time_state_.samples.emplace_back(tick, time);
const auto limit = static_cast<std::size_t>(std::max(512, time_state_.visible_count * 4));
while (time_state_.samples.size() > limit)
time_state_.samples.pop_front();
}
changed();
return tick;
}
Time_Of_Day Time_Axis::tick_to_time(int tick) const {
std::lock_guard lock(time_mutex_);
auto iterator = std::find_if(time_state_.samples.begin(), time_state_.samples.end(),
[tick](const auto& value) { return value.first == tick; });
return iterator == time_state_.samples.end() ? Time_Of_Day{} : iterator->second;
}
Range Time_Axis::coord_range() const {
std::lock_guard lock(time_mutex_);
const int latest = std::max(1, time_state_.next_tick - 1);
const int earliest = std::max(0, latest - time_state_.visible_count + 1);
return time_state_.newest_at_start ? Range{static_cast<double>(latest), static_cast<double>(earliest)}
: Range{static_cast<double>(earliest), static_cast<double>(latest)};
}
double Time_Axis::tick_step(Range range) const {
const double available = static_cast<double>(pixel_length());
const double label_width = std::max(48.0, font().size * 7.0);
const double spacing = static_cast<double>(tick_label_spacing_px());
const double label_count = std::max(1.0, available / (label_width + spacing));
return std::max(1.0, std::ceil(range.size() / label_count));
}
std::string Time_Axis::tick_label(double tick) const {
const Time_Of_Day time = tick_to_time(static_cast<int>(std::llround(tick)));
if (!time.valid())
return {};
return formatted_time(time, time_format());
}
Time_Axis::Builder::Builder(std::shared_ptr<Renderable> parent, Orientation orientation)
: Axis_Builder_Base(std::move(parent), orientation) {}
std::shared_ptr<Time_Axis> Time_Axis::Builder::build() {
if (!parent_)
return {};
auto result = parent_->plot().make_renderable<Time_Axis>(parent_, orientation_);
apply(*result);
result->set_visible_time_point_count(visible_count_);
result->set_tick_label_spacing_px(spacing_);
result->set_time_format(format_);
result->set_font(font_);
result->set_newest_at_axis_start(newest_at_start_);
return result;
}
} // namespace renderive