- vqt6: 包装类统一补 __new__ 堆分配, 修"创建函数栈帧 alloca"被存进字段/跨帧返回后 _ptr 悬垂(传给 QLayout::addWidget 等即崩溃); 菜单类按依赖顺序前移, menuBar()/statusBar() 改返回裸指针; 接入 Qt6 Charts 并新增 _bridge_canvas(Canvas 手绘板 / Graph3D 软件 3D 网络图 / ChartView 折线图); 新增自绘 QRadioButton(圆环+内点, 过渡动画)、剪贴板、应用级字体族 API - 新增模块: gzip.vp(RFC1952/1951 解码器)、torch/(torch-lite 稠密张量 + conv/pool 前反向) - vipersimd: 新增 scalar_fma/add/mul/sqrt_array 未向量化对照内核(与对应 SIMD 版逐位一致) - w32: 新增 UTF-8/UTF-16 路径转换, CreateFile/CreateDirectory 走 W 系列, 修中文路径被按 GBK 误读; read_all 参数 max_count -> buf_size 并文档化"容量含终止符"契约 - requests: https 响应缓冲按需翻倍扩容(上限 256MB), 修 MB 级正文(如 MNIST .gz)被 4KB 静默截断 - vipermath: 重写 exp 为范围归约 + 短泰勒, 修负值发散(x=-30 得 3.7e10)与 NaN 触发 2^31 次缩放导致的卡死 - builtins: 新增 sizet/uzt 别名; stdarg: va_arg 参数 CType -> CPtr - chore: 删除 vendored jpeg change.log
1193 lines
42 KiB
C++
1193 lines
42 KiB
C++
// ============================================================
|
|
// vqt6/_bridge_canvas.cpp — 自绘控件桥接实现
|
|
//
|
|
// * Vqt6Canvas 手绘板 / 28x28 灰度显示
|
|
// * Vqt6Graph3D 软件 3D 神经网络结构图(透视投影 + 画家算法排序)
|
|
// * ChartView Qt Charts 折线图
|
|
//
|
|
// 三个控件都不使用 Q_OBJECT / 信号, 回调统一经 vqt6_signal_register
|
|
// 拿到 slot_id, 事件发生时 vqt6_trigger_slot(slot_id) 回调 Viper。
|
|
// ============================================================
|
|
|
|
#include "_bridge.h"
|
|
#include "_bridge_canvas.h"
|
|
|
|
#include <QWidget>
|
|
#include <QMainWindow>
|
|
#include <QImage>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QMouseEvent>
|
|
#include <QWheelEvent>
|
|
#include <QColor>
|
|
#include <QFont>
|
|
#include <QPen>
|
|
#include <QHash>
|
|
#include <QMargins>
|
|
|
|
#include <QtCharts/QChartView>
|
|
#include <QtCharts/QChart>
|
|
#include <QtCharts/QLineSeries>
|
|
#include <QtCharts/QValueAxis>
|
|
#include <QtCharts/QLegend>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
// ============================================================
|
|
// 公共小工具
|
|
// ============================================================
|
|
namespace {
|
|
|
|
inline double ClampD(double v, double lo, double hi) {
|
|
return v < lo ? lo : (v > hi ? hi : v);
|
|
}
|
|
|
|
// 自绘控件统一的方形绘制区(居中): 返回边长与左上角偏移
|
|
inline double SquareSide(int w, int h, double* ox, double* oy) {
|
|
double side = (w < h) ? static_cast<double>(w) : static_cast<double>(h);
|
|
*ox = (static_cast<double>(w) - side) * 0.5;
|
|
*oy = (static_cast<double>(h) - side) * 0.5;
|
|
return side;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ============================================================
|
|
// Vqt6Canvas — 手绘板 / 28x28 灰度显示
|
|
// ============================================================
|
|
namespace {
|
|
|
|
class Vqt6Canvas : public QWidget {
|
|
public:
|
|
explicit Vqt6Canvas(QWidget* parent = nullptr) : QWidget(parent) {
|
|
setMinimumSize(200, 200);
|
|
setAttribute(Qt::WA_OpaquePaintEvent, true);
|
|
}
|
|
|
|
void Clear() {
|
|
strokes_.clear();
|
|
mode_ = kModeDraw;
|
|
update();
|
|
}
|
|
bool HasInk() const { return !strokes_.empty(); }
|
|
|
|
void SetMode(int m) { mode_ = (m != 0) ? kModeShow : kModeDraw; update(); }
|
|
int Mode() const { return mode_; }
|
|
|
|
void SetPen(const QColor& c, double width_norm) {
|
|
penColor_ = c;
|
|
penWidth_ = ClampD(width_norm, 0.005, 0.4);
|
|
update();
|
|
}
|
|
void SetGrid(bool on) { grid_ = on; update(); }
|
|
void SetScanLine(int y_norm) { scanY_ = y_norm; update(); }
|
|
void SetBg(const QColor& c) { bg_ = c; update(); }
|
|
|
|
// 手绘 → 28x28 灰度(MNIST 预处理); 返回是否有墨迹
|
|
int ToMnist28(unsigned char* out) {
|
|
if (out == nullptr || strokes_.empty()) return 0;
|
|
const int kSuper = 4; // 超采样倍数
|
|
const int kSide = kMnist * kSuper; // 112
|
|
QImage hi(kSide, kSide, QImage::Format_Grayscale8);
|
|
hi.fill(0);
|
|
{
|
|
QPainter p(&hi);
|
|
p.setRenderHint(QPainter::Antialiasing, true);
|
|
QPen pen(QColor(255, 255, 255));
|
|
pen.setCapStyle(Qt::RoundCap);
|
|
pen.setJoinStyle(Qt::RoundJoin);
|
|
double pw = penWidth_ * kSide;
|
|
if (pw < kSuper) pw = kSuper;
|
|
pen.setWidthF(pw);
|
|
p.setPen(pen);
|
|
p.setBrush(Qt::NoBrush);
|
|
for (size_t s = 0; s < strokes_.size(); ++s) {
|
|
const std::vector<QPointF>& st = strokes_[s];
|
|
if (st.empty()) continue;
|
|
if (st.size() == 1) {
|
|
p.drawPoint(QPointF(st[0].x() * kSide, st[0].y() * kSide));
|
|
continue;
|
|
}
|
|
QPainterPath path(QPointF(st[0].x() * kSide, st[0].y() * kSide));
|
|
for (size_t i = 1; i < st.size(); ++i) {
|
|
path.lineTo(QPointF(st[i].x() * kSide, st[i].y() * kSide));
|
|
}
|
|
p.drawPath(path);
|
|
}
|
|
}
|
|
// 4x4 盒式降采样
|
|
double raw[kMnist][kMnist];
|
|
double maxv = 0.0;
|
|
for (int y = 0; y < kMnist; ++y) {
|
|
for (int x = 0; x < kMnist; ++x) {
|
|
double acc = 0.0;
|
|
for (int dy = 0; dy < kSuper; ++dy) {
|
|
for (int dx = 0; dx < kSuper; ++dx) {
|
|
QRgb px = hi.pixel(x * kSuper + dx, y * kSuper + dy);
|
|
acc += static_cast<double>(qGray(px));
|
|
}
|
|
}
|
|
double v = acc / (kSuper * kSuper);
|
|
raw[y][x] = v;
|
|
if (v > maxv) maxv = v;
|
|
}
|
|
}
|
|
if (maxv <= 0.0) return 0;
|
|
for (int y = 0; y < kMnist; ++y) {
|
|
for (int x = 0; x < kMnist; ++x) raw[y][x] /= maxv;
|
|
}
|
|
// 包围盒
|
|
int minX = kMnist, maxX = -1, minY = kMnist, maxY = -1;
|
|
for (int y = 0; y < kMnist; ++y) {
|
|
for (int x = 0; x < kMnist; ++x) {
|
|
if (raw[y][x] > 0.02) {
|
|
if (x < minX) minX = x;
|
|
if (x > maxX) maxX = x;
|
|
if (y < minY) minY = y;
|
|
if (y > maxY) maxY = y;
|
|
}
|
|
}
|
|
}
|
|
if (maxX < minX || maxY < minY) return 0;
|
|
// 包围盒长边缩放到 20x20(对齐 MNIST 官方预处理)
|
|
int bw = maxX - minX + 1;
|
|
int bh = maxY - minY + 1;
|
|
double scale = 20.0 / static_cast<double>(bw > bh ? bw : bh);
|
|
double cx0 = (minX + maxX) * 0.5;
|
|
double cy0 = (minY + maxY) * 0.5;
|
|
double tmp[kMnist][kMnist];
|
|
std::memset(tmp, 0, sizeof(tmp));
|
|
for (int y = minY; y <= maxY; ++y) {
|
|
for (int x = minX; x <= maxX; ++x) {
|
|
double v = raw[y][x];
|
|
if (v <= 0.0) continue;
|
|
int tx = static_cast<int>(std::lround((x - cx0) * scale + 14.0));
|
|
int ty = static_cast<int>(std::lround((y - cy0) * scale + 14.0));
|
|
if (tx < 0 || tx >= kMnist || ty < 0 || ty >= kMnist) continue;
|
|
if (v > tmp[ty][tx]) tmp[ty][tx] = v;
|
|
}
|
|
}
|
|
// 质心居中
|
|
double sum = 0.0, mx = 0.0, my = 0.0;
|
|
for (int y = 0; y < kMnist; ++y) {
|
|
for (int x = 0; x < kMnist; ++x) {
|
|
sum += tmp[y][x];
|
|
mx += tmp[y][x] * x;
|
|
my += tmp[y][x] * y;
|
|
}
|
|
}
|
|
if (sum <= 0.0) return 0;
|
|
mx /= sum;
|
|
my /= sum;
|
|
int shiftX = static_cast<int>(std::lround(14.0 - mx));
|
|
int shiftY = static_cast<int>(std::lround(14.0 - my));
|
|
unsigned char dst[kMnist][kMnist];
|
|
std::memset(dst, 0, sizeof(dst));
|
|
for (int y = 0; y < kMnist; ++y) {
|
|
for (int x = 0; x < kMnist; ++x) {
|
|
double v = tmp[y][x];
|
|
if (v <= 0.0) continue;
|
|
int tx = x + shiftX;
|
|
int ty = y + shiftY;
|
|
if (tx < 0 || tx >= kMnist || ty < 0 || ty >= kMnist) continue;
|
|
int g = static_cast<int>(std::lround(v * 255.0));
|
|
dst[ty][tx] = static_cast<unsigned char>(g < 0 ? 0 : (g > 255 ? 255 : g));
|
|
}
|
|
}
|
|
std::memcpy(out, dst, kMnist * kMnist);
|
|
return 1;
|
|
}
|
|
|
|
void SetImage28(const unsigned char* in) {
|
|
if (in == nullptr) return;
|
|
QImage img(kMnist, kMnist, QImage::Format_Grayscale8);
|
|
std::memcpy(img.bits(), in, kMnist * kMnist);
|
|
image_ = img;
|
|
mode_ = kModeShow;
|
|
update();
|
|
}
|
|
|
|
int slotId = -1;
|
|
vqt6_slot_callback_t changedCb = nullptr;
|
|
void* userData = nullptr;
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent*) override {
|
|
QPainter p(this);
|
|
p.fillRect(rect(), bg_);
|
|
double ox = 0.0, oy = 0.0;
|
|
double side = SquareSide(width(), height(), &ox, &oy);
|
|
if (side <= 1.0) return;
|
|
|
|
p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
if (mode_ == kModeShow && !image_.isNull()) {
|
|
// 图像显示: 最近邻放大, 保持像素感
|
|
p.setRenderHint(QPainter::SmoothPixmapTransform, false);
|
|
QRectF dstRect(ox, oy, side, side);
|
|
p.drawImage(dstRect, image_);
|
|
} else {
|
|
// 手绘: 归一化坐标 → 方形绘制区
|
|
QPen pen(penColor_);
|
|
pen.setCapStyle(Qt::RoundCap);
|
|
pen.setJoinStyle(Qt::RoundJoin);
|
|
pen.setWidthF(penWidth_ * side);
|
|
p.setPen(pen);
|
|
p.setBrush(Qt::NoBrush);
|
|
for (size_t s = 0; s < strokes_.size(); ++s) {
|
|
const std::vector<QPointF>& st = strokes_[s];
|
|
if (st.empty()) continue;
|
|
if (st.size() == 1) {
|
|
p.drawPoint(QPointF(ox + st[0].x() * side, oy + st[0].y() * side));
|
|
continue;
|
|
}
|
|
QPainterPath path(QPointF(ox + st[0].x() * side, oy + st[0].y() * side));
|
|
for (size_t i = 1; i < st.size(); ++i) {
|
|
path.lineTo(QPointF(ox + st[i].x() * side, oy + st[i].y() * side));
|
|
}
|
|
p.drawPath(path);
|
|
}
|
|
}
|
|
|
|
// 28x28 辅助网格
|
|
if (grid_) {
|
|
QPen gp(QColor(90, 100, 120, 90));
|
|
gp.setWidthF(1.0);
|
|
p.setPen(gp);
|
|
for (int i = 1; i < kMnist; ++i) {
|
|
double t = static_cast<double>(i) / kMnist;
|
|
p.drawLine(QPointF(ox + t * side, oy), QPointF(ox + t * side, oy + side));
|
|
p.drawLine(QPointF(ox, oy + t * side), QPointF(ox + side, oy + t * side));
|
|
}
|
|
}
|
|
|
|
// 扫描线(推理动画)
|
|
if (scanY_ >= 0) {
|
|
double t = ClampD(static_cast<double>(scanY_) / 1000.0, 0.0, 1.0);
|
|
QPen sp(QColor(255, 214, 102, 220));
|
|
sp.setWidthF(2.0);
|
|
p.setPen(sp);
|
|
p.drawLine(QPointF(ox, oy + t * side), QPointF(ox + side, oy + t * side));
|
|
}
|
|
|
|
// 边框
|
|
QPen bp(QColor(70, 78, 92));
|
|
bp.setWidthF(1.0);
|
|
p.setPen(bp);
|
|
p.setBrush(Qt::NoBrush);
|
|
p.drawRect(QRectF(ox, oy, side, side));
|
|
}
|
|
|
|
void mousePressEvent(QMouseEvent* e) override {
|
|
if (mode_ != kModeDraw || e->button() != Qt::LeftButton) return;
|
|
strokes_.push_back(std::vector<QPointF>());
|
|
AppendPoint(e->position());
|
|
}
|
|
|
|
void mouseMoveEvent(QMouseEvent* e) override {
|
|
if (mode_ != kModeDraw) return;
|
|
if (!(e->buttons() & Qt::LeftButton)) return;
|
|
AppendPoint(e->position());
|
|
}
|
|
|
|
void mouseReleaseEvent(QMouseEvent* e) override {
|
|
if (mode_ != kModeDraw || e->button() != Qt::LeftButton) return;
|
|
if (slotId >= 0) vqt6_trigger_slot(slotId);
|
|
}
|
|
|
|
private:
|
|
void AppendPoint(const QPointF& pos) {
|
|
if (strokes_.empty()) return;
|
|
double ox = 0.0, oy = 0.0;
|
|
double side = SquareSide(width(), height(), &ox, &oy);
|
|
if (side <= 1.0) return;
|
|
double nx = ClampD((pos.x() - ox) / side, 0.0, 1.0);
|
|
double ny = ClampD((pos.y() - oy) / side, 0.0, 1.0);
|
|
strokes_.back().push_back(QPointF(nx, ny));
|
|
update();
|
|
}
|
|
|
|
static const int kModeDraw = 0;
|
|
static const int kModeShow = 1;
|
|
static const int kMnist = 28;
|
|
|
|
std::vector<std::vector<QPointF> > strokes_;
|
|
QImage image_;
|
|
QColor penColor_ = QColor(255, 255, 255);
|
|
QColor bg_ = QColor(12, 14, 18);
|
|
double penWidth_ = 0.07;
|
|
bool grid_ = false;
|
|
int scanY_ = -1;
|
|
int mode_ = kModeDraw;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
extern "C" VQT6_API void* vqt6_canvas_create(void) {
|
|
return new Vqt6Canvas();
|
|
}
|
|
|
|
extern "C" VQT6_API void* vqt6_canvas_create_with_parent(void* parent) {
|
|
if (parent == nullptr) return nullptr;
|
|
return new Vqt6Canvas(static_cast<QWidget*>(parent));
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_clear(void* canvas) {
|
|
if (canvas == nullptr) return;
|
|
static_cast<Vqt6Canvas*>(canvas)->Clear();
|
|
}
|
|
|
|
extern "C" VQT6_API int vqt6_canvas_has_ink(void* canvas) {
|
|
if (canvas == nullptr) return 0;
|
|
return static_cast<Vqt6Canvas*>(canvas)->HasInk() ? 1 : 0;
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_set_mode(void* canvas, int mode) {
|
|
if (canvas == nullptr) return;
|
|
static_cast<Vqt6Canvas*>(canvas)->SetMode(mode);
|
|
}
|
|
|
|
extern "C" VQT6_API int vqt6_canvas_mode(void* canvas) {
|
|
if (canvas == nullptr) return 0;
|
|
return static_cast<Vqt6Canvas*>(canvas)->Mode();
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_set_pen(void* canvas, int r, int g, int b, double width_norm) {
|
|
if (canvas == nullptr) return;
|
|
static_cast<Vqt6Canvas*>(canvas)->SetPen(QColor(r, g, b), width_norm);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_set_grid(void* canvas, int on) {
|
|
if (canvas == nullptr) return;
|
|
static_cast<Vqt6Canvas*>(canvas)->SetGrid(on != 0);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_set_scan_line(void* canvas, int y_norm) {
|
|
if (canvas == nullptr) return;
|
|
static_cast<Vqt6Canvas*>(canvas)->SetScanLine(y_norm);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_set_bg(void* canvas, int r, int g, int b) {
|
|
if (canvas == nullptr) return;
|
|
static_cast<Vqt6Canvas*>(canvas)->SetBg(QColor(r, g, b));
|
|
}
|
|
|
|
extern "C" VQT6_API int vqt6_canvas_to_mnist28(void* canvas, unsigned char* out) {
|
|
if (canvas == nullptr) return 0;
|
|
return static_cast<Vqt6Canvas*>(canvas)->ToMnist28(out);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_set_image28(void* canvas, const unsigned char* in) {
|
|
if (canvas == nullptr) return;
|
|
static_cast<Vqt6Canvas*>(canvas)->SetImage28(in);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_canvas_connect_changed(void* canvas, vqt6_slot_callback_t cb, void* user_data) {
|
|
if (canvas == nullptr || cb == nullptr) return;
|
|
Vqt6Canvas* c = static_cast<Vqt6Canvas*>(canvas);
|
|
c->changedCb = cb;
|
|
c->userData = user_data;
|
|
c->slotId = vqt6_signal_register(cb, user_data);
|
|
}
|
|
|
|
// ============================================================
|
|
// Vqt6Graph3D — 软件 3D 神经网络结构图
|
|
// ============================================================
|
|
namespace {
|
|
|
|
struct GraphNode {
|
|
double x, y, z;
|
|
int layer;
|
|
double value;
|
|
};
|
|
|
|
struct GraphEdge {
|
|
int a, b;
|
|
double w;
|
|
double norm; // |w| / max|w|
|
|
};
|
|
|
|
struct GraphProj {
|
|
double sx, sy;
|
|
double depth; // 相机距离, 越大越远
|
|
double scale;
|
|
bool ok;
|
|
};
|
|
|
|
class Vqt6Graph3D : public QWidget {
|
|
public:
|
|
explicit Vqt6Graph3D(QWidget* parent = nullptr) : QWidget(parent) {
|
|
setMinimumSize(320, 240);
|
|
setAttribute(Qt::WA_OpaquePaintEvent, true);
|
|
setMouseTracking(true);
|
|
}
|
|
|
|
void Setup(const int* sizes, int n_layers) {
|
|
nodes_.clear();
|
|
edges_.clear();
|
|
layerStart_.clear();
|
|
layerCount_.clear();
|
|
layerAlpha_.clear();
|
|
edgeDraw_.clear();
|
|
if (sizes == nullptr || n_layers <= 0) { update(); return; }
|
|
|
|
// 各层沿 Z 轴等距排布(输入层在最远端)
|
|
for (int li = 0; li < n_layers; ++li) {
|
|
int cnt = sizes[li];
|
|
if (cnt < 0) cnt = 0;
|
|
layerStart_.push_back(static_cast<int>(nodes_.size()));
|
|
layerCount_.push_back(cnt);
|
|
layerAlpha_.push_back(1.0);
|
|
double z = (n_layers > 1)
|
|
? (-1.1 + 2.2 * static_cast<double>(li) / static_cast<double>(n_layers - 1))
|
|
: 0.0;
|
|
// 近正方形网格布局
|
|
int cols = static_cast<int>(std::ceil(std::sqrt(static_cast<double>(cnt > 0 ? cnt : 1))));
|
|
if (cols < 1) cols = 1;
|
|
int rows = (cnt + cols - 1) / cols;
|
|
if (rows < 1) rows = 1;
|
|
double spacing = 1.6 / static_cast<double>(cols > rows ? cols : rows);
|
|
for (int i = 0; i < cnt; ++i) {
|
|
int cx = i % cols;
|
|
int cy = i / cols;
|
|
GraphNode n;
|
|
n.x = (static_cast<double>(cx) - (cols - 1) * 0.5) * spacing;
|
|
n.y = (static_cast<double>(cy) - (rows - 1) * 0.5) * spacing;
|
|
n.z = z;
|
|
n.layer = li;
|
|
n.value = 0.0;
|
|
nodes_.push_back(n);
|
|
}
|
|
}
|
|
// 相邻层前向全连接
|
|
for (int li = 0; li + 1 < n_layers; ++li) {
|
|
int aStart = layerStart_[li], aCnt = layerCount_[li];
|
|
int bStart = layerStart_[li + 1], bCnt = layerCount_[li + 1];
|
|
for (int i = 0; i < aCnt; ++i) {
|
|
for (int j = 0; j < bCnt; ++j) {
|
|
GraphEdge e;
|
|
e.a = aStart + i;
|
|
e.b = bStart + j;
|
|
e.w = 0.0;
|
|
e.norm = 0.0;
|
|
edges_.push_back(e);
|
|
}
|
|
}
|
|
}
|
|
RebuildEdgeDraw();
|
|
update();
|
|
}
|
|
|
|
void Clear() {
|
|
nodes_.clear();
|
|
edges_.clear();
|
|
layerStart_.clear();
|
|
layerCount_.clear();
|
|
layerAlpha_.clear();
|
|
edgeDraw_.clear();
|
|
update();
|
|
}
|
|
|
|
int NodeCount() const { return static_cast<int>(nodes_.size()); }
|
|
int EdgeCount() const { return static_cast<int>(edges_.size()); }
|
|
|
|
void SetLayerAlpha(int layer, double a) {
|
|
if (layer < 0 || layer >= static_cast<int>(layerAlpha_.size())) return;
|
|
layerAlpha_[layer] = ClampD(a, 0.0, 1.0);
|
|
update();
|
|
}
|
|
|
|
void SetAllLayerAlpha(double a) {
|
|
for (size_t i = 0; i < layerAlpha_.size(); ++i) layerAlpha_[i] = ClampD(a, 0.0, 1.0);
|
|
update();
|
|
}
|
|
|
|
void SetValues(const double* v, int n) {
|
|
if (v == nullptr) return;
|
|
int cnt = (n < static_cast<int>(nodes_.size())) ? n : static_cast<int>(nodes_.size());
|
|
for (int i = 0; i < cnt; ++i) nodes_[i].value = ClampD(v[i], 0.0, 1.0);
|
|
update();
|
|
}
|
|
|
|
void SetEdgeWeights(const double* w, int n) {
|
|
if (w == nullptr) return;
|
|
int cnt = (n < static_cast<int>(edges_.size())) ? n : static_cast<int>(edges_.size());
|
|
double maxAbs = 0.0;
|
|
for (int i = 0; i < cnt; ++i) {
|
|
edges_[i].w = w[i];
|
|
double a = std::fabs(w[i]);
|
|
if (a > maxAbs) maxAbs = a;
|
|
}
|
|
for (int i = 0; i < cnt; ++i) {
|
|
edges_[i].norm = (maxAbs > 0.0) ? (std::fabs(edges_[i].w) / maxAbs) : 0.0;
|
|
}
|
|
RebuildEdgeDraw();
|
|
update();
|
|
}
|
|
|
|
void SetEdgeThreshold(double t) {
|
|
edgeThreshold_ = ClampD(t, 0.0, 1.0);
|
|
RebuildEdgeDraw();
|
|
update();
|
|
}
|
|
|
|
void SetEdgeBudget(int k) {
|
|
edgeBudget_ = (k > 0) ? k : 0;
|
|
RebuildEdgeDraw();
|
|
update();
|
|
}
|
|
|
|
void SetView(double yaw, double pitch, double dist) {
|
|
yaw_ = yaw;
|
|
pitch_ = ClampD(pitch, -85.0, 85.0);
|
|
dist_ = ClampD(dist, 0.6, 20.0);
|
|
update();
|
|
}
|
|
|
|
void GetView(double* yaw, double* pitch, double* dist) const {
|
|
if (yaw != nullptr) *yaw = yaw_;
|
|
if (pitch != nullptr) *pitch = pitch_;
|
|
if (dist != nullptr) *dist = dist_;
|
|
}
|
|
|
|
void ResetView() {
|
|
yaw_ = -28.0;
|
|
pitch_ = 16.0;
|
|
dist_ = 3.4;
|
|
update();
|
|
}
|
|
|
|
void SetBg(const QColor& c) { bg_ = c; update(); }
|
|
void SetNodeScale(double s) { nodeScale_ = ClampD(s, 0.1, 5.0); update(); }
|
|
|
|
int slotId = -1;
|
|
vqt6_slot_callback_t viewCb = nullptr;
|
|
void* userData = nullptr;
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent*) override {
|
|
QPainter p(this);
|
|
p.fillRect(rect(), bg_);
|
|
const int w = width();
|
|
const int h = height();
|
|
if (w < 8 || h < 8) return;
|
|
p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
if (nodes_.empty()) {
|
|
p.setPen(QColor(120, 130, 148));
|
|
p.drawText(rect(), Qt::AlignCenter, QStringLiteral("尚无网络结构"));
|
|
return;
|
|
}
|
|
|
|
const double px = (w < h ? w : h) * 0.34; // 单位长度 → 像素
|
|
const double cx = w * 0.5;
|
|
const double cy = h * 0.5;
|
|
const double focal = 1.6;
|
|
|
|
const double ry = yaw_ * kPi / 180.0;
|
|
const double rx = pitch_ * kPi / 180.0;
|
|
const double sy = std::sin(ry), cyw = std::cos(ry);
|
|
const double sx = std::sin(rx), cxw = std::cos(rx);
|
|
|
|
// 投影全部节点
|
|
std::vector<GraphProj> proj(nodes_.size());
|
|
for (size_t i = 0; i < nodes_.size(); ++i) {
|
|
const GraphNode& n = nodes_[i];
|
|
double x1 = n.x * cyw + n.z * sy;
|
|
double z1 = -n.x * sy + n.z * cyw;
|
|
double y2 = n.y * cxw - z1 * sx;
|
|
double z2 = n.y * sx + z1 * cxw;
|
|
double depth = dist_ - z2;
|
|
GraphProj gp;
|
|
if (depth < 0.15) {
|
|
gp.ok = false;
|
|
gp.sx = gp.sy = 0.0;
|
|
gp.depth = 1e9;
|
|
gp.scale = 0.0;
|
|
} else {
|
|
double sc = focal / depth;
|
|
gp.ok = true;
|
|
gp.sx = cx + x1 * sc * px;
|
|
gp.sy = cy - y2 * sc * px;
|
|
gp.depth = depth;
|
|
gp.scale = sc;
|
|
}
|
|
proj[i] = gp;
|
|
}
|
|
|
|
// ---- 边: 先画远的 ----
|
|
std::vector<int> eorder(edgeDraw_.begin(), edgeDraw_.end());
|
|
std::sort(eorder.begin(), eorder.end(), [&](int a, int b) {
|
|
const GraphEdge& ea = edges_[a];
|
|
const GraphEdge& eb = edges_[b];
|
|
double da = proj[ea.a].depth + proj[ea.b].depth;
|
|
double db = proj[eb.a].depth + proj[eb.b].depth;
|
|
return da > db;
|
|
});
|
|
p.setBrush(Qt::NoBrush);
|
|
for (size_t k = 0; k < eorder.size(); ++k) {
|
|
const GraphEdge& e = edges_[eorder[k]];
|
|
const GraphProj& pa = proj[e.a];
|
|
const GraphProj& pb = proj[e.b];
|
|
if (!pa.ok || !pb.ok) continue;
|
|
double la = LayerAlphaOf(e.a);
|
|
double lb = LayerAlphaOf(e.b);
|
|
double la2 = (la < lb) ? la : lb;
|
|
if (la2 <= 0.01) continue;
|
|
double depthFall = ClampD(1.35 - (pa.depth + pb.depth) * 0.5 / dist_, 0.15, 1.0);
|
|
int alpha = static_cast<int>(std::lround((0.08 + 0.42 * e.norm) * la2 * depthFall * 255.0));
|
|
if (alpha < 4) continue;
|
|
QColor col = (e.w >= 0.0) ? QColor(255, 107, 107, alpha) : QColor(77, 171, 247, alpha);
|
|
QPen pen(col);
|
|
pen.setWidthF(1.0);
|
|
p.setPen(pen);
|
|
p.drawLine(QPointF(pa.sx, pa.sy), QPointF(pb.sx, pb.sy));
|
|
}
|
|
|
|
// ---- 节点: 由远及近 ----
|
|
std::vector<int> norder(nodes_.size());
|
|
for (size_t i = 0; i < norder.size(); ++i) norder[i] = static_cast<int>(i);
|
|
std::sort(norder.begin(), norder.end(), [&](int a, int b) {
|
|
return proj[a].depth > proj[b].depth;
|
|
});
|
|
for (size_t k = 0; k < norder.size(); ++k) {
|
|
int idx = norder[k];
|
|
const GraphNode& n = nodes_[idx];
|
|
const GraphProj& gp = proj[idx];
|
|
if (!gp.ok) continue;
|
|
double la = LayerAlphaOf(idx);
|
|
if (la <= 0.01) continue;
|
|
double r = 0.020 * px * nodeScale_ * (0.55 + 0.95 * n.value) * gp.scale;
|
|
if (r < 1.2) r = 1.2;
|
|
QColor base = LayerColor(n.layer);
|
|
double depthFall = ClampD(1.3 - gp.depth / dist_, 0.25, 1.0);
|
|
int a = static_cast<int>(std::lround((0.28 + 0.72 * n.value) * la * depthFall * 255.0));
|
|
QColor col(base.red(), base.green(), base.blue(), a < 0 ? 0 : (a > 255 ? 255 : a));
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(col);
|
|
p.drawEllipse(QPointF(gp.sx, gp.sy), r, r);
|
|
}
|
|
|
|
// ---- 输出层数字标签 ----
|
|
int last = static_cast<int>(layerCount_.size()) - 1;
|
|
if (last >= 0 && LayerAlphaOf(layerStart_[last]) > 0.25) {
|
|
int a = static_cast<int>(std::lround(LayerAlphaOf(layerStart_[last]) * 255.0));
|
|
QFont f = p.font();
|
|
f.setPointSizeF(9.0);
|
|
f.setBold(true);
|
|
p.setFont(f);
|
|
int base = layerStart_[last];
|
|
int cnt = layerCount_[last];
|
|
for (int i = 0; i < cnt; ++i) {
|
|
const GraphProj& gp = proj[base + i];
|
|
if (!gp.ok) continue;
|
|
p.setPen(QColor(255, 214, 102, a));
|
|
p.drawText(QPointF(gp.sx + 7.0, gp.sy + 4.0), QString::number(i));
|
|
}
|
|
}
|
|
|
|
// ---- 层标题 ----
|
|
const char* kNames[] = { "输入", "隐藏", "输出" };
|
|
QFont sf = p.font();
|
|
sf.setPointSizeF(8.5);
|
|
p.setFont(sf);
|
|
for (size_t li = 0; li < layerCount_.size(); ++li) {
|
|
double z = LayerZ(static_cast<int>(li));
|
|
double x1 = 0.0 * cyw + z * sy;
|
|
double z1 = -0.0 * sy + z * cyw;
|
|
double y2 = (-0.95) * cxw - z1 * sx;
|
|
double z2 = (-0.95) * sx + z1 * cxw;
|
|
double depth = dist_ - z2;
|
|
if (depth < 0.15) continue;
|
|
double sc = focal / depth;
|
|
double txx = cx + x1 * sc * px;
|
|
double tyy = cy - y2 * sc * px;
|
|
int a = static_cast<int>(std::lround(LayerAlphaOf(layerStart_[li]) * 210.0));
|
|
if (a < 10) a = 10;
|
|
QString cap;
|
|
if (li < 3) cap = QString::fromUtf8(kNames[li]);
|
|
else cap = QStringLiteral("层");
|
|
cap += QStringLiteral(" %1").arg(layerCount_[li]);
|
|
p.setPen(QColor(150, 160, 180, a));
|
|
p.drawText(QPointF(txx - 22.0, tyy), cap);
|
|
}
|
|
}
|
|
|
|
void mousePressEvent(QMouseEvent* e) override {
|
|
if (e->button() != Qt::LeftButton) return;
|
|
dragging_ = true;
|
|
lastPos_ = e->position();
|
|
}
|
|
|
|
void mouseMoveEvent(QMouseEvent* e) override {
|
|
if (!dragging_) return;
|
|
QPointF d = e->position() - lastPos_;
|
|
lastPos_ = e->position();
|
|
yaw_ += d.x() * 0.45;
|
|
pitch_ = ClampD(pitch_ + d.y() * 0.35, -85.0, 85.0);
|
|
update();
|
|
if (slotId >= 0) vqt6_trigger_slot(slotId);
|
|
}
|
|
|
|
void mouseReleaseEvent(QMouseEvent* e) override {
|
|
if (e->button() != Qt::LeftButton) return;
|
|
dragging_ = false;
|
|
if (slotId >= 0) vqt6_trigger_slot(slotId);
|
|
}
|
|
|
|
void wheelEvent(QWheelEvent* e) override {
|
|
double steps = e->angleDelta().y() / 120.0;
|
|
dist_ = ClampD(dist_ * std::pow(1.12, -steps), 0.6, 20.0);
|
|
update();
|
|
if (slotId >= 0) vqt6_trigger_slot(slotId);
|
|
}
|
|
|
|
private:
|
|
static constexpr double kPi = 3.14159265358979323846;
|
|
|
|
double LayerZ(int li) const {
|
|
int n = static_cast<int>(layerCount_.size());
|
|
if (n <= 1) return 0.0;
|
|
return -1.1 + 2.2 * static_cast<double>(li) / static_cast<double>(n - 1);
|
|
}
|
|
|
|
double LayerAlphaOf(int nodeIndex) const {
|
|
if (nodeIndex < 0 || nodeIndex >= static_cast<int>(nodes_.size())) return 1.0;
|
|
int li = nodes_[nodeIndex].layer;
|
|
if (li < 0 || li >= static_cast<int>(layerAlpha_.size())) return 1.0;
|
|
return layerAlpha_[li];
|
|
}
|
|
|
|
QColor LayerColor(int layer) const {
|
|
if (layer == 0) return QColor(200, 210, 225); // 输入层: 冷白
|
|
int last = static_cast<int>(layerCount_.size()) - 1;
|
|
if (layer == last) return QColor(255, 190, 70); // 输出层: 琥珀
|
|
return QColor(90, 200, 190); // 隐藏层: 青
|
|
}
|
|
|
|
// 按阈值 + 预算重建待画边索引(|w| 大的优先)
|
|
void RebuildEdgeDraw() {
|
|
edgeDraw_.clear();
|
|
std::vector<int> cand;
|
|
for (size_t i = 0; i < edges_.size(); ++i) {
|
|
if (edges_[i].norm >= edgeThreshold_ && edges_[i].norm > 0.0) {
|
|
cand.push_back(static_cast<int>(i));
|
|
}
|
|
}
|
|
if (edgeBudget_ > 0 && static_cast<int>(cand.size()) > edgeBudget_) {
|
|
std::partial_sort(cand.begin(), cand.begin() + edgeBudget_, cand.end(),
|
|
[&](int a, int b) { return edges_[a].norm > edges_[b].norm; });
|
|
cand.resize(static_cast<size_t>(edgeBudget_));
|
|
}
|
|
edgeDraw_ = cand;
|
|
}
|
|
|
|
std::vector<GraphNode> nodes_;
|
|
std::vector<GraphEdge> edges_;
|
|
std::vector<int> layerStart_;
|
|
std::vector<int> layerCount_;
|
|
std::vector<double> layerAlpha_;
|
|
std::vector<int> edgeDraw_;
|
|
double edgeThreshold_ = 0.0;
|
|
int edgeBudget_ = 3000;
|
|
double yaw_ = -28.0;
|
|
double pitch_ = 16.0;
|
|
double dist_ = 3.4;
|
|
double nodeScale_ = 1.0;
|
|
QColor bg_ = QColor(14, 16, 22);
|
|
bool dragging_ = false;
|
|
QPointF lastPos_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
extern "C" VQT6_API void* vqt6_graph3d_create(void) {
|
|
return new Vqt6Graph3D();
|
|
}
|
|
|
|
extern "C" VQT6_API void* vqt6_graph3d_create_with_parent(void* parent) {
|
|
if (parent == nullptr) return nullptr;
|
|
return new Vqt6Graph3D(static_cast<QWidget*>(parent));
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_setup(void* g, const int* layer_sizes, int n_layers) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->Setup(layer_sizes, n_layers);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_clear(void* g) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->Clear();
|
|
}
|
|
|
|
extern "C" VQT6_API int vqt6_graph3d_node_count(void* g) {
|
|
if (g == nullptr) return 0;
|
|
return static_cast<Vqt6Graph3D*>(g)->NodeCount();
|
|
}
|
|
|
|
extern "C" VQT6_API int vqt6_graph3d_edge_count(void* g) {
|
|
if (g == nullptr) return 0;
|
|
return static_cast<Vqt6Graph3D*>(g)->EdgeCount();
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_layer_alpha(void* g, int layer, double alpha) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetLayerAlpha(layer, alpha);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_all_layer_alpha(void* g, double alpha) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetAllLayerAlpha(alpha);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_values(void* g, const double* values, int n) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetValues(values, n);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_edge_weights(void* g, const double* w, int n) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetEdgeWeights(w, n);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_edge_threshold(void* g, double t) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetEdgeThreshold(t);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_edge_budget(void* g, int max_edges) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetEdgeBudget(max_edges);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_view(void* g, double yaw_deg, double pitch_deg, double dist) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetView(yaw_deg, pitch_deg, dist);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_get_view(void* g, double* yaw_deg, double* pitch_deg, double* dist) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->GetView(yaw_deg, pitch_deg, dist);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_bg(void* g, int r, int gcol, int b) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetBg(QColor(r, gcol, b));
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_set_node_scale(void* g, double s) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->SetNodeScale(s);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_connect_view_changed(void* g, vqt6_slot_callback_t cb, void* user_data) {
|
|
if (g == nullptr || cb == nullptr) return;
|
|
Vqt6Graph3D* gp = static_cast<Vqt6Graph3D*>(g);
|
|
gp->viewCb = cb;
|
|
gp->userData = user_data;
|
|
gp->slotId = vqt6_signal_register(cb, user_data);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_graph3d_reset_view(void* g) {
|
|
if (g == nullptr) return;
|
|
static_cast<Vqt6Graph3D*>(g)->ResetView();
|
|
}
|
|
|
|
// ============================================================
|
|
// ChartView — Qt Charts 折线图
|
|
// ============================================================
|
|
namespace {
|
|
|
|
struct ChartAxes {
|
|
QValueAxis* x;
|
|
QValueAxis* yLeft;
|
|
QValueAxis* yRight;
|
|
};
|
|
|
|
QHash<QChartView*, ChartAxes>& ChartAxesMap() {
|
|
static QHash<QChartView*, ChartAxes> m;
|
|
return m;
|
|
}
|
|
|
|
QHash<QLineSeries*, int>& SeriesSideMap() {
|
|
static QHash<QLineSeries*, int> m;
|
|
return m;
|
|
}
|
|
|
|
// 每条曲线的滑动窗口上限(0 = 不限制), 由 vqt6_chartview_set_max_points 设置
|
|
QHash<QChartView*, int>& MaxPointsMap() {
|
|
static QHash<QChartView*, int> m;
|
|
return m;
|
|
}
|
|
|
|
// 放大窗: 每个 ChartView 至多一个, 建一次后复用
|
|
// (不设 WA_DeleteOnClose —— QChartView 被 delete 时 Qt Charts 的销毁路径很脆,
|
|
// 关闭大窗只要隐藏即可, 下次点"查看大图"直接复用)
|
|
struct ZoomView {
|
|
QMainWindow* win;
|
|
QChartView* view;
|
|
QChart* chart;
|
|
QValueAxis* x;
|
|
QValueAxis* yLeft;
|
|
QValueAxis* yRight;
|
|
QList<QLineSeries*> series; // 与源图 series 顺序一一对应
|
|
ZoomView() : win(nullptr), view(nullptr), chart(nullptr),
|
|
x(nullptr), yLeft(nullptr), yRight(nullptr) {}
|
|
};
|
|
|
|
QHash<QChartView*, ZoomView>& ZoomMap() {
|
|
static QHash<QChartView*, ZoomView> m;
|
|
return m;
|
|
}
|
|
|
|
// 把源图当前所有曲线与轴范围整体复制到放大窗(替换式, 不是追加)
|
|
void SyncZoomView(QChartView* src, ZoomView& z) {
|
|
if (z.chart == nullptr || z.win == nullptr) return;
|
|
if (!z.win->isVisible()) return; // 关着就不做无谓的拷贝
|
|
const QList<QAbstractSeries*> all = src->chart()->series();
|
|
if (z.series.size() != all.size()) return;
|
|
for (int i = 0; i < all.size(); ++i) {
|
|
QLineSeries* ls = qobject_cast<QLineSeries*>(all.at(i));
|
|
if (ls == nullptr) continue;
|
|
z.series.at(i)->replace(ls->points());
|
|
}
|
|
ChartAxes sa = ChartAxesMap().value(src);
|
|
if (sa.x != nullptr) z.x->setRange(sa.x->min(), sa.x->max());
|
|
if (sa.yLeft != nullptr) z.yLeft->setRange(sa.yLeft->min(), sa.yLeft->max());
|
|
if (sa.yRight != nullptr && sa.yRight->isVisible()) {
|
|
z.yRight->setRange(sa.yRight->min(), sa.yRight->max());
|
|
z.yRight->setVisible(true);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" VQT6_API void* vqt6_chartview_create(void) {
|
|
QChart* chart = new QChart();
|
|
QChartView* view = new QChartView(chart);
|
|
view->setRenderHint(QPainter::Antialiasing, true);
|
|
chart->legend()->setVisible(true);
|
|
chart->legend()->setAlignment(Qt::AlignBottom);
|
|
chart->setMargins(QMargins(4, 4, 4, 4));
|
|
chart->setBackgroundRoundness(0);
|
|
|
|
ChartAxes ax;
|
|
ax.x = new QValueAxis();
|
|
ax.x->setTitleText(QStringLiteral("step"));
|
|
ax.yLeft = new QValueAxis();
|
|
ax.yRight = new QValueAxis();
|
|
ax.yRight->setVisible(false);
|
|
chart->addAxis(ax.x, Qt::AlignBottom);
|
|
chart->addAxis(ax.yLeft, Qt::AlignLeft);
|
|
chart->addAxis(ax.yRight, Qt::AlignRight);
|
|
ChartAxesMap().insert(view, ax);
|
|
return view;
|
|
}
|
|
|
|
extern "C" VQT6_API void* vqt6_chartview_create_with_parent(void* parent) {
|
|
if (parent == nullptr) return nullptr;
|
|
void* view = vqt6_chartview_create();
|
|
static_cast<QWidget*>(view)->setParent(static_cast<QWidget*>(parent));
|
|
return view;
|
|
}
|
|
|
|
extern "C" VQT6_API void* vqt6_chartview_add_line(void* view, const char* name,
|
|
int r, int g, int b, int right_axis) {
|
|
if (view == nullptr) return nullptr;
|
|
QChartView* cv = static_cast<QChartView*>(view);
|
|
ChartAxes axes = ChartAxesMap().value(cv);
|
|
if (axes.x == nullptr) return nullptr;
|
|
QLineSeries* series = new QLineSeries();
|
|
if (name != nullptr) series->setName(QString::fromUtf8(name));
|
|
QPen pen(QColor(r, g, b));
|
|
pen.setWidthF(2.0);
|
|
series->setPen(pen);
|
|
cv->chart()->addSeries(series);
|
|
series->attachAxis(axes.x);
|
|
if (right_axis != 0) {
|
|
series->attachAxis(axes.yRight);
|
|
axes.yRight->setVisible(true);
|
|
} else {
|
|
series->attachAxis(axes.yLeft);
|
|
}
|
|
SeriesSideMap().insert(series, right_axis != 0 ? 1 : 0);
|
|
return series;
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_append(void* view, void* series, double x, double y) {
|
|
if (view == nullptr || series == nullptr) return;
|
|
QLineSeries* ls = static_cast<QLineSeries*>(series);
|
|
ls->append(x, y);
|
|
// 滑动窗口: 超出上限就丢掉最旧的点, 避免长时间训练后曲线挤成一团
|
|
const int maxp = MaxPointsMap().value(static_cast<QChartView*>(view), 0);
|
|
if (maxp > 0) {
|
|
const int extra = ls->count() - maxp;
|
|
if (extra > 0) ls->removePoints(0, extra);
|
|
}
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_clear_series(void* view, void* series) {
|
|
if (series == nullptr) return;
|
|
static_cast<QLineSeries*>(series)->clear();
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_clear_all(void* view) {
|
|
if (view == nullptr) return;
|
|
QChartView* cv = static_cast<QChartView*>(view);
|
|
const QList<QAbstractSeries*> all = cv->chart()->series();
|
|
for (int i = 0; i < all.size(); ++i) {
|
|
QLineSeries* ls = qobject_cast<QLineSeries*>(all.at(i));
|
|
if (ls != nullptr) ls->clear();
|
|
}
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_autoscale(void* view) {
|
|
if (view == nullptr) return;
|
|
QChartView* cv = static_cast<QChartView*>(view);
|
|
ChartAxes axes = ChartAxesMap().value(cv);
|
|
if (axes.x == nullptr) return;
|
|
const QList<QAbstractSeries*> all = cv->chart()->series();
|
|
double xmin = 1e300, xmax = -1e300;
|
|
double lmin = 1e300, lmax = -1e300;
|
|
double rmin = 1e300, rmax = -1e300;
|
|
bool hasRight = false;
|
|
for (int i = 0; i < all.size(); ++i) {
|
|
QLineSeries* ls = qobject_cast<QLineSeries*>(all.at(i));
|
|
if (ls == nullptr) continue;
|
|
int side = SeriesSideMap().value(ls, 0);
|
|
QVector<QPointF> pts = ls->points();
|
|
for (int k = 0; k < pts.size(); ++k) {
|
|
double x = pts.at(k).x();
|
|
double y = pts.at(k).y();
|
|
if (x < xmin) xmin = x;
|
|
if (x > xmax) xmax = x;
|
|
if (side != 0) {
|
|
hasRight = true;
|
|
if (y < rmin) rmin = y;
|
|
if (y > rmax) rmax = y;
|
|
} else {
|
|
if (y < lmin) lmin = y;
|
|
if (y > lmax) lmax = y;
|
|
}
|
|
}
|
|
}
|
|
if (xmax < xmin) { xmin = 0.0; xmax = 1.0; }
|
|
if (xmax - xmin < 1e-9) xmax = xmin + 1.0;
|
|
axes.x->setRange(xmin, xmax);
|
|
if (lmax >= lmin) {
|
|
if (lmax - lmin < 1e-9) { lmin -= 0.5; lmax += 0.5; }
|
|
axes.yLeft->setRange(lmin, lmax);
|
|
}
|
|
if (hasRight && rmax >= rmin) {
|
|
if (rmax - rmin < 1e-9) { rmin -= 0.5; rmax += 0.5; }
|
|
axes.yRight->setRange(rmin, rmax);
|
|
axes.yRight->setVisible(true);
|
|
}
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_set_title(void* view, const char* utf8) {
|
|
if (view == nullptr) return;
|
|
QChartView* cv = static_cast<QChartView*>(view);
|
|
cv->chart()->setTitle(utf8 != nullptr ? QString::fromUtf8(utf8) : QString());
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_set_legend_visible(void* view, int visible) {
|
|
if (view == nullptr) return;
|
|
QChartView* cv = static_cast<QChartView*>(view);
|
|
cv->chart()->legend()->setVisible(visible != 0);
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_set_background(void* view, int r, int g, int b) {
|
|
if (view == nullptr) return;
|
|
QChartView* cv = static_cast<QChartView*>(view);
|
|
QColor c(r, g, b);
|
|
cv->chart()->setBackgroundVisible(true);
|
|
cv->chart()->setBackgroundBrush(c);
|
|
cv->chart()->setPlotAreaBackgroundVisible(true);
|
|
cv->chart()->setPlotAreaBackgroundBrush(c.darker(115));
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_set_max_points(void* view, int max_points) {
|
|
if (view == nullptr) return;
|
|
MaxPointsMap().insert(static_cast<QChartView*>(view), max_points);
|
|
}
|
|
|
|
extern "C" VQT6_API void* vqt6_chartview_open_zoom(void* view, const char* title) {
|
|
if (view == nullptr) return nullptr;
|
|
QChartView* src = static_cast<QChartView*>(view);
|
|
ZoomView& z = ZoomMap()[src];
|
|
|
|
if (z.win == nullptr) {
|
|
QChart* chart = new QChart();
|
|
chart->legend()->setVisible(true);
|
|
chart->legend()->setAlignment(Qt::AlignBottom);
|
|
chart->setMargins(QMargins(6, 6, 6, 6));
|
|
chart->setBackgroundRoundness(0);
|
|
chart->setBackgroundVisible(true);
|
|
chart->setBackgroundBrush(src->chart()->backgroundBrush());
|
|
chart->setPlotAreaBackgroundVisible(true);
|
|
chart->setPlotAreaBackgroundBrush(src->chart()->plotAreaBackgroundBrush());
|
|
|
|
QValueAxis* ax = new QValueAxis();
|
|
ax->setTitleText(QStringLiteral("step"));
|
|
QValueAxis* yLeft = new QValueAxis();
|
|
QValueAxis* yRight = new QValueAxis();
|
|
yRight->setVisible(false);
|
|
chart->addAxis(ax, Qt::AlignBottom);
|
|
chart->addAxis(yLeft, Qt::AlignLeft);
|
|
chart->addAxis(yRight, Qt::AlignRight);
|
|
|
|
// 按源图的曲线条数建空曲线(同名/同色/同左右轴), 数据靠 SyncZoomView 拷贝
|
|
const QList<QAbstractSeries*> all = src->chart()->series();
|
|
for (int i = 0; i < all.size(); ++i) {
|
|
QLineSeries* ls = qobject_cast<QLineSeries*>(all.at(i));
|
|
if (ls == nullptr) continue;
|
|
QLineSeries* cp = new QLineSeries();
|
|
cp->setName(ls->name());
|
|
cp->setPen(ls->pen());
|
|
chart->addSeries(cp);
|
|
cp->attachAxis(ax);
|
|
if (SeriesSideMap().value(ls, 0) != 0) {
|
|
cp->attachAxis(yRight);
|
|
yRight->setVisible(true);
|
|
} else {
|
|
cp->attachAxis(yLeft);
|
|
}
|
|
z.series.append(cp);
|
|
}
|
|
|
|
QChartView* zv = new QChartView(chart);
|
|
zv->setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
QMainWindow* win = new QMainWindow();
|
|
win->setCentralWidget(zv);
|
|
win->resize(1100, 700);
|
|
|
|
z.win = win;
|
|
z.view = zv;
|
|
z.chart = chart;
|
|
z.x = ax;
|
|
z.yLeft = yLeft;
|
|
z.yRight = yRight;
|
|
}
|
|
|
|
z.win->setWindowTitle(title != nullptr ? QString::fromUtf8(title)
|
|
: QStringLiteral("训练曲线"));
|
|
// 先显示再同步: SyncZoomView 只在窗口可见时才拷贝, 首次打开也要有数据
|
|
z.win->show();
|
|
z.win->raise();
|
|
z.win->activateWindow();
|
|
SyncZoomView(src, z);
|
|
return z.win;
|
|
}
|
|
|
|
extern "C" VQT6_API void vqt6_chartview_sync_zoom(void* view) {
|
|
if (view == nullptr) return;
|
|
QChartView* src = static_cast<QChartView*>(view);
|
|
if (!ZoomMap().contains(src)) return;
|
|
SyncZoomView(src, ZoomMap()[src]);
|
|
}
|