Files
Aethera/render_2D/render_2D/axis/Abs_Axis.ipp
T
2026-08-21 10:16:16 +08:00

246 lines
14 KiB
C++

#pragma once
#include <algorithm>
#include <cmath>
#include <vector>
namespace aethera::render_2d {
struct Abs_Axis::Private : Prev_Private {
/*
* 派生轴 Private 必须继承 Prev_Private,并提供以下 CRTP 能力:
* Axis_Range coordinate_range(const T* object) const:从最终轴当前 State 返回权威坐标区间。
* double tick_step(const T* object, Axis_Range coordinate_range) const:返回主刻度步长。
* std::string tick_label(const T* object, double tick) const:返回主刻度显示文本。
* int sub_tick_count(const T* object, double major_step) const:返回次刻度数量;默认固定返回 4。
* 坐标映射直接读取 Abs_Axis::Base_Tag 状态并调用最终 Private 的 coordinate_range(...),不保存变换快照。
*/
using Coordinate_Range_Call = Axis_Range (*)(const Root*);
using Scalar_Call = double (*)(const Root*, double);
using Point_Call = double (*)(const Root*, Point_F);
using Range_Count_Call = int (*)(const Root*, Axis_Range);
using Range_Scalar_Call = double (*)(const Root*, Axis_Range);
using Count_Call = int (*)(const Root*, double);
using Label_Call = std::string (*)(const Root*, double);
struct Dispatch {
Coordinate_Range_Call coordinate_range; /* 读取最终轴权威坐标区间。 */
Scalar_Call coordinate_to_pixel; /* 坐标到像素的最终类型分派。 */
Scalar_Call pixel_to_coordinate; /* 像素到坐标的最终类型分派。 */
Point_Call point_to_coordinate; /* 二维点到坐标的最终类型分派。 */
Range_Count_Call pixel_sample_count; /* 像素样本数量的最终类型分派。 */
Range_Scalar_Call tick_step; /* 主刻度步长的最终类型分派。 */
Label_Call tick_label; /* 主刻度标签的最终类型分派。 */
Count_Call sub_tick_count; /* 次刻度数量的最终类型分派。 */
};
struct Prepared_Line {
Point_F first{}; /* 线段起点,单位为画布像素。 */
Point_F second{}; /* 线段终点,单位为画布像素。 */
};
struct Prepared_Label {
Point_F position{}; /* 标签左上角位置,单位为画布像素。 */
std::string text{}; /* 已按最终轴规则格式化的标签文本。 */
};
struct Prepared_Axis {
std::vector<Prepared_Line> lines{}; /* 本轮 Prepare 生成的轴线与刻度线。 */
std::vector<Prepared_Label> labels{}; /* 本轮 Prepare 生成的刻度标签。 */
Point_F unit_position{}; /* 单位文本左上角位置,单位为画布像素。 */
double unit_width{}; /* 单位文本背景的估算宽度,单位为像素。 */
bool valid{}; /* 本轮 Prepare 是否生成了可绘制内容。 */
};
const Dispatch* dispatch{}; /* Builder 绑定最终轴类型后指向静态分派表。 */
Prepared_Axis prepared{}; /* 由当前 State 推导、仅供紧随其后的 Paint 消费。 */
[[nodiscard]] Axis_Range coordinate_range(const Root* object) const;
[[nodiscard]] double coordinate_to_pixel(const Root* object, double coordinate) const;
[[nodiscard]] double pixel_to_coordinate(const Root* object, double pixel) const;
[[nodiscard]] double point_to_coordinate(const Root* object, Point_F point) const;
[[nodiscard]] int pixel_sample_count(const Root* object, Axis_Range coordinate_range) const;
[[nodiscard]] double tick_step(const Root* object, Axis_Range coordinate_range) const;
[[nodiscard]] std::string tick_label(const Root* object, double tick) const;
[[nodiscard]] int sub_tick_count(const Root* object, double major_step) const;
template <Axis_Object Object>
[[nodiscard]] static const Dispatch& dispatch_for();
/* 供派生轴复用的 1/2/5 十进制主刻度算法。 */
[[nodiscard]] static double nice_tick_step(Axis_Range coordinate_range);
/* 供派生轴复用的定点数值标签格式化算法。 */
[[nodiscard]] static std::string localized_number(double value, int precision, Number_Locale locale);
/* CRTP 实现:从最终轴当前 State 生成本轮 Paint 使用的线段与标签。 */
void prepare_data(Attached auto* object);
/* CRTP 实现:将本轮 Prepared_Axis 绘制到 Color_Cache 的写缓冲。 */
void paint(Attached auto* object);
/* CRTP Prop hook:任一轴属性变化都会使该轴的准备数据和颜色缓存失效。 */
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> props);
/* CRTP 默认:每两个主刻度之间生成 4 个次刻度。 */
[[nodiscard]] int sub_tick_count(const Attached auto* object, double major_step) const;
/* CRTP 覆盖:绑定 Renderable 机制和最终轴公开薄壳分派;派生 Private 必须先调用此实现。 */
template <Axis_Object Object>
void bind_private_crtp(Object* object);
};
template <Axis_Object Object>
const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() {
static const Dispatch result{
[](const Root* root) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
return private_data.coordinate_range(object);
},
[](const Root* root, double coordinate) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const Axis_Range range = private_data.coordinate_range(object);
const double coordinate_length = range.length();
const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal
? axis_state.position.x
: axis_state.position.y;
if (coordinate_length == 0.0) return pixel_origin;
return pixel_origin + (coordinate - range.origin) / coordinate_length * axis_state.pixel_length;
},
[](const Root* root, double pixel) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const Axis_Range range = private_data.coordinate_range(object);
if (axis_state.pixel_length == 0.0) return range.origin;
const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal
? axis_state.position.x
: axis_state.position.y;
return range.origin + (pixel - pixel_origin) / axis_state.pixel_length * range.length();
},
[](const Root* root, Point_F point) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const double pixel = axis_state.orientation == Axis_Orientation::horizontal ? point.x : point.y;
const Axis_Range range = private_data.coordinate_range(object);
if (axis_state.pixel_length == 0.0) return range.origin;
const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal
? axis_state.position.x
: axis_state.position.y;
return range.origin + (pixel - pixel_origin) / axis_state.pixel_length * range.length();
},
[](const Root* root, Axis_Range coordinate_range) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const Axis_Range range = private_data.coordinate_range(object);
const auto map = [&](double coordinate) {
const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal
? axis_state.position.x
: axis_state.position.y;
if (range.length() == 0.0) return pixel_origin;
return pixel_origin + (coordinate - range.origin) / range.length() * axis_state.pixel_length;
};
const double first_pixel = map(coordinate_range.origin);
const double last_pixel = map(coordinate_range.target);
return std::max(0, static_cast<int>(std::abs(last_pixel - first_pixel)) + 1);
},
[](const Root* root, Axis_Range coordinate_range) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
return private_data.tick_step(object, coordinate_range);
},
[](const Root* root, double tick) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
return private_data.tick_label(object, tick);
},
[](const Root* root, double major_step) {
auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
return private_data.sub_tick_count(object, major_step);
}
};
return result;
}
template <Axis_Object Object>
void Abs_Axis::Private::bind_private_crtp(Object* object) {
Prev_Private::bind_private_crtp(object);
dispatch = &Private::dispatch_for<Object>();
}
inline void Abs_Axis::Private::prepare_data(Attached auto* object) {
using Object = std::remove_pointer_t<decltype(object)>;
auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*private_data.current);
auto& output = prepared;
output = {};
if (state.pixel_length == 0.0 || state.canvas_size.empty()) return;
const Axis_Range coordinates = private_data.coordinate_range(object);
const double step = private_data.tick_step(object, coordinates);
if (!(step > 0.0) || !std::isfinite(step)) return;
const Point_F first = state.position;
const Point_F last = state.orientation == Axis_Orientation::horizontal
? Point_F{first.x + state.pixel_length, first.y}
: Point_F{first.x, first.y + state.pixel_length};
output.lines.push_back({first, last});
const auto [low, high] = std::minmax(coordinates.origin, coordinates.target);
const double initial = std::ceil(low / step) * step;
for (int tick_index = 0; tick_index < 1000; ++tick_index) {
const double tick = initial + tick_index * step;
if (tick > high + step * 1e-6) break;
const double pixel = coordinate_to_pixel(object, tick);
Point_F tick_start{};
Point_F tick_end{};
Point_F label{};
if (state.orientation == Axis_Orientation::horizontal) {
tick_start = {pixel, state.position.y};
tick_end = {pixel, state.position.y + state.tick_length};
label = {pixel + 2.0, state.position.y + state.tick_length + 2.0};
}
else {
tick_start = {state.position.x, pixel};
tick_end = {state.position.x + state.tick_length, pixel};
label = {state.position.x + state.tick_length + 2.0, pixel - 7.0};
}
output.lines.push_back({tick_start, tick_end});
output.labels.push_back({label, private_data.tick_label(object, tick)});
const int subdivisions = std::max(0, private_data.sub_tick_count(object, 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 = coordinate_to_pixel(object, sub_tick);
if (state.orientation == Axis_Orientation::horizontal)
output.lines.push_back({
{sub_pixel, state.position.y},
{sub_pixel, state.position.y + state.sub_tick_length}
});
else
output.lines.push_back({
{state.position.x, sub_pixel},
{state.position.x + state.sub_tick_length, sub_pixel}
});
}
}
if (!state.unit_text.empty()) {
output.unit_position = {last.x + 4.0, last.y + 4.0};
output.unit_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65);
}
output.valid = true;
object->template mark_dirty<Paint_Tag>();
}
inline void Abs_Axis::Private::paint(Attached auto* object) {
using Object = std::remove_pointer_t<decltype(object)>;
auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const Prop&>(*private_data.current);
auto& cache = private_data.paint_surface();
if (!prepared.valid) return;
detail::Painter painter(cache, state.canvas_size);
for (const auto& line : prepared.lines) painter.line(line.first, line.second, state.axis_pen);
for (const auto& label : prepared.labels)
painter.text(label.position, label.text, state.unit_text_font, state.unit_text_pen,
state.label_rotation_degrees);
if (!state.unit_text.empty()) {
painter.rect({
prepared.unit_position.x - 2.0, prepared.unit_position.y - 2.0,
prepared.unit_width + 4.0, state.unit_text_font.size * 1.5 + 4.0
},
Pen{.style = Line_Style::none}, state.unit_text_background_brush);
painter.text(prepared.unit_position, state.unit_text, state.unit_text_font, state.unit_text_pen);
}
}
template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Abs_Axis::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
object->template mark_dirty<Prepare_Data_Tag>();
}
inline int Abs_Axis::Private::sub_tick_count(const Attached auto*, double) const {
return 4;
}
}