157 lines
6.7 KiB
C++
157 lines
6.7 KiB
C++
#pragma once
|
|
|
|
#include "Abs_Axis.h"
|
|
#include "Axis_Format.h"
|
|
#include "../render/Blend2D_Cache.h"
|
|
|
|
#include <renderive/base/Atomic_Mutex.hpp>
|
|
#include <renderive/base/observer/Observer.hpp>
|
|
#include <renderive/state/Double_State_Strategy.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
namespace renderive::detail {
|
|
|
|
template <class State>
|
|
requires std::is_base_of_v<Axis_Base_Properties, State>
|
|
class Axis_State_Strategy
|
|
: public Double_State_Strategy<Abs_Axis, State, Atomic_Spin_Mutex,
|
|
Observer_State<Renderable_State_Observer>> {
|
|
using State_Observer = Observer_State<Renderable_State_Observer>;
|
|
using Base = Double_State_Strategy<Abs_Axis, State, Atomic_Spin_Mutex, State_Observer>;
|
|
|
|
public:
|
|
Axis_State_Strategy(Plot_Core& plot, State state)
|
|
: Base(state,
|
|
With_Observer<State_Observer>{State_Observer(Renderable_State_Observer(this))},
|
|
plot) {}
|
|
|
|
[[nodiscard]] Axis_Transform transform() const final {
|
|
return Base::read([this](const State& state) {
|
|
return make_transform(state, coordinate_range(state));
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] Axis_Transform transform(const Render_State_View& view) const noexcept final {
|
|
const auto& state = view.get(static_cast<const Base&>(*this));
|
|
return make_transform(state, coordinate_range(state));
|
|
}
|
|
|
|
[[nodiscard]] double tick_step(Range range) const final {
|
|
return Base::read([this, range](const State& state) {
|
|
return calculate_tick_step(range, state);
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] std::string tick_label(double tick) const final {
|
|
return Base::read([this, tick](const State& state) {
|
|
return format_tick_label(tick, state);
|
|
});
|
|
}
|
|
|
|
protected:
|
|
[[nodiscard]] virtual Range coordinate_range(const State& state) const noexcept = 0;
|
|
|
|
[[nodiscard]] virtual double calculate_tick_step(Range range, const State&) 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;
|
|
}
|
|
|
|
[[nodiscard]] virtual std::string format_tick_label(double tick, const State& state) const {
|
|
int precision = 2;
|
|
if constexpr (requires { state.precision; })
|
|
precision = state.precision.get();
|
|
return localized_axis_number(tick, precision, state.locale);
|
|
}
|
|
|
|
private:
|
|
static Axis_Transform make_transform(const State& state, Range coordinates) noexcept {
|
|
return {
|
|
coordinates,
|
|
state.orientation == Orientation::Horizontal ? static_cast<double>(state.x)
|
|
: static_cast<double>(state.y),
|
|
static_cast<double>(state.pixel_length)
|
|
};
|
|
}
|
|
|
|
void paint(Painter& painter, const Render_State_View& view) final {
|
|
const auto& state = view.get(static_cast<const Base&>(*this));
|
|
if (state.pixel_length == 0)
|
|
return;
|
|
|
|
const Range coordinates = coordinate_range(state);
|
|
const Axis_Transform axis = make_transform(state, coordinates);
|
|
const double step = calculate_tick_step(coordinates, state);
|
|
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 = axis.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, format_tick_label(tick, state), state.unit_text_font,
|
|
state.unit_text_pen, state.label_rotation_degrees);
|
|
const int subdivisions = std::max(0, this->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 = axis.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);
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace renderive::detail
|