Files
Viper_VRTE/vqt6/_bridge_canvas.cpp
TermiNexus 728d090815 feat(vqt6/vipermath/vipersimd/w32/requests): 自绘 Canvas/Graph3D/ChartView 与 Qt6 Charts、新增 gzip 与 torch-lite 模块, 修包装类悬垂指针与中文路径/exp/大文件下载
- 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
2026-09-10 23:09:05 +08:00

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]);
}