новый файл: 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:
Gregory Bednov 2026-02-23 00:21:10 +03:00
commit 53afa3f728
87 changed files with 48249 additions and 0 deletions

39
src/MainWindow.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
};

View 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
View 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
View 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();
}