246 lines
14 KiB
C++
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;
|
|
}
|
|
}
|