#include "DiagramScene.h" #include "items/BlockItem.h" #include "items/ArrowItem.h" #include "items/JunctionItem.h" #include #include #include #include #include #include #include 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(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(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(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(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(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(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 blockSet; QSet junctionSet; for (QGraphicsItem* it : items()) { if (auto* b = qgraphicsitem_cast(it)) blockSet.insert(b); if (auto* j = qgraphicsitem_cast(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(it)) { s.blocks.push_back({b->id(), b->title(), b->pos()}); } } for (QGraphicsItem* it : items()) { if (auto* j = qgraphicsitem_cast(it)) { s.junctions.push_back({j->id(), j->pos()}); } } for (QGraphicsItem* it : items()) { if (auto* a = qgraphicsitem_cast(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 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 blockMap; QHash 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 toDelete; const auto sel = selectedItems(); for (QGraphicsItem* it : sel) { if (qgraphicsitem_cast(it) || qgraphicsitem_cast(it) || qgraphicsitem_cast(it)) { toDelete.insert(it); } } // also delete arrows connected to selected blocks/junctions for (QGraphicsItem* it : items()) { auto* arrow = qgraphicsitem_cast(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(it)) { removeItem(it); delete it; } } for (QGraphicsItem* it : toDelete) { if (!qgraphicsitem_cast(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(); }