Files
ts-qt/components/charts/vector_field.cc

319 lines
11 KiB
C++

//
// Created by Codex on 2025/12/05.
//
#include "vector_field.hh"
#include <algorithm>
#include <cmath>
#include <vector>
#include <QLinearGradient>
#include <QPainter>
#include <QPainterPath>
#include <QPointF>
using creeper::vector_widget::internal::VectorPlot;
VectorPlot::VectorPlot() {
setBackground(Qt::transparent);
}
VectorPlot::~VectorPlot() = default;
void VectorPlot::load_theme_manager(creeper::ThemeManager& mgr) {
mgr.append_handler(this, [this](const creeper::ThemeManager& manager) {
scheme_ = manager.color_scheme();
apply_color_scheme();
if (initialized_) {
replot();
}
});
}
void VectorPlot::set_matrix_size(const QSize& size) {
if (size == matrix_size_) {
return;
}
matrix_size_ = size;
reset_plot();
}
void VectorPlot::set_data(const QVector<PointData>& data) {
data_points_ = data;
if (initialized_) {
update_vectors();
}
}
void VectorPlot::paintEvent(QPaintEvent* event) {
if (!initialized_) {
initialize_plot();
}
QCustomPlot::paintEvent(event);
// Custom glossy arrow rendering
const auto rect = axisRect();
if (!rect) {
return;
}
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
const QRectF plot_rect = rect->rect();
const double side_pad = plot_rect.width() * 0.24;
QRectF area = plot_rect.adjusted(side_pad, 8.0, -side_pad, -8.0);
const double cx_canvas = area.center().x();
const double cy_canvas = area.center().y();
const double scale = 0.72; // overall shrink to make it slimmer
area.setWidth(area.width() * scale);
area.setHeight(area.height() * scale);
area.moveCenter(QPointF(cx_canvas, cy_canvas));
if (area.width() <= 0.0 || area.height() <= 0.0) {
return;
}
const double w = area.width();
const double h = area.height();
const double cx = area.center().x();
const double shaft_w = w * 0.18;
const double shaft_r = shaft_w * 0.25;
const double head_h = h * 0.36;
const double head_w = w * 0.52;
const double shaft_h = h - head_h;
const double shaft_x = cx - shaft_w * 0.5;
const double shaft_y = area.top();
const double head_base_y = shaft_y + shaft_h - shaft_r * 0.4; // overlap slightly to avoid seam
QPainterPath shaft;
shaft.addRoundedRect(QRectF(shaft_x, shaft_y, shaft_w, shaft_h), shaft_r, shaft_r);
QPainterPath head;
head.moveTo(cx - head_w * 0.5, head_base_y);
head.lineTo(cx + head_w * 0.5, head_base_y);
head.lineTo(cx, area.bottom());
head.closeSubpath();
QPainterPath arrow = shaft.united(head);
// Vibrant orange palette (fixed)
QColor base_color(255, 140, 0);
QColor highlight = base_color.lighter(165);
QColor mid = base_color;
QColor shadow = base_color.darker(180);
QLinearGradient body_grad(area.topLeft(), QPointF(area.left(), area.bottom()));
body_grad.setColorAt(0.0, highlight);
body_grad.setColorAt(0.28, mid);
body_grad.setColorAt(0.72, base_color.darker(110));
body_grad.setColorAt(1.0, shadow);
const double angle_deg = std::atan2(arrow_dir_.y(), arrow_dir_.x()) * 180.0 / 3.14159265358979323846 - 90.0;
QTransform transform;
transform.translate(cx, (shaft_y + head_base_y + area.bottom()) / 3.0); // approximate center
transform.rotate(angle_deg);
transform.translate(-cx, -(shaft_y + head_base_y + area.bottom()) / 3.0);
auto draw_with_transform = [&](const QPainterPath& path, const QBrush& brush, const QPen* pen = nullptr) {
const QPainterPath rotated = transform.map(path);
painter.setBrush(brush);
if (pen) {
painter.setPen(*pen);
}
else {
painter.setPen(Qt::NoPen);
}
painter.drawPath(rotated);
};
draw_with_transform(arrow, QBrush(body_grad));
// Gloss highlight
QPainterPath gloss;
const double gloss_w = shaft_w * 0.42;
QRectF gloss_rect(cx - gloss_w * 0.5, shaft_y + h * 0.04, gloss_w, shaft_h * 0.5);
gloss.addRoundedRect(gloss_rect, gloss_w * 0.4, gloss_w * 0.4);
QLinearGradient gloss_grad(gloss_rect.topLeft(), gloss_rect.bottomLeft());
QColor gloss_hi = Qt::white;
gloss_hi.setAlpha(190);
QColor gloss_lo = Qt::white;
gloss_lo.setAlpha(40);
gloss_grad.setColorAt(0.0, gloss_hi);
gloss_grad.setColorAt(1.0, gloss_lo);
draw_with_transform(gloss, QBrush(gloss_grad));
// Head specular highlights
const double spec_w = head_w * 0.18;
QRectF spec_left(cx - head_w * 0.32, head_base_y + head_h * 0.08, spec_w, head_h * 0.2);
QRectF spec_right(cx + head_w * 0.14, head_base_y + head_h * 0.1, spec_w * 0.8, head_h * 0.2);
auto paint_spec = [&](const QRectF& r) {
QPainterPath p;
p.addRoundedRect(r, r.width() * 0.4, r.height() * 0.6);
QLinearGradient g(r.topLeft(), r.bottomLeft());
QColor hi = Qt::white;
hi.setAlpha(180);
QColor lo = Qt::white;
lo.setAlpha(30);
g.setColorAt(0.0, hi);
g.setColorAt(1.0, lo);
draw_with_transform(p, QBrush(g));
};
paint_spec(spec_left);
paint_spec(spec_right);
}
void VectorPlot::initialize_plot() {
if (initialized_) {
return;
}
xAxis->setVisible(false);
yAxis->setVisible(false);
xAxis->grid()->setPen(Qt::NoPen);
yAxis->grid()->setPen(Qt::NoPen);
xAxis->setRange(0.0, std::max(1, matrix_size_.width()));
yAxis->setRange(0.0, std::max(1, matrix_size_.height()));
xAxis->setSubTicks(true);
yAxis->setSubTicks(true);
xAxis->setTickLength(0);
yAxis->setTickLength(0);
xAxis->setSubTickLength(4);
yAxis->setSubTickLength(4);
initialized_ = true;
apply_color_scheme();
ensure_arrows();
update_vectors();
}
void VectorPlot::reset_plot() {
clearItems();
primary_arrow_ = nullptr;
initialized_ = false;
initialize_plot();
}
void VectorPlot::ensure_arrows() {
// no-op: legacy multi-arrow support removed
}
void VectorPlot::ensure_primary_arrow() {
if (primary_arrow_) {
return;
}
primary_arrow_ = new QCPItemLine(this);
primary_arrow_->start->setType(QCPItemPosition::ptPlotCoords);
primary_arrow_->end->setType(QCPItemPosition::ptPlotCoords);
primary_arrow_->setClipToAxisRect(true);
primary_arrow_->setClipAxisRect(axisRect());
primary_arrow_->setHead(QCPLineEnding(QCPLineEnding::esSpikeArrow, 14, 8));
primary_arrow_->setTail(QCPLineEnding(QCPLineEnding::esNone));
apply_color_scheme();
}
void VectorPlot::apply_color_scheme() {
QColor pen_color = arrow_color_;
if (scheme_.has_value() && scheme_->primary.isValid()) {
pen_color = scheme_->primary;
}
QPen strong_pen(pen_color);
strong_pen.setWidthF(8.0); // 稍细一点但仍然饱满
strong_pen.setCapStyle(Qt::FlatCap);
strong_pen.setJoinStyle(Qt::MiterJoin);
if (primary_arrow_) {
primary_arrow_->setPen(strong_pen);
primary_arrow_->setVisible(false); // 使用自绘 3D 箭头
}
}
void VectorPlot::update_vectors() {
if (!initialized_) {
return;
}
ensure_primary_arrow();
const int width = std::max(1, matrix_size_.width());
const int height = std::max(1, matrix_size_.height());
const int expected = width * height;
if (expected <= 0 || !primary_arrow_) {
replot();
return;
}
std::vector<double> values(static_cast<std::size_t>(expected), 0.0);
for (const auto& item: data_points_) {
const int x = static_cast<int>(item.x);
const int y = static_cast<int>(item.y);
if (x >= 0 && x < width && y >= 0 && y < height) {
const int idx = y * width + x;
values[static_cast<std::size_t>(idx)] = item.z;
}
}
auto value_at = [&](int x, int y) -> double {
if (x < 0 || x >= width || y < 0 || y >= height) {
return 0.0;
}
return values[static_cast<std::size_t>(y * width + x)];
};
std::vector<std::pair<double, double>> grads(static_cast<std::size_t>(expected), { 0.0, 0.0 });
double max_mag = 0.0;
double sum_gx = 0.0;
double sum_gy = 0.0;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const double gx = 0.5 * (value_at(x + 1, y) - value_at(x - 1, y));
const double gy = 0.5 * (value_at(x, y + 1) - value_at(x, y - 1));
const double mag = std::sqrt(gx * gx + gy * gy);
grads[static_cast<std::size_t>(y * width + x)] = { gx, gy };
max_mag = std::max(max_mag, mag);
sum_gx += gx;
sum_gy += gy;
}
}
const double scale = (max_mag > 1e-6) ? (0.35 / max_mag) : 0.0;
const bool fallback = max_mag <= 1e-6;
const double mid_x = static_cast<double>(width) * 0.5;
const double mid_y = static_cast<double>(height) * 0.5;
double dir_x = sum_gx;
double dir_y = sum_gy;
if (fallback || std::abs(dir_x) + std::abs(dir_y) < 1e-6) {
dir_x = 0.0;
dir_y = -1.0; // 默认向上指,保证可见
}
double dir_len = std::sqrt(dir_x * dir_x + dir_y * dir_y);
if (dir_len < 1e-6) {
dir_x = 0.0;
dir_y = -1.0;
dir_len = 1.0;
}
dir_x /= dir_len;
dir_y /= dir_len;
arrow_dir_ = QPointF(dir_x, dir_y);
const double arrow_len = 0.48 * std::min(width, height); // 稍长的指针
const double tail_ratio = 0.25; // 穿过中心的尾巴略长
const double tail_len = arrow_len * tail_ratio;
const double head_len = arrow_len - tail_len;
const double backoff = std::max(0.5, head_len * 0.12); // 轻微回缩,避免方头顶出
const double head_base = std::max(0.0, head_len - backoff);
const double cx = static_cast<double>(width) * 0.5;
const double cy = static_cast<double>(height) * 0.5;
if (primary_arrow_) {
primary_arrow_->start->setCoords(cx - dir_x * tail_len, cy - dir_y * tail_len);
primary_arrow_->end->setCoords(cx + dir_x * head_base, cy + dir_y * head_base);
primary_arrow_->setVisible(false); // 位置保留以备需要,但当前使用自绘 3D 箭头
}
replot();
}
using namespace creeper;
void VectorFieldPlot::paintEvent(QPaintEvent* event) {
vector_widget::internal::VectorPlot::paintEvent(event);
}