новый файл: CMakeLists.txt
новый файл: shell.nix новый файл: src/MainWindow.cpp новый файл: src/MainWindow.h новый файл: src/items/ArrowItem.cpp новый файл: src/items/ArrowItem.h новый файл: src/items/BlockItem.cpp новый файл: src/items/BlockItem.h новый файл: src/items/DiagramScene.cpp новый файл: src/items/DiagramScene.h новый файл: src/items/JunctionItem.cpp новый файл: src/items/JunctionItem.h новый файл: src/main.cpp изменено: src/items/ArrowItem.cpp изменено: src/items/ArrowItem.h изменено: src/items/BlockItem.h изменено: src/items/DiagramScene.cpp изменено: src/items/DiagramScene.h
This commit is contained in:
commit
53afa3f728
87 changed files with 48249 additions and 0 deletions
39
src/MainWindow.cpp
Normal file
39
src/MainWindow.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include "MainWindow.h"
|
||||
#include "items/DiagramScene.h"
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QToolBar>
|
||||
#include <QAction>
|
||||
#include <QStatusBar>
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
setupUi();
|
||||
setupActions();
|
||||
statusBar()->showMessage("RMB: add block. LMB on port: drag arrow. Double click block: rename.");
|
||||
}
|
||||
|
||||
void MainWindow::setupUi() {
|
||||
m_scene = new DiagramScene(this);
|
||||
m_view = new QGraphicsView(m_scene, this);
|
||||
m_view->setRenderHint(QPainter::Antialiasing, true);
|
||||
m_view->setDragMode(QGraphicsView::RubberBandDrag);
|
||||
// Полное обновление избавляет от графических артефактов во время drag временной стрелки.
|
||||
m_view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
|
||||
|
||||
setCentralWidget(m_view);
|
||||
|
||||
// стартовые блоки
|
||||
m_scene->createBlockAt(QPointF(-200, -50))->setPos(-300, -150);
|
||||
m_scene->createBlockAt(QPointF(200, -50))->setPos(200, -150);
|
||||
}
|
||||
|
||||
void MainWindow::setupActions() {
|
||||
auto* tb = addToolBar("Tools");
|
||||
auto* actFit = new QAction("Fit", this);
|
||||
connect(actFit, &QAction::triggered, this, [this]{
|
||||
m_view->fitInView(m_scene->itemsBoundingRect().adjusted(-50,-50,50,50), Qt::KeepAspectRatio);
|
||||
});
|
||||
tb->addAction(actFit);
|
||||
}
|
||||
18
src/MainWindow.h
Normal file
18
src/MainWindow.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
#include <QMainWindow>
|
||||
|
||||
class QGraphicsView;
|
||||
class DiagramScene;
|
||||
|
||||
class MainWindow final : public QMainWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
QGraphicsView* m_view = nullptr;
|
||||
DiagramScene* m_scene = nullptr;
|
||||
|
||||
void setupUi();
|
||||
void setupActions();
|
||||
};
|
||||
718
src/items/ArrowItem.cpp
Normal file
718
src/items/ArrowItem.cpp
Normal file
|
|
@ -0,0 +1,718 @@
|
|||
#include "ArrowItem.h"
|
||||
#include "BlockItem.h"
|
||||
#include "JunctionItem.h"
|
||||
|
||||
#include <QPen>
|
||||
#include <QtMath>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsSimpleTextItem>
|
||||
#include <QCursor>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include "DiagramScene.h"
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
|
||||
ArrowItem::ArrowItem(QGraphicsItem* parent)
|
||||
: QGraphicsPathItem(parent)
|
||||
{
|
||||
QPen pen(QColor(10,10,10));
|
||||
pen.setWidthF(1.4);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
setPen(pen);
|
||||
setZValue(-1); // стрелки под блоками
|
||||
|
||||
setAcceptHoverEvents(true);
|
||||
setAcceptedMouseButtons(Qt::LeftButton);
|
||||
|
||||
m_labelItem = new QGraphicsSimpleTextItem(this);
|
||||
m_labelItem->setBrush(QColor(10, 10, 10));
|
||||
m_labelItem->setZValue(2);
|
||||
m_labelItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
|
||||
m_labelItem->setVisible(false);
|
||||
m_labelOffset = QPointF(0, 0);
|
||||
}
|
||||
|
||||
ArrowItem::~ArrowItem() {
|
||||
if (m_from.block) m_from.block->removeArrow(this);
|
||||
if (m_to.block) m_to.block->removeArrow(this);
|
||||
if (m_from.junction) m_from.junction->removeArrow(this);
|
||||
if (m_to.junction) m_to.junction->removeArrow(this);
|
||||
}
|
||||
|
||||
void ArrowItem::setLabel(const QString& text) {
|
||||
if (text == m_label) return;
|
||||
m_label = text;
|
||||
if (m_labelItem) {
|
||||
m_labelItem->setText(m_label);
|
||||
m_labelItem->setVisible(!m_label.isEmpty());
|
||||
}
|
||||
updateLabelItem(computePolyline());
|
||||
}
|
||||
|
||||
void ArrowItem::setOffsets(qreal bend, qreal top, qreal bottom) {
|
||||
m_bendOffset = bend;
|
||||
m_topOffset = top;
|
||||
m_bottomOffset = bottom;
|
||||
updatePath();
|
||||
}
|
||||
|
||||
void ArrowItem::setLabelOffset(const QPointF& off) {
|
||||
m_labelOffset = off;
|
||||
updateLabelItem(m_lastPolyline.isEmpty() ? computePolyline() : m_lastPolyline);
|
||||
}
|
||||
|
||||
void ArrowItem::setFrom(const Endpoint& ep) {
|
||||
if (m_from.block) m_from.block->removeArrow(this);
|
||||
if (m_from.junction) m_from.junction->removeArrow(this);
|
||||
m_from = ep;
|
||||
if (m_from.block) m_from.block->addArrow(this);
|
||||
if (m_from.junction) m_from.junction->addArrow(this);
|
||||
updatePath();
|
||||
}
|
||||
|
||||
void ArrowItem::setTo(const Endpoint& ep) {
|
||||
if (m_to.block) m_to.block->removeArrow(this);
|
||||
if (m_to.junction) m_to.junction->removeArrow(this);
|
||||
m_to = ep;
|
||||
if (m_to.block) m_to.block->addArrow(this);
|
||||
if (m_to.junction) m_to.junction->addArrow(this);
|
||||
m_hasTempEnd = false;
|
||||
updatePath();
|
||||
}
|
||||
|
||||
void ArrowItem::setTempEndPoint(const QPointF& scenePos) {
|
||||
m_hasTempEnd = true;
|
||||
m_tempEndScene = scenePos;
|
||||
updatePath();
|
||||
}
|
||||
|
||||
void ArrowItem::finalize() {
|
||||
m_hasTempEnd = false;
|
||||
updatePath();
|
||||
}
|
||||
|
||||
static QPointF scenePortPos(const ArrowItem::Endpoint& ep) {
|
||||
if (ep.junction) {
|
||||
if (ep.localPos) return ep.junction->mapToScene(*ep.localPos);
|
||||
return ep.junction->scenePos();
|
||||
}
|
||||
if (ep.scenePos) return *ep.scenePos;
|
||||
if (ep.block) {
|
||||
if (ep.localPos) return ep.block->mapToScene(*ep.localPos);
|
||||
return ep.block->portScenePos(ep.port);
|
||||
}
|
||||
return QPointF();
|
||||
}
|
||||
|
||||
qreal ArrowItem::distToSegment(const QPointF& p, const QPointF& a, const QPointF& b) {
|
||||
const QPointF ab = b - a;
|
||||
const qreal ab2 = ab.x()*ab.x() + ab.y()*ab.y();
|
||||
if (ab2 < 1e-6) return std::hypot(p.x() - a.x(), p.y() - a.y());
|
||||
const qreal t = std::clamp(((p - a).x()*ab.x() + (p - a).y()*ab.y()) / ab2, 0.0, 1.0);
|
||||
const QPointF proj = a + ab * t;
|
||||
return std::hypot(p.x() - proj.x(), p.y() - proj.y());
|
||||
}
|
||||
|
||||
static QPointF normalizedOr(const QPointF& v, const QPointF& fallback) {
|
||||
qreal len = std::hypot(v.x(), v.y());
|
||||
if (len < 1e-6) return fallback;
|
||||
return v / len;
|
||||
}
|
||||
|
||||
static bool almostEqual(qreal a, qreal b, qreal eps = 1e-6) {
|
||||
return std::abs(a - b) < eps;
|
||||
}
|
||||
|
||||
static bool almostEqual(const QPointF& a, const QPointF& b, qreal eps = 1e-6) {
|
||||
return almostEqual(a.x(), b.x(), eps) && almostEqual(a.y(), b.y(), eps);
|
||||
}
|
||||
|
||||
static bool pointInside(const QPointF& p, const QRectF& r) {
|
||||
return p.x() > r.left() && p.x() < r.right() && p.y() > r.top() && p.y() < r.bottom();
|
||||
}
|
||||
|
||||
static bool segmentBlocked(const QPointF& a, const QPointF& b, const QVector<QRectF>& obstacles) {
|
||||
const bool vertical = almostEqual(a.x(), b.x());
|
||||
const bool horizontal = almostEqual(a.y(), b.y());
|
||||
if (!vertical && !horizontal) {
|
||||
return true; // Manhattan only
|
||||
}
|
||||
|
||||
for (const auto& r : obstacles) {
|
||||
if (vertical) {
|
||||
const qreal x = a.x();
|
||||
if (x > r.left() && x < r.right()) {
|
||||
const qreal lo = std::min(a.y(), b.y());
|
||||
const qreal hi = std::max(a.y(), b.y());
|
||||
if (hi > r.top() && lo < r.bottom()) return true;
|
||||
}
|
||||
} else if (horizontal) {
|
||||
const qreal y = a.y();
|
||||
if (y > r.top() && y < r.bottom()) {
|
||||
const qreal lo = std::min(a.x(), b.x());
|
||||
const qreal hi = std::max(a.x(), b.x());
|
||||
if (hi > r.left() && lo < r.right()) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static QVector<QPointF> manhattanRoute(const QPointF& start,
|
||||
const QPointF& end,
|
||||
const QVector<QRectF>& obstacles,
|
||||
const QRectF& sceneRect,
|
||||
const QVector<qreal>& extraXs,
|
||||
const QVector<qreal>& extraYs) {
|
||||
QVector<qreal> xs = extraXs;
|
||||
QVector<qreal> ys = extraYs;
|
||||
xs << start.x() << end.x();
|
||||
ys << start.y() << end.y();
|
||||
|
||||
for (const auto& r : obstacles) {
|
||||
xs << r.left() << r.right();
|
||||
ys << r.top() << r.bottom();
|
||||
}
|
||||
if (!sceneRect.isNull()) {
|
||||
xs << sceneRect.left() << sceneRect.right();
|
||||
ys << sceneRect.top() << sceneRect.bottom();
|
||||
}
|
||||
|
||||
auto dedupSort = [](QVector<qreal>& v) {
|
||||
std::sort(v.begin(), v.end());
|
||||
v.erase(std::unique(v.begin(), v.end(), [](qreal lhs, qreal rhs) {
|
||||
return almostEqual(lhs, rhs);
|
||||
}), v.end());
|
||||
};
|
||||
dedupSort(xs);
|
||||
dedupSort(ys);
|
||||
|
||||
const int nx = xs.size();
|
||||
const int ny = ys.size();
|
||||
if (nx == 0 || ny == 0) return {};
|
||||
|
||||
struct Node {
|
||||
QPointF pos;
|
||||
QVector<int> neighbors;
|
||||
};
|
||||
|
||||
QVector<Node> nodes;
|
||||
nodes.reserve(nx * ny);
|
||||
QVector<int> grid(nx * ny, -1);
|
||||
|
||||
auto gridIndex = [nx](int xi, int yi) { return yi * nx + xi; };
|
||||
|
||||
auto registerNode = [&](int xi, int yi) -> int {
|
||||
const QPointF p(xs[xi], ys[yi]);
|
||||
for (const auto& r : obstacles) {
|
||||
if (pointInside(p, r)) return -1;
|
||||
}
|
||||
const int idx = nodes.size();
|
||||
nodes.push_back(Node{p, {}});
|
||||
grid[gridIndex(xi, yi)] = idx;
|
||||
return idx;
|
||||
};
|
||||
|
||||
int startIdx = -1;
|
||||
int endIdx = -1;
|
||||
|
||||
for (int yi = 0; yi < ny; ++yi) {
|
||||
for (int xi = 0; xi < nx; ++xi) {
|
||||
const int idx = registerNode(xi, yi);
|
||||
if (idx < 0) continue;
|
||||
const QPointF p = nodes[idx].pos;
|
||||
if (startIdx < 0 && almostEqual(p, start)) startIdx = idx;
|
||||
if (endIdx < 0 && almostEqual(p, end)) endIdx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIdx < 0 || endIdx < 0) return {};
|
||||
|
||||
// Horizontal connections
|
||||
for (int yi = 0; yi < ny; ++yi) {
|
||||
int lastIdx = -1;
|
||||
QPointF lastPt;
|
||||
for (int xi = 0; xi < nx; ++xi) {
|
||||
const int idx = grid[gridIndex(xi, yi)];
|
||||
if (idx < 0) continue;
|
||||
const QPointF pt(xs[xi], ys[yi]);
|
||||
if (lastIdx >= 0) {
|
||||
if (!segmentBlocked(lastPt, pt, obstacles)) {
|
||||
nodes[lastIdx].neighbors.push_back(idx);
|
||||
nodes[idx].neighbors.push_back(lastIdx);
|
||||
} else {
|
||||
lastIdx = idx;
|
||||
lastPt = pt;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
lastIdx = idx;
|
||||
lastPt = pt;
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical connections
|
||||
for (int xi = 0; xi < nx; ++xi) {
|
||||
int lastIdx = -1;
|
||||
QPointF lastPt;
|
||||
for (int yi = 0; yi < ny; ++yi) {
|
||||
const int idx = grid[gridIndex(xi, yi)];
|
||||
if (idx < 0) continue;
|
||||
const QPointF pt(xs[xi], ys[yi]);
|
||||
if (lastIdx >= 0) {
|
||||
if (!segmentBlocked(lastPt, pt, obstacles)) {
|
||||
nodes[lastIdx].neighbors.push_back(idx);
|
||||
nodes[idx].neighbors.push_back(lastIdx);
|
||||
} else {
|
||||
lastIdx = idx;
|
||||
lastPt = pt;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
lastIdx = idx;
|
||||
lastPt = pt;
|
||||
}
|
||||
}
|
||||
|
||||
const int n = nodes.size();
|
||||
QVector<qreal> gScore(n, std::numeric_limits<qreal>::infinity());
|
||||
QVector<int> cameFrom(n, -1);
|
||||
QVector<bool> closed(n, false);
|
||||
|
||||
auto manhattan = [](const QPointF& p0, const QPointF& p1) {
|
||||
return std::abs(p0.x() - p1.x()) + std::abs(p0.y() - p1.y());
|
||||
};
|
||||
|
||||
struct QueueItem {
|
||||
int idx;
|
||||
qreal f;
|
||||
};
|
||||
auto cmp = [](const QueueItem& lhs, const QueueItem& rhs) { return lhs.f > rhs.f; };
|
||||
std::priority_queue<QueueItem, std::vector<QueueItem>, decltype(cmp)> open(cmp);
|
||||
|
||||
gScore[startIdx] = 0.0;
|
||||
open.push({startIdx, manhattan(nodes[startIdx].pos, nodes[endIdx].pos)});
|
||||
|
||||
while (!open.empty()) {
|
||||
const int current = open.top().idx;
|
||||
open.pop();
|
||||
if (closed[current]) continue;
|
||||
closed[current] = true;
|
||||
if (current == endIdx) break;
|
||||
|
||||
for (int nb : nodes[current].neighbors) {
|
||||
if (closed[nb]) continue;
|
||||
const qreal tentative = gScore[current] + manhattan(nodes[current].pos, nodes[nb].pos);
|
||||
if (tentative + 1e-6 < gScore[nb]) {
|
||||
cameFrom[nb] = current;
|
||||
gScore[nb] = tentative;
|
||||
const qreal fScore = tentative + manhattan(nodes[nb].pos, nodes[endIdx].pos);
|
||||
open.push({nb, fScore});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVector<QPointF> path;
|
||||
if (cameFrom[endIdx] == -1 && startIdx != endIdx) {
|
||||
return path; // unreachable
|
||||
}
|
||||
|
||||
int cur = endIdx;
|
||||
path.push_front(nodes[cur].pos);
|
||||
while (cur != startIdx) {
|
||||
cur = cameFrom[cur];
|
||||
if (cur < 0) break;
|
||||
path.push_front(nodes[cur].pos);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
QPointF ArrowItem::portDirection(const Endpoint& ep, bool isFrom, const QPointF& fallbackDir, const QGraphicsScene* scene) {
|
||||
if (ep.junction) return QPointF(); // junction is isotropic
|
||||
if (ep.scenePos) {
|
||||
const QRectF r = scene ? scene->sceneRect() : QRectF();
|
||||
const QPointF p = *ep.scenePos;
|
||||
const qreal dl = std::abs(p.x() - r.left());
|
||||
const qreal dr = std::abs(p.x() - r.right());
|
||||
const qreal dt = std::abs(p.y() - r.top());
|
||||
const qreal db = std::abs(p.y() - r.bottom());
|
||||
const qreal m = std::min({dl, dr, dt, db});
|
||||
if (m == dl) return isFrom ? QPointF(1,0) : QPointF(-1,0);
|
||||
if (m == dr) return isFrom ? QPointF(-1,0) : QPointF(1,0); // right edge is target-only
|
||||
if (m == dt) return isFrom ? QPointF(0,1) : QPointF(0,-1);
|
||||
if (m == db) return isFrom ? QPointF(0,-1) : QPointF(0,1);
|
||||
}
|
||||
switch (ep.port) {
|
||||
case BlockItem::Port::Input: return QPointF(-1, 0);
|
||||
case BlockItem::Port::Output: return QPointF(1, 0);
|
||||
case BlockItem::Port::Control: return QPointF(0, -1);
|
||||
case BlockItem::Port::Mechanism: return QPointF(0, 1);
|
||||
}
|
||||
return fallbackDir;
|
||||
}
|
||||
|
||||
QVector<QPointF> ArrowItem::simplifyPolyline(const QVector<QPointF>& ptsIn) {
|
||||
QVector<QPointF> pts = ptsIn;
|
||||
// убрать дубли
|
||||
pts.erase(std::unique(pts.begin(), pts.end(), [](const QPointF& lhs, const QPointF& rhs){
|
||||
return qFuzzyCompare(lhs.x(), rhs.x()) && qFuzzyCompare(lhs.y(), rhs.y());
|
||||
}), pts.end());
|
||||
|
||||
auto isCollinear = [](const QPointF& a, const QPointF& b, const QPointF& c) {
|
||||
const QPointF ab = b - a;
|
||||
const QPointF bc = c - b;
|
||||
const qreal cross = ab.x()*bc.y() - ab.y()*bc.x();
|
||||
if (std::abs(cross) > 1e-6) return false;
|
||||
return (ab.x()*bc.x() + ab.y()*bc.y()) >= 0;
|
||||
};
|
||||
|
||||
for (int i = 0; i + 2 < pts.size();) {
|
||||
if (isCollinear(pts[i], pts[i+1], pts[i+2])) {
|
||||
pts.removeAt(i+1);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
QVector<QPointF> ArrowItem::computePolyline() const {
|
||||
const qreal gap = 24.0;
|
||||
QPointF a = scenePortPos(m_from);
|
||||
QPointF b = m_hasTempEnd ? m_tempEndScene : scenePortPos(m_to);
|
||||
|
||||
// Направления
|
||||
QPointF dirA = portDirection(m_from, true, QPointF(1,0), scene());
|
||||
QPointF dirB = m_hasTempEnd
|
||||
? normalizedOr((std::abs((a - b).x()) >= std::abs((a - b).y()))
|
||||
? QPointF(((a - b).x() >= 0 ? 1 : -1), 0)
|
||||
: QPointF(0, ((a - b).y() >= 0 ? 1 : -1)),
|
||||
QPointF(-1,0))
|
||||
: portDirection(m_to, false, QPointF(-1,0), scene());
|
||||
|
||||
auto expandRect = [](const QRectF& r, qreal m) {
|
||||
return QRectF(r.x()-m, r.y()-m, r.width()+2*m, r.height()+2*m);
|
||||
};
|
||||
|
||||
const QPointF startBase = a + dirA * gap;
|
||||
const QPointF endBase = b + dirB * gap;
|
||||
const bool startHorizontal = std::abs(dirA.x()) >= std::abs(dirA.y());
|
||||
const bool endHorizontal = std::abs(dirB.x()) >= std::abs(dirB.y());
|
||||
const QPointF start = startBase + (startHorizontal ? QPointF(0, m_topOffset) : QPointF());
|
||||
const QPointF end = endBase + (endHorizontal ? QPointF(0, m_bottomOffset) : QPointF());
|
||||
|
||||
QVector<QRectF> obstacles;
|
||||
if (scene()) {
|
||||
const auto itemsInScene = scene()->items();
|
||||
for (QGraphicsItem* it : itemsInScene) {
|
||||
if (auto* blk = qgraphicsitem_cast<BlockItem*>(it)) {
|
||||
obstacles << expandRect(blk->mapRectToScene(blk->boundingRect()), gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVector<qreal> xHints;
|
||||
QVector<qreal> yHints;
|
||||
xHints << startBase.x() << endBase.x() << a.x() << b.x();
|
||||
yHints << startBase.y() << endBase.y() << a.y() << b.y();
|
||||
if (!almostEqual(m_bendOffset, 0.0)) {
|
||||
xHints << start.x() + m_bendOffset << end.x() + m_bendOffset;
|
||||
yHints << start.y() + m_bendOffset << end.y() + m_bendOffset;
|
||||
}
|
||||
|
||||
const QRectF sceneBounds = scene() ? scene()->sceneRect() : QRectF();
|
||||
QVector<QPointF> spine = manhattanRoute(start, end, obstacles, sceneBounds, xHints, yHints);
|
||||
if (spine.isEmpty()) {
|
||||
spine << start;
|
||||
if (!almostEqual(start.x(), end.x()) && !almostEqual(start.y(), end.y())) {
|
||||
spine << QPointF(end.x(), start.y());
|
||||
}
|
||||
spine << end;
|
||||
}
|
||||
|
||||
QVector<QPointF> pts;
|
||||
pts << a;
|
||||
if (!almostEqual(a, startBase)) pts << startBase;
|
||||
for (const QPointF& p : spine) {
|
||||
if (pts.isEmpty() || !almostEqual(pts.last(), p)) pts << p;
|
||||
}
|
||||
if (!almostEqual(end, endBase)) pts << endBase;
|
||||
pts << b;
|
||||
|
||||
return simplifyPolyline(pts);
|
||||
}
|
||||
|
||||
std::optional<QPointF> ArrowItem::hitTest(const QPointF& scenePos, qreal radius) const {
|
||||
const QVector<QPointF> pts = computePolyline();
|
||||
std::optional<QPointF> bestPoint;
|
||||
qreal bestDist = radius;
|
||||
|
||||
for (int i = 0; i + 1 < pts.size(); ++i) {
|
||||
const QPointF a = pts[i];
|
||||
const QPointF b = pts[i+1];
|
||||
const bool vertical = almostEqual(a.x(), b.x());
|
||||
const bool horizontal = almostEqual(a.y(), b.y());
|
||||
if (!vertical && !horizontal) continue;
|
||||
|
||||
const qreal d = distToSegment(scenePos, a, b);
|
||||
if (d > bestDist + 1e-6) continue;
|
||||
|
||||
QPointF proj;
|
||||
if (vertical) {
|
||||
const qreal y = std::clamp(scenePos.y(), std::min(a.y(), b.y()), std::max(a.y(), b.y()));
|
||||
proj = QPointF(a.x(), y);
|
||||
} else {
|
||||
const qreal x = std::clamp(scenePos.x(), std::min(a.x(), b.x()), std::max(a.x(), b.x()));
|
||||
proj = QPointF(x, a.y());
|
||||
}
|
||||
|
||||
const qreal toA = std::hypot(proj.x() - a.x(), proj.y() - a.y());
|
||||
const qreal toB = std::hypot(proj.x() - b.x(), proj.y() - b.y());
|
||||
if (toA < 2.0 || toB < 2.0) continue; // skip near endpoints
|
||||
|
||||
bestDist = d;
|
||||
bestPoint = proj;
|
||||
}
|
||||
|
||||
return bestPoint;
|
||||
}
|
||||
|
||||
void ArrowItem::updateLabelItem(const QVector<QPointF>& pts) {
|
||||
if (!m_labelItem) return;
|
||||
if (m_label.isEmpty() || pts.size() < 2) {
|
||||
m_labelItem->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
qreal totalLen = 0.0;
|
||||
for (int i = 0; i + 1 < pts.size(); ++i) {
|
||||
totalLen += std::abs(pts[i+1].x() - pts[i].x()) + std::abs(pts[i+1].y() - pts[i].y());
|
||||
}
|
||||
if (totalLen < 1e-3) {
|
||||
m_labelItem->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal target = totalLen * 0.5;
|
||||
qreal acc = 0.0;
|
||||
QPointF pos = pts.first();
|
||||
for (int i = 0; i + 1 < pts.size(); ++i) {
|
||||
const QPointF a = pts[i];
|
||||
const QPointF b = pts[i+1];
|
||||
const qreal segLen = std::abs(b.x() - a.x()) + std::abs(b.y() - a.y());
|
||||
if (segLen < 1e-6) continue;
|
||||
|
||||
if (acc + segLen >= target) {
|
||||
const qreal t = (target - acc) / segLen;
|
||||
pos = a + (b - a) * t;
|
||||
break;
|
||||
}
|
||||
acc += segLen;
|
||||
pos = b;
|
||||
}
|
||||
|
||||
const QRectF br = m_labelItem->boundingRect();
|
||||
m_labelItem->setPos(pos + m_labelOffset - QPointF(br.width() * 0.5, br.height() * 0.5));
|
||||
m_labelItem->setVisible(true);
|
||||
}
|
||||
|
||||
// Обновляем маршрут ортогонально, добавляя/схлопывая сегменты при необходимости.
|
||||
void ArrowItem::updatePath() {
|
||||
const QRectF oldSceneRect = mapRectToScene(boundingRect());
|
||||
|
||||
QVector<QPointF> pts = computePolyline();
|
||||
m_lastPolyline = pts;
|
||||
|
||||
QPainterPath p;
|
||||
if (!pts.isEmpty()) {
|
||||
p.moveTo(pts.first());
|
||||
for (int i = 1; i < pts.size(); ++i) {
|
||||
p.lineTo(pts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// наконечник стрелки
|
||||
if (pts.size() >= 2) {
|
||||
const QPointF tip = pts.last();
|
||||
const QPointF from = pts[pts.size() - 2];
|
||||
drawArrowHead(p, tip, from);
|
||||
}
|
||||
|
||||
// Обновляем геометрию, чтобы сцена инвалидацировала старую и новую области.
|
||||
prepareGeometryChange();
|
||||
setPath(p);
|
||||
updateLabelItem(pts);
|
||||
|
||||
// Явно инвалидируем область (фон + элементы), чтобы очистить следы при drag.
|
||||
if (scene()) {
|
||||
const QRectF newSceneRect = mapRectToScene(boundingRect());
|
||||
scene()->invalidate(oldSceneRect.united(newSceneRect),
|
||||
QGraphicsScene::BackgroundLayer | QGraphicsScene::ItemLayer);
|
||||
}
|
||||
}
|
||||
|
||||
void ArrowItem::hoverMoveEvent(QGraphicsSceneHoverEvent* e) {
|
||||
const qreal tol = 6.0;
|
||||
const QVector<QPointF> pts = computePolyline();
|
||||
QPointF pos = e->scenePos();
|
||||
|
||||
Qt::CursorShape cursor = Qt::ArrowCursor;
|
||||
for (int i = 0; i + 1 < pts.size(); ++i) {
|
||||
const QPointF a = pts[i];
|
||||
const QPointF b = pts[i+1];
|
||||
if (std::abs(a.x() - b.x()) < 1e-6) {
|
||||
if (distToSegment(pos, a, b) <= tol) { cursor = Qt::SizeHorCursor; break; }
|
||||
} else if (std::abs(a.y() - b.y()) < 1e-6) {
|
||||
if (distToSegment(pos, a, b) <= tol) { cursor = Qt::SizeVerCursor; break; }
|
||||
}
|
||||
}
|
||||
setCursor(cursor);
|
||||
QGraphicsPathItem::hoverMoveEvent(e);
|
||||
}
|
||||
|
||||
void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
QGraphicsPathItem::mousePressEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_labelItem && m_labelItem->isVisible() &&
|
||||
m_labelItem->sceneBoundingRect().adjusted(-6, -4, 6, 4).contains(e->scenePos())) {
|
||||
m_labelDragging = true;
|
||||
m_labelDragLastScene = e->scenePos();
|
||||
setCursor(Qt::SizeAllCursor);
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal tol = 6.0;
|
||||
const QVector<QPointF> pts = computePolyline();
|
||||
const QPointF pos = e->scenePos();
|
||||
|
||||
m_dragPart = DragPart::None;
|
||||
int closestIdx = -1;
|
||||
qreal best = tol;
|
||||
for (int i = 0; i + 1 < pts.size(); ++i) {
|
||||
const QPointF a = pts[i], b = pts[i+1];
|
||||
if (std::abs(a.x() - b.x()) < 1e-6 || std::abs(a.y() - b.y()) < 1e-6) {
|
||||
qreal d = distToSegment(pos, a, b);
|
||||
if (d <= best) {
|
||||
best = d;
|
||||
closestIdx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closestIdx >= 0) {
|
||||
const QPointF a = pts[closestIdx], b = pts[closestIdx+1];
|
||||
if (std::abs(a.x() - b.x()) < 1e-6) {
|
||||
m_dragPart = DragPart::BendX; // вертикальный сегмент
|
||||
} else {
|
||||
// горизонтальный — решаем, к какому концу ближе
|
||||
m_dragPart = (closestIdx <= (pts.size() / 2)) ? DragPart::TopHorizontal : DragPart::BottomHorizontal;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_dragPart != DragPart::None) {
|
||||
m_lastDragScenePos = e->scenePos();
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
QGraphicsPathItem::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void ArrowItem::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (m_labelDragging) {
|
||||
const QPointF delta = e->scenePos() - m_labelDragLastScene;
|
||||
m_labelDragLastScene = e->scenePos();
|
||||
m_labelOffset += delta;
|
||||
updateLabelItem(m_lastPolyline.isEmpty() ? computePolyline() : m_lastPolyline);
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_dragPart == DragPart::None) {
|
||||
QGraphicsPathItem::mouseMoveEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const QPointF delta = e->scenePos() - m_lastDragScenePos;
|
||||
m_lastDragScenePos = e->scenePos();
|
||||
|
||||
switch (m_dragPart) {
|
||||
case DragPart::BendX:
|
||||
m_bendOffset += delta.x();
|
||||
break;
|
||||
case DragPart::TopHorizontal:
|
||||
m_topOffset += delta.y();
|
||||
break;
|
||||
case DragPart::BottomHorizontal:
|
||||
m_bottomOffset += delta.y();
|
||||
break;
|
||||
case DragPart::None:
|
||||
break;
|
||||
}
|
||||
|
||||
updatePath();
|
||||
e->accept();
|
||||
}
|
||||
|
||||
void ArrowItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (m_labelDragging && e->button() == Qt::LeftButton) {
|
||||
m_labelDragging = false;
|
||||
unsetCursor();
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene())) {
|
||||
ds->requestSnapshot();
|
||||
}
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (m_dragPart != DragPart::None && e->button() == Qt::LeftButton) {
|
||||
m_dragPart = DragPart::None;
|
||||
unsetCursor();
|
||||
if (auto* ds = qobject_cast<DiagramScene*>(scene())) {
|
||||
ds->requestSnapshot();
|
||||
}
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsPathItem::mouseReleaseEvent(e);
|
||||
}
|
||||
|
||||
void ArrowItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
bool ok = false;
|
||||
const QString text = QInputDialog::getText(nullptr, "Arrow label", "Label:", QLineEdit::Normal, m_label, &ok);
|
||||
if (ok) {
|
||||
setLabel(text);
|
||||
if (text.isEmpty()) m_labelOffset = QPointF();
|
||||
}
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsPathItem::mouseDoubleClickEvent(e);
|
||||
}
|
||||
|
||||
void ArrowItem::drawArrowHead(QPainterPath& path, const QPointF& tip, const QPointF& from) {
|
||||
const QPointF v = tip - from;
|
||||
const qreal len = std::hypot(v.x(), v.y());
|
||||
if (len < 0.001) return;
|
||||
|
||||
const QPointF dir = v / len;
|
||||
const QPointF n(-dir.y(), dir.x());
|
||||
|
||||
const qreal size = 10.0;
|
||||
const QPointF back = tip - dir * size;
|
||||
const QPointF left = back + n * (size * 0.45);
|
||||
const QPointF right = back - n * (size * 0.45);
|
||||
|
||||
path.moveTo(left);
|
||||
path.lineTo(tip);
|
||||
path.lineTo(right);
|
||||
}
|
||||
80
src/items/ArrowItem.h
Normal file
80
src/items/ArrowItem.h
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#pragma once
|
||||
#include <QGraphicsPathItem>
|
||||
#include <QPointer>
|
||||
#include <optional>
|
||||
#include <QString>
|
||||
|
||||
#include "BlockItem.h"
|
||||
class JunctionItem;
|
||||
class QGraphicsSimpleTextItem;
|
||||
|
||||
class ArrowItem final : public QGraphicsPathItem {
|
||||
public:
|
||||
enum { Type = UserType + 3 };
|
||||
struct Endpoint {
|
||||
QPointer<BlockItem> block;
|
||||
QPointer<JunctionItem> junction;
|
||||
BlockItem::Port port = BlockItem::Port::Input;
|
||||
std::optional<QPointF> localPos; // точка на стороне блока (в локальных координатах)
|
||||
std::optional<QPointF> scenePos; // произвольная точка на сцене (например, граница)
|
||||
};
|
||||
|
||||
ArrowItem(QGraphicsItem* parent = nullptr);
|
||||
~ArrowItem() override;
|
||||
|
||||
void setFrom(const Endpoint& ep);
|
||||
void setTo(const Endpoint& ep);
|
||||
|
||||
void setTempEndPoint(const QPointF& scenePos); // когда тянем мышью
|
||||
void finalize(); // убираем "временный" режим, если надо
|
||||
|
||||
Endpoint from() const { return m_from; }
|
||||
Endpoint to() const { return m_to; }
|
||||
|
||||
QString label() const { return m_label; }
|
||||
void setLabel(const QString& text);
|
||||
QPointF labelOffset() const { return m_labelOffset; }
|
||||
void setOffsets(qreal bend, qreal top, qreal bottom);
|
||||
void setLabelOffset(const QPointF& off);
|
||||
qreal bendOffset() const { return m_bendOffset; }
|
||||
qreal topOffset() const { return m_topOffset; }
|
||||
qreal bottomOffset() const { return m_bottomOffset; }
|
||||
int type() const override { return Type; }
|
||||
|
||||
void updatePath();
|
||||
std::optional<QPointF> hitTest(const QPointF& scenePos, qreal radius) const;
|
||||
|
||||
private:
|
||||
enum class DragPart { None, BendX, TopHorizontal, BottomHorizontal };
|
||||
|
||||
Endpoint m_from;
|
||||
Endpoint m_to;
|
||||
bool m_hasTempEnd = false;
|
||||
QPointF m_tempEndScene;
|
||||
qreal m_bendOffset = 0.0;
|
||||
qreal m_topOffset = 0.0;
|
||||
qreal m_bottomOffset = 0.0;
|
||||
QString m_label;
|
||||
QGraphicsSimpleTextItem* m_labelItem = nullptr;
|
||||
QPointF m_labelOffset; // in scene-space relative to anchor point
|
||||
bool m_labelDragging = false;
|
||||
QPointF m_labelDragLastScene;
|
||||
QVector<QPointF> m_lastPolyline;
|
||||
|
||||
DragPart m_dragPart = DragPart::None;
|
||||
QPointF m_lastDragScenePos;
|
||||
|
||||
void drawArrowHead(QPainterPath& path, const QPointF& tip, const QPointF& from);
|
||||
QVector<QPointF> computePolyline() const;
|
||||
static QPointF portDirection(const Endpoint& ep, bool isFrom, const QPointF& fallbackDir, const QGraphicsScene* scene);
|
||||
static QVector<QPointF> simplifyPolyline(const QVector<QPointF>& pts);
|
||||
static qreal distToSegment(const QPointF& p, const QPointF& a, const QPointF& b);
|
||||
void updateLabelItem(const QVector<QPointF>& pts);
|
||||
|
||||
protected:
|
||||
void hoverMoveEvent(QGraphicsSceneHoverEvent* e) override;
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
};
|
||||
151
src/items/BlockItem.cpp
Normal file
151
src/items/BlockItem.cpp
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#include "BlockItem.h"
|
||||
#include "ArrowItem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
#include <QPainter>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QInputDialog>
|
||||
|
||||
BlockItem::BlockItem(QString title, QGraphicsItem* parent, int id)
|
||||
: QGraphicsObject(parent),
|
||||
m_title(std::move(title)),
|
||||
m_rect(0, 0, 200, 100),
|
||||
m_id(id)
|
||||
{
|
||||
setFlags(ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges);
|
||||
}
|
||||
|
||||
QRectF BlockItem::boundingRect() const {
|
||||
// небольшой запас под обводку/выделение
|
||||
return m_rect.adjusted(-2, -2, 2, 2);
|
||||
}
|
||||
|
||||
void BlockItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) {
|
||||
p->setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
// фон
|
||||
p->setPen(Qt::NoPen);
|
||||
p->setBrush(isSelected() ? QColor(240,240,255) : QColor(250,250,250));
|
||||
p->drawRoundedRect(m_rect, 6, 6);
|
||||
|
||||
// рамка
|
||||
QPen pen(QColor(30,30,30));
|
||||
pen.setWidthF(1.5);
|
||||
p->setPen(pen);
|
||||
p->setBrush(Qt::NoBrush);
|
||||
p->drawRoundedRect(m_rect, 6, 6);
|
||||
|
||||
// заголовок
|
||||
p->setPen(QColor(20,20,20));
|
||||
p->drawText(m_rect.adjusted(10, 8, -10, -8), Qt::AlignLeft | Qt::AlignTop, m_title);
|
||||
}
|
||||
|
||||
void BlockItem::setTitle(const QString& t) {
|
||||
if (t == m_title) return;
|
||||
m_title = t;
|
||||
update();
|
||||
}
|
||||
|
||||
QPointF BlockItem::portLocalPos(Port p) const {
|
||||
const qreal x0 = m_rect.left();
|
||||
const qreal x1 = m_rect.right();
|
||||
const qreal y0 = m_rect.top();
|
||||
const qreal y1 = m_rect.bottom();
|
||||
const qreal yc = m_rect.center().y();
|
||||
const qreal xc = m_rect.center().x();
|
||||
|
||||
switch (p) {
|
||||
case Port::Input: return QPointF(x0, yc);
|
||||
case Port::Output: return QPointF(x1, yc);
|
||||
case Port::Control: return QPointF(xc, y0);
|
||||
case Port::Mechanism: return QPointF(xc, y1);
|
||||
}
|
||||
return QPointF();
|
||||
}
|
||||
|
||||
QPair<QPointF, QPointF> BlockItem::portSegment(Port p) const {
|
||||
const qreal x0 = m_rect.left();
|
||||
const qreal x1 = m_rect.right();
|
||||
const qreal y0 = m_rect.top();
|
||||
const qreal y1 = m_rect.bottom();
|
||||
|
||||
switch (p) {
|
||||
case Port::Input: return {QPointF(x0, y0), QPointF(x0, y1)};
|
||||
case Port::Output: return {QPointF(x1, y0), QPointF(x1, y1)};
|
||||
case Port::Control: return {QPointF(x0, y0), QPointF(x1, y0)};
|
||||
case Port::Mechanism: return {QPointF(x0, y1), QPointF(x1, y1)};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QPointF BlockItem::portScenePos(Port p) const {
|
||||
return mapToScene(portLocalPos(p));
|
||||
}
|
||||
|
||||
static qreal distToSegmentLocal(const QPointF& p, const QPointF& a, const QPointF& b, QPointF* outProj = nullptr) {
|
||||
const QPointF ab = b - a;
|
||||
const qreal ab2 = ab.x()*ab.x() + ab.y()*ab.y();
|
||||
if (ab2 < 1e-6) {
|
||||
if (outProj) *outProj = a;
|
||||
return std::hypot(p.x() - a.x(), p.y() - a.y());
|
||||
}
|
||||
const qreal t = std::clamp(((p - a).x()*ab.x() + (p - a).y()*ab.y()) / ab2, 0.0, 1.0);
|
||||
const QPointF proj = a + ab * t;
|
||||
if (outProj) *outProj = proj;
|
||||
return std::hypot(p.x() - proj.x(), p.y() - proj.y());
|
||||
}
|
||||
|
||||
bool BlockItem::hitTestPort(const QPointF& scenePos, Port* outPort, QPointF* outLocalPos, qreal radiusPx) const {
|
||||
const QPointF local = mapFromScene(scenePos);
|
||||
|
||||
qreal bestDist = std::numeric_limits<qreal>::max();
|
||||
Port bestPort = Port::Input;
|
||||
QPointF bestProj;
|
||||
|
||||
const Port ports[] = {Port::Input, Port::Control, Port::Output, Port::Mechanism};
|
||||
for (Port p : ports) {
|
||||
const auto seg = portSegment(p);
|
||||
QPointF proj;
|
||||
const qreal d = distToSegmentLocal(local, seg.first, seg.second, &proj);
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestPort = p;
|
||||
bestProj = proj;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDist <= radiusPx) {
|
||||
if (outPort) *outPort = bestPort;
|
||||
if (outLocalPos) *outLocalPos = bestProj;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BlockItem::addArrow(ArrowItem* a) {
|
||||
m_arrows.insert(a);
|
||||
}
|
||||
|
||||
void BlockItem::removeArrow(ArrowItem* a) {
|
||||
m_arrows.remove(a);
|
||||
}
|
||||
|
||||
QVariant BlockItem::itemChange(GraphicsItemChange change, const QVariant& value) {
|
||||
if (change == ItemPositionHasChanged || change == ItemTransformHasChanged) {
|
||||
// уведомим стрелки, что нужно пересчитать геометрию
|
||||
for (auto& a : m_arrows) {
|
||||
if (a) a->updatePath();
|
||||
}
|
||||
emit geometryChanged();
|
||||
}
|
||||
return QGraphicsObject::itemChange(change, value);
|
||||
}
|
||||
|
||||
void BlockItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
|
||||
bool ok = false;
|
||||
const QString t = QInputDialog::getText(nullptr, "Rename function", "Title:", QLineEdit::Normal, m_title, &ok);
|
||||
if (ok) setTitle(t);
|
||||
QGraphicsObject::mouseDoubleClickEvent(e);
|
||||
}
|
||||
45
src/items/BlockItem.h
Normal file
45
src/items/BlockItem.h
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
#include <QGraphicsObject>
|
||||
#include <QSet>
|
||||
|
||||
class ArrowItem;
|
||||
|
||||
class BlockItem final : public QGraphicsObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class Port { Input, Control, Output, Mechanism };
|
||||
enum { Type = UserType + 1 };
|
||||
|
||||
explicit BlockItem(QString title = "Function", QGraphicsItem* parent = nullptr, int id = -1);
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) override;
|
||||
int type() const override { return Type; }
|
||||
|
||||
QString title() const { return m_title; }
|
||||
void setTitle(const QString& t);
|
||||
int id() const { return m_id; }
|
||||
void setId(int id) { m_id = id; }
|
||||
|
||||
QPointF portScenePos(Port p) const;
|
||||
bool hitTestPort(const QPointF& scenePos, Port* outPort, QPointF* outLocalPos = nullptr, qreal radiusPx = 10.0) const;
|
||||
|
||||
void addArrow(ArrowItem* a);
|
||||
void removeArrow(ArrowItem* a);
|
||||
|
||||
signals:
|
||||
void geometryChanged();
|
||||
|
||||
protected:
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant& value) override;
|
||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
|
||||
private:
|
||||
QString m_title;
|
||||
QRectF m_rect; // local rect
|
||||
int m_id;
|
||||
QSet<ArrowItem*> m_arrows;
|
||||
|
||||
QPointF portLocalPos(Port p) const;
|
||||
QPair<QPointF, QPointF> portSegment(Port p) const;
|
||||
};
|
||||
604
src/items/DiagramScene.cpp
Normal file
604
src/items/DiagramScene.cpp
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
#include "DiagramScene.h"
|
||||
#include "items/BlockItem.h"
|
||||
#include "items/ArrowItem.h"
|
||||
#include "items/JunctionItem.h"
|
||||
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QTimer>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
DiagramScene::DiagramScene(QObject* parent)
|
||||
: QGraphicsScene(parent)
|
||||
{
|
||||
// Ограничиваем рабочую область A4 (210x297 мм) в пикселях при 96 dpi (~793x1122).
|
||||
// По умолчанию используем альбомную ориентацию (ширина больше высоты).
|
||||
const qreal w = 1122;
|
||||
const qreal h = 793;
|
||||
setSceneRect(-w * 0.5, -h * 0.5, w, h);
|
||||
auto* frame = addRect(sceneRect(), QPen(QColor(80, 80, 80), 1.5, Qt::DashLine), Qt::NoBrush);
|
||||
frame->setZValue(-5);
|
||||
frame->setEnabled(false);
|
||||
frame->setAcceptedMouseButtons(Qt::NoButton);
|
||||
frame->setData(0, QVariant(QStringLiteral("static-frame")));
|
||||
pushSnapshot();
|
||||
}
|
||||
|
||||
BlockItem* DiagramScene::createBlockAt(const QPointF& scenePos) {
|
||||
auto* b = new BlockItem("Function", nullptr, m_nextBlockId++);
|
||||
addItem(b);
|
||||
b->setPos(scenePos - QPointF(100, 50)); // центрируем
|
||||
pushSnapshot();
|
||||
return b;
|
||||
}
|
||||
|
||||
BlockItem* DiagramScene::createBlockWithId(const QPointF& scenePos, int id, const QString& title) {
|
||||
m_nextBlockId = std::max(m_nextBlockId, id + 1);
|
||||
auto* b = new BlockItem(title, nullptr, id);
|
||||
addItem(b);
|
||||
b->setPos(scenePos);
|
||||
return b;
|
||||
}
|
||||
|
||||
DiagramScene::Edge DiagramScene::hitTestEdge(const QPointF& scenePos, QPointF* outScenePoint) const {
|
||||
const QRectF r = sceneRect();
|
||||
const qreal tol = 12.0;
|
||||
Edge edge = Edge::None;
|
||||
QPointF proj = scenePos;
|
||||
|
||||
if (scenePos.y() >= r.top() && scenePos.y() <= r.bottom()) {
|
||||
if (std::abs(scenePos.x() - r.left()) <= tol) {
|
||||
edge = Edge::Left;
|
||||
proj = QPointF(r.left(), std::clamp(scenePos.y(), r.top(), r.bottom()));
|
||||
} else if (std::abs(scenePos.x() - r.right()) <= tol) {
|
||||
edge = Edge::Right;
|
||||
proj = QPointF(r.right(), std::clamp(scenePos.y(), r.top(), r.bottom()));
|
||||
}
|
||||
}
|
||||
if (scenePos.x() >= r.left() && scenePos.x() <= r.right()) {
|
||||
if (std::abs(scenePos.y() - r.top()) <= tol) {
|
||||
edge = Edge::Top;
|
||||
proj = QPointF(std::clamp(scenePos.x(), r.left(), r.right()), r.top());
|
||||
} else if (std::abs(scenePos.y() - r.bottom()) <= tol) {
|
||||
edge = Edge::Bottom;
|
||||
proj = QPointF(std::clamp(scenePos.x(), r.left(), r.right()), r.bottom());
|
||||
}
|
||||
}
|
||||
|
||||
if (outScenePoint) *outScenePoint = proj;
|
||||
return edge;
|
||||
}
|
||||
|
||||
bool DiagramScene::tryStartArrowDrag(const QPointF& scenePos) {
|
||||
const auto itemsUnder = items(scenePos);
|
||||
// проверяем, не стартуем ли с существующего junction
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
if (auto* j = qgraphicsitem_cast<JunctionItem*>(it)) {
|
||||
if (!j->hitTest(scenePos, 8.0)) continue;
|
||||
auto* a = new ArrowItem();
|
||||
addItem(a);
|
||||
ArrowItem::Endpoint from;
|
||||
from.junction = j;
|
||||
a->setFrom(from);
|
||||
a->setTempEndPoint(scenePos);
|
||||
m_dragArrow = a;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction = j;
|
||||
m_dragFromPort = BlockItem::Port::Output;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ищем блок под курсором
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
auto* b = qgraphicsitem_cast<BlockItem*>(it);
|
||||
if (!b) continue;
|
||||
|
||||
BlockItem::Port port{};
|
||||
QPointF localPos;
|
||||
if (!b->hitTestPort(scenePos, &port, &localPos)) continue;
|
||||
if (port != BlockItem::Port::Output) continue; // стартуем только с выходной стороны
|
||||
|
||||
// Стартуем стрелку из порта
|
||||
m_dragFromBlock = b;
|
||||
m_dragFromJunction.clear();
|
||||
m_dragFromPort = port;
|
||||
|
||||
auto* a = new ArrowItem();
|
||||
addItem(a);
|
||||
|
||||
ArrowItem::Endpoint from;
|
||||
from.block = b;
|
||||
from.port = port;
|
||||
from.localPos = localPos;
|
||||
a->setFrom(from);
|
||||
a->setTempEndPoint(scenePos);
|
||||
m_dragArrow = a;
|
||||
return true;
|
||||
}
|
||||
|
||||
QPointF edgePoint;
|
||||
Edge edge = hitTestEdge(scenePos, &edgePoint);
|
||||
if (edge != Edge::None && edge != Edge::Right) { // разрешаем старт с левой/верхней/нижней границы
|
||||
auto* a = new ArrowItem();
|
||||
addItem(a);
|
||||
ArrowItem::Endpoint from;
|
||||
from.scenePos = edgePoint;
|
||||
from.port = BlockItem::Port::Input; // ориентация не важна для свободной точки
|
||||
a->setFrom(from);
|
||||
a->setTempEndPoint(scenePos);
|
||||
m_dragArrow = a;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction.clear();
|
||||
m_dragFromPort = BlockItem::Port::Output;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiagramScene::tryFinishArrowDrag(const QPointF& scenePos) {
|
||||
if (!m_dragArrow) return false;
|
||||
|
||||
const auto itemsUnder = items(scenePos);
|
||||
// сначала пробуем попасть в блок
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
auto* b = qgraphicsitem_cast<BlockItem*>(it);
|
||||
if (!b) continue;
|
||||
|
||||
BlockItem::Port port{};
|
||||
QPointF localPos;
|
||||
if (!b->hitTestPort(scenePos, &port, &localPos)) continue;
|
||||
|
||||
// запретим соединять в тот же самый порт того же блока (упрощение)
|
||||
if (b == m_dragFromBlock && port == m_dragFromPort) continue;
|
||||
|
||||
ArrowItem::Endpoint to;
|
||||
to.block = b;
|
||||
to.port = port;
|
||||
to.localPos = localPos;
|
||||
m_dragArrow->setTo(to);
|
||||
m_dragArrow->finalize();
|
||||
|
||||
m_dragArrow = nullptr;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction.clear();
|
||||
pushSnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
// затем — уже существующий junction
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
auto* j = qgraphicsitem_cast<JunctionItem*>(it);
|
||||
if (!j) continue;
|
||||
if (!j->hitTest(scenePos, 8.0)) continue;
|
||||
if (j == m_dragFromJunction) continue;
|
||||
|
||||
ArrowItem::Endpoint to;
|
||||
to.junction = j;
|
||||
to.port = BlockItem::Port::Input;
|
||||
m_dragArrow->setTo(to);
|
||||
m_dragArrow->finalize();
|
||||
|
||||
m_dragArrow = nullptr;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction.clear();
|
||||
pushSnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Попали в существующую стрелку — создаем junction и расщепляем.
|
||||
ArrowItem* targetArrow = nullptr;
|
||||
QPointF splitPoint;
|
||||
qreal bestDist = 8.0;
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
auto* arrow = qgraphicsitem_cast<ArrowItem*>(it);
|
||||
if (!arrow || arrow == m_dragArrow) continue;
|
||||
auto proj = arrow->hitTest(scenePos, bestDist);
|
||||
if (proj) {
|
||||
bestDist = std::hypot(proj->x() - scenePos.x(), proj->y() - scenePos.y());
|
||||
splitPoint = *proj;
|
||||
targetArrow = arrow;
|
||||
}
|
||||
}
|
||||
if (targetArrow) {
|
||||
auto* junction = new JunctionItem(nullptr, m_nextJunctionId++);
|
||||
addItem(junction);
|
||||
junction->setPos(splitPoint);
|
||||
|
||||
ArrowItem::Endpoint mid;
|
||||
mid.junction = junction;
|
||||
mid.port = BlockItem::Port::Input;
|
||||
|
||||
const ArrowItem::Endpoint origTo = targetArrow->to();
|
||||
targetArrow->setTo(mid);
|
||||
targetArrow->finalize();
|
||||
|
||||
auto* forward = new ArrowItem();
|
||||
addItem(forward);
|
||||
forward->setFrom(mid);
|
||||
forward->setTo(origTo);
|
||||
forward->finalize();
|
||||
|
||||
m_dragArrow->setTo(mid);
|
||||
m_dragArrow->finalize();
|
||||
|
||||
m_dragArrow = nullptr;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction.clear();
|
||||
pushSnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
// если не попали в блок — попробуем закрепиться на границе сцены
|
||||
QPointF edgePoint;
|
||||
Edge edge = hitTestEdge(scenePos, &edgePoint);
|
||||
if (edge != Edge::None) {
|
||||
// правая граница может быть только приемником (уже гарантировано)
|
||||
ArrowItem::Endpoint to;
|
||||
to.scenePos = edgePoint;
|
||||
to.port = BlockItem::Port::Input;
|
||||
m_dragArrow->setTo(to);
|
||||
m_dragArrow->finalize();
|
||||
m_dragArrow = nullptr;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction.clear();
|
||||
pushSnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiagramScene::tryBranchAtArrow(const QPointF& scenePos) {
|
||||
ArrowItem* targetArrow = nullptr;
|
||||
QPointF splitPoint;
|
||||
qreal bestDist = 8.0;
|
||||
|
||||
const auto itemsUnder = items(scenePos);
|
||||
for (QGraphicsItem* it : itemsUnder) {
|
||||
auto* arrow = qgraphicsitem_cast<ArrowItem*>(it);
|
||||
if (!arrow || arrow == m_dragArrow) continue;
|
||||
auto proj = arrow->hitTest(scenePos, bestDist);
|
||||
if (proj) {
|
||||
bestDist = std::hypot(proj->x() - scenePos.x(), proj->y() - scenePos.y());
|
||||
splitPoint = *proj;
|
||||
targetArrow = arrow;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetArrow) return false;
|
||||
|
||||
auto* junction = new JunctionItem(nullptr, m_nextJunctionId++);
|
||||
addItem(junction);
|
||||
junction->setPos(splitPoint);
|
||||
|
||||
ArrowItem::Endpoint mid;
|
||||
mid.junction = junction;
|
||||
mid.port = BlockItem::Port::Input;
|
||||
|
||||
const ArrowItem::Endpoint origTo = targetArrow->to();
|
||||
targetArrow->setTo(mid);
|
||||
targetArrow->finalize();
|
||||
|
||||
auto* forward = new ArrowItem();
|
||||
addItem(forward);
|
||||
forward->setFrom(mid);
|
||||
forward->setTo(origTo);
|
||||
forward->finalize();
|
||||
|
||||
auto* branch = new ArrowItem();
|
||||
addItem(branch);
|
||||
branch->setFrom(mid);
|
||||
branch->setTempEndPoint(scenePos);
|
||||
m_dragArrow = branch;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction = junction;
|
||||
m_dragFromPort = BlockItem::Port::Output;
|
||||
return true;
|
||||
}
|
||||
|
||||
DiagramScene::Snapshot DiagramScene::captureSnapshot() const {
|
||||
Snapshot s;
|
||||
|
||||
QSet<BlockItem*> blockSet;
|
||||
QSet<JunctionItem*> junctionSet;
|
||||
for (QGraphicsItem* it : items()) {
|
||||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) blockSet.insert(b);
|
||||
if (auto* j = qgraphicsitem_cast<JunctionItem*>(it)) junctionSet.insert(j);
|
||||
}
|
||||
|
||||
auto encodeEp = [&](const ArrowItem::Endpoint& ep) {
|
||||
Snapshot::Endpoint out;
|
||||
if (ep.block && blockSet.contains(ep.block) && ep.block->scene() == this) {
|
||||
out.kind = Snapshot::Endpoint::Kind::Block;
|
||||
out.id = ep.block->id();
|
||||
out.port = ep.port;
|
||||
if (ep.localPos) out.localPos = *ep.localPos;
|
||||
} else if (ep.junction && junctionSet.contains(ep.junction) && ep.junction->scene() == this) {
|
||||
out.kind = Snapshot::Endpoint::Kind::Junction;
|
||||
out.id = ep.junction->id();
|
||||
} else if (ep.scenePos) {
|
||||
out.kind = Snapshot::Endpoint::Kind::Scene;
|
||||
out.scenePos = *ep.scenePos;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
for (QGraphicsItem* it : items()) {
|
||||
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
|
||||
s.blocks.push_back({b->id(), b->title(), b->pos()});
|
||||
}
|
||||
}
|
||||
|
||||
for (QGraphicsItem* it : items()) {
|
||||
if (auto* j = qgraphicsitem_cast<JunctionItem*>(it)) {
|
||||
s.junctions.push_back({j->id(), j->pos()});
|
||||
}
|
||||
}
|
||||
|
||||
for (QGraphicsItem* it : items()) {
|
||||
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
|
||||
Snapshot::Arrow ar;
|
||||
ar.from = encodeEp(a->from());
|
||||
ar.to = encodeEp(a->to());
|
||||
if (ar.from.kind == Snapshot::Endpoint::Kind::None || ar.to.kind == Snapshot::Endpoint::Kind::None) {
|
||||
continue; // skip incomplete arrows
|
||||
}
|
||||
ar.bend = a->bendOffset();
|
||||
ar.top = a->topOffset();
|
||||
ar.bottom = a->bottomOffset();
|
||||
ar.label = a->label();
|
||||
ar.labelOffset = a->labelOffset();
|
||||
s.arrows.push_back(std::move(ar));
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void DiagramScene::restoreSnapshot(const Snapshot& snap) {
|
||||
m_restoringSnapshot = true;
|
||||
cancelCurrentDrag();
|
||||
// Clear dynamic items but keep the static frame.
|
||||
QList<QGraphicsItem*> toRemove;
|
||||
for (QGraphicsItem* it : items()) {
|
||||
if (it->data(0).toString() == QStringLiteral("static-frame")) continue;
|
||||
toRemove.append(it);
|
||||
}
|
||||
for (QGraphicsItem* it : toRemove) {
|
||||
removeItem(it);
|
||||
delete it;
|
||||
}
|
||||
|
||||
m_nextBlockId = 1;
|
||||
m_nextJunctionId = 1;
|
||||
|
||||
QHash<int, BlockItem*> blockMap;
|
||||
QHash<int, JunctionItem*> junctionMap;
|
||||
|
||||
for (const auto& b : snap.blocks) {
|
||||
auto* blk = createBlockWithId(b.pos, b.id, b.title);
|
||||
blockMap.insert(b.id, blk);
|
||||
m_nextBlockId = std::max(m_nextBlockId, b.id + 1);
|
||||
}
|
||||
for (const auto& j : snap.junctions) {
|
||||
m_nextJunctionId = std::max(m_nextJunctionId, j.id + 1);
|
||||
auto* jun = new JunctionItem(nullptr, j.id);
|
||||
addItem(jun);
|
||||
jun->setPos(j.pos);
|
||||
junctionMap.insert(j.id, jun);
|
||||
}
|
||||
|
||||
auto decodeEp = [&](const Snapshot::Endpoint& ep) {
|
||||
ArrowItem::Endpoint out;
|
||||
switch (ep.kind) {
|
||||
case Snapshot::Endpoint::Kind::Block:
|
||||
out.block = blockMap.value(ep.id, nullptr);
|
||||
out.port = ep.port;
|
||||
out.localPos = ep.localPos;
|
||||
break;
|
||||
case Snapshot::Endpoint::Kind::Junction:
|
||||
out.junction = junctionMap.value(ep.id, nullptr);
|
||||
break;
|
||||
case Snapshot::Endpoint::Kind::Scene:
|
||||
out.scenePos = ep.scenePos;
|
||||
out.port = BlockItem::Port::Input;
|
||||
break;
|
||||
case Snapshot::Endpoint::Kind::None:
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
for (const auto& a : snap.arrows) {
|
||||
auto* ar = new ArrowItem();
|
||||
addItem(ar);
|
||||
ar->setFrom(decodeEp(a.from));
|
||||
ar->setTo(decodeEp(a.to));
|
||||
ar->setOffsets(a.bend, a.top, a.bottom);
|
||||
ar->setLabel(a.label);
|
||||
ar->setLabelOffset(a.labelOffset);
|
||||
ar->finalize();
|
||||
}
|
||||
|
||||
m_restoringSnapshot = false;
|
||||
}
|
||||
|
||||
void DiagramScene::pushSnapshot() {
|
||||
if (m_restoringSnapshot) return;
|
||||
Snapshot s = captureSnapshot();
|
||||
if (m_historyIndex + 1 < m_history.size()) {
|
||||
m_history.resize(m_historyIndex + 1);
|
||||
}
|
||||
m_history.push_back(std::move(s));
|
||||
m_historyIndex = m_history.size() - 1;
|
||||
}
|
||||
|
||||
void DiagramScene::scheduleSnapshot() {
|
||||
if (m_restoringSnapshot || m_snapshotScheduled) return;
|
||||
m_snapshotScheduled = true;
|
||||
QTimer::singleShot(0, this, [this]{
|
||||
m_snapshotScheduled = false;
|
||||
pushSnapshot();
|
||||
});
|
||||
}
|
||||
|
||||
void DiagramScene::undo() {
|
||||
if (m_historyIndex <= 0) return;
|
||||
m_historyIndex -= 1;
|
||||
restoreSnapshot(m_history[m_historyIndex]);
|
||||
}
|
||||
|
||||
void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
if (e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
if (tryBranchAtArrow(e->scenePos())) {
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// если кликнули на порт — начинаем протягивание стрелки
|
||||
if (tryStartArrowDrag(e->scenePos())) {
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
// начало перемещения существующих элементов — запомним позиции
|
||||
m_pressPositions.clear();
|
||||
const auto sel = selectedItems();
|
||||
for (QGraphicsItem* it : sel) {
|
||||
if (it->flags() & QGraphicsItem::ItemIsMovable) {
|
||||
m_pressPositions.insert(it, it->pos());
|
||||
}
|
||||
}
|
||||
m_itemDragActive = !m_pressPositions.isEmpty();
|
||||
}
|
||||
|
||||
if (e->button() == Qt::RightButton) {
|
||||
// правой кнопкой — добавить блок
|
||||
createBlockAt(e->scenePos());
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
QGraphicsScene::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (m_dragArrow) {
|
||||
m_dragArrow->setTempEndPoint(e->scenePos());
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsScene::mouseMoveEvent(e);
|
||||
}
|
||||
|
||||
void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (m_dragArrow && e->button() == Qt::LeftButton) {
|
||||
if (!tryFinishArrowDrag(e->scenePos())) {
|
||||
// не получилось — удаляем временную стрелку
|
||||
cancelCurrentDrag();
|
||||
}
|
||||
e->accept();
|
||||
}
|
||||
if (m_itemDragActive && e->button() == Qt::LeftButton) {
|
||||
maybeSnapshotMovedItems();
|
||||
m_itemDragActive = false;
|
||||
}
|
||||
QGraphicsScene::mouseReleaseEvent(e);
|
||||
}
|
||||
|
||||
void DiagramScene::keyPressEvent(QKeyEvent* e) {
|
||||
if (e->key() == Qt::Key_Escape) {
|
||||
cancelCurrentDrag();
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) {
|
||||
deleteSelection();
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (e->matches(QKeySequence::Undo)) {
|
||||
undo();
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsScene::keyPressEvent(e);
|
||||
}
|
||||
|
||||
void DiagramScene::cancelCurrentDrag() {
|
||||
if (m_dragArrow) {
|
||||
removeItem(m_dragArrow);
|
||||
delete m_dragArrow;
|
||||
m_dragArrow = nullptr;
|
||||
m_dragFromBlock.clear();
|
||||
m_dragFromJunction.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void DiagramScene::deleteSelection() {
|
||||
if (m_dragArrow) {
|
||||
cancelCurrentDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
QSet<QGraphicsItem*> toDelete;
|
||||
const auto sel = selectedItems();
|
||||
for (QGraphicsItem* it : sel) {
|
||||
if (qgraphicsitem_cast<BlockItem*>(it) || qgraphicsitem_cast<JunctionItem*>(it) || qgraphicsitem_cast<ArrowItem*>(it)) {
|
||||
toDelete.insert(it);
|
||||
}
|
||||
}
|
||||
|
||||
// also delete arrows connected to selected blocks/junctions
|
||||
for (QGraphicsItem* it : items()) {
|
||||
auto* arrow = qgraphicsitem_cast<ArrowItem*>(it);
|
||||
if (!arrow) continue;
|
||||
const auto f = arrow->from();
|
||||
const auto t = arrow->to();
|
||||
if ((f.block && toDelete.contains(f.block)) ||
|
||||
(t.block && toDelete.contains(t.block)) ||
|
||||
(f.junction && toDelete.contains(f.junction)) ||
|
||||
(t.junction && toDelete.contains(t.junction))) {
|
||||
toDelete.insert(arrow);
|
||||
}
|
||||
}
|
||||
|
||||
// delete arrows first to avoid dangling removal from deleted blocks/junctions
|
||||
for (QGraphicsItem* it : toDelete) {
|
||||
if (qgraphicsitem_cast<ArrowItem*>(it)) {
|
||||
removeItem(it);
|
||||
delete it;
|
||||
}
|
||||
}
|
||||
for (QGraphicsItem* it : toDelete) {
|
||||
if (!qgraphicsitem_cast<ArrowItem*>(it)) {
|
||||
removeItem(it);
|
||||
delete it;
|
||||
}
|
||||
}
|
||||
|
||||
if (!toDelete.isEmpty()) pushSnapshot();
|
||||
}
|
||||
|
||||
void DiagramScene::requestSnapshot() {
|
||||
pushSnapshot();
|
||||
}
|
||||
|
||||
void DiagramScene::maybeSnapshotMovedItems() {
|
||||
bool moved = false;
|
||||
for (auto it = m_pressPositions.cbegin(); it != m_pressPositions.cend(); ++it) {
|
||||
if (!it.key()) continue;
|
||||
const QPointF cur = it.key()->pos();
|
||||
const QPointF old = it.value();
|
||||
if (!qFuzzyCompare(cur.x(), old.x()) || !qFuzzyCompare(cur.y(), old.y())) {
|
||||
moved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_pressPositions.clear();
|
||||
if (moved) pushSnapshot();
|
||||
}
|
||||
78
src/items/DiagramScene.h
Normal file
78
src/items/DiagramScene.h
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
#include <QGraphicsScene>
|
||||
#include <QPointer>
|
||||
|
||||
#include "BlockItem.h"
|
||||
class ArrowItem;
|
||||
class JunctionItem;
|
||||
|
||||
class DiagramScene final : public QGraphicsScene {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DiagramScene(QObject* parent = nullptr);
|
||||
|
||||
BlockItem* createBlockAt(const QPointF& scenePos);
|
||||
BlockItem* createBlockWithId(const QPointF& scenePos, int id, const QString& title);
|
||||
void requestSnapshot();
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent* e) override;
|
||||
void keyPressEvent(QKeyEvent* e) override;
|
||||
|
||||
private:
|
||||
ArrowItem* m_dragArrow = nullptr;
|
||||
QPointer<BlockItem> m_dragFromBlock;
|
||||
QPointer<JunctionItem> m_dragFromJunction;
|
||||
BlockItem::Port m_dragFromPort = BlockItem::Port::Output;
|
||||
int m_nextBlockId = 1;
|
||||
int m_nextJunctionId = 1;
|
||||
bool m_snapshotScheduled = false;
|
||||
bool m_restoringSnapshot = false;
|
||||
bool m_itemDragActive = false;
|
||||
QHash<QGraphicsItem*, QPointF> m_pressPositions;
|
||||
|
||||
bool tryStartArrowDrag(const QPointF& scenePos);
|
||||
bool tryFinishArrowDrag(const QPointF& scenePos);
|
||||
bool tryBranchAtArrow(const QPointF& scenePos);
|
||||
void cancelCurrentDrag();
|
||||
void deleteSelection();
|
||||
|
||||
struct Snapshot {
|
||||
struct Endpoint {
|
||||
enum class Kind { None, Block, Junction, Scene } kind = Kind::None;
|
||||
int id = -1;
|
||||
BlockItem::Port port = BlockItem::Port::Input;
|
||||
QPointF localPos;
|
||||
QPointF scenePos;
|
||||
};
|
||||
struct Block { int id; QString title; QPointF pos; };
|
||||
struct Junction { int id; QPointF pos; };
|
||||
struct Arrow {
|
||||
Endpoint from;
|
||||
Endpoint to;
|
||||
qreal bend = 0.0;
|
||||
qreal top = 0.0;
|
||||
qreal bottom = 0.0;
|
||||
QString label;
|
||||
QPointF labelOffset;
|
||||
};
|
||||
QVector<Block> blocks;
|
||||
QVector<Junction> junctions;
|
||||
QVector<Arrow> arrows;
|
||||
};
|
||||
|
||||
QVector<Snapshot> m_history;
|
||||
int m_historyIndex = -1;
|
||||
|
||||
Snapshot captureSnapshot() const;
|
||||
void restoreSnapshot(const Snapshot& snap);
|
||||
void pushSnapshot();
|
||||
void scheduleSnapshot();
|
||||
void undo();
|
||||
void maybeSnapshotMovedItems();
|
||||
|
||||
enum class Edge { None, Left, Right, Top, Bottom };
|
||||
Edge hitTestEdge(const QPointF& scenePos, QPointF* outScenePoint = nullptr) const;
|
||||
};
|
||||
47
src/items/JunctionItem.cpp
Normal file
47
src/items/JunctionItem.cpp
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#include "JunctionItem.h"
|
||||
#include "ArrowItem.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <cmath>
|
||||
|
||||
JunctionItem::JunctionItem(QGraphicsItem* parent, int id)
|
||||
: QGraphicsObject(parent),
|
||||
m_id(id)
|
||||
{
|
||||
setFlags(ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges);
|
||||
setZValue(1); // slightly above arrows
|
||||
}
|
||||
|
||||
QRectF JunctionItem::boundingRect() const {
|
||||
return QRectF(-m_radius - 2, -m_radius - 2, (m_radius + 2) * 2, (m_radius + 2) * 2);
|
||||
}
|
||||
|
||||
void JunctionItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) {
|
||||
p->setRenderHint(QPainter::Antialiasing, true);
|
||||
p->setPen(Qt::NoPen);
|
||||
p->setBrush(isSelected() ? QColor(40, 100, 255) : QColor(30, 30, 30));
|
||||
p->drawEllipse(QPointF(0, 0), m_radius, m_radius);
|
||||
}
|
||||
|
||||
void JunctionItem::addArrow(ArrowItem* a) {
|
||||
m_arrows.insert(a);
|
||||
}
|
||||
|
||||
void JunctionItem::removeArrow(ArrowItem* a) {
|
||||
m_arrows.remove(a);
|
||||
}
|
||||
|
||||
bool JunctionItem::hitTest(const QPointF& scenePos, qreal radius) const {
|
||||
const QPointF local = mapFromScene(scenePos);
|
||||
return std::hypot(local.x(), local.y()) <= radius;
|
||||
}
|
||||
|
||||
QVariant JunctionItem::itemChange(GraphicsItemChange change, const QVariant& value) {
|
||||
if (change == ItemPositionHasChanged || change == ItemTransformHasChanged) {
|
||||
for (ArrowItem* a : m_arrows) {
|
||||
if (a) a->updatePath();
|
||||
}
|
||||
}
|
||||
return QGraphicsObject::itemChange(change, value);
|
||||
}
|
||||
32
src/items/JunctionItem.h
Normal file
32
src/items/JunctionItem.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
#include <QGraphicsObject>
|
||||
#include <QSet>
|
||||
|
||||
class ArrowItem;
|
||||
|
||||
// Small movable node that lets several arrows meet at a single point.
|
||||
class JunctionItem final : public QGraphicsObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum { Type = UserType + 2 };
|
||||
explicit JunctionItem(QGraphicsItem* parent = nullptr, int id = -1);
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter* p, const QStyleOptionGraphicsItem* opt, QWidget* widget) override;
|
||||
int type() const override { return Type; }
|
||||
|
||||
void addArrow(ArrowItem* a);
|
||||
void removeArrow(ArrowItem* a);
|
||||
|
||||
bool hitTest(const QPointF& scenePos, qreal radius) const;
|
||||
int id() const { return m_id; }
|
||||
void setId(int id) { m_id = id; }
|
||||
|
||||
protected:
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant& value) override;
|
||||
|
||||
private:
|
||||
qreal m_radius = 5.0;
|
||||
int m_id;
|
||||
QSet<ArrowItem*> m_arrows;
|
||||
};
|
||||
10
src/main.cpp
Normal file
10
src/main.cpp
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#include <QApplication>
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
QApplication app(argc, argv);
|
||||
MainWindow w;
|
||||
w.resize(1200, 800);
|
||||
w.show();
|
||||
return app.exec();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue