Ilya Ashikhmin b257c1a7cf feat(scene): add interactive graphics scene with grid, ports and styled edges
- SVG node items with snap-to-grid dragging; ports appear on focus/hover
  and highlight compatible targets during a connection drag
- Orthogonally routed edge items with line styles, head/tail decorations
  (arrow, circle, diamond, bar), selection halo and title labels; later
  edges jump crossings with arcs when enabled
- Grid background view with cursor-anchored zoom, middle-button pan,
  rubber-band selection and palette drag-and-drop
- Undoable commands: add/move/delete/connect/property/paste with
  clipboard fragments; light/dark/high-contrast themes
- Flow overlay: animated dashes, direction and density follow flow rate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:25:06 +02:00

138 lines
5.0 KiB
C++

#include "TestFixtures.h"
#include "core/NetworkModel.h"
#include "scene/Commands.h"
#include <QTest>
#include <QUndoStack>
using namespace diag;
class TestUndo : public QObject {
Q_OBJECT
private slots:
void init()
{
fixtures::fillRegistry(m_reg);
delete m_model;
m_model = new NetworkModel(&m_reg, this);
m_undo.clear();
}
void addNode_undoRedo()
{
auto* cmd = new AddNodeCommand(m_model, QStringLiteral("pump"), {100, 100});
m_undo.push(cmd);
const QUuid id = cmd->nodeId();
QVERIFY(m_model->node(id));
m_undo.undo();
QVERIFY(!m_model->node(id));
m_undo.redo();
QVERIFY(m_model->node(id)); // same id restored
}
void addEdge_undoRedo()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
auto* cmd = new AddEdgeCommand(m_model, src, QStringLiteral("out"), pump,
QStringLiteral("in"));
m_undo.push(cmd);
QVERIFY(m_model->edge(cmd->edgeId()));
m_undo.undo();
QCOMPARE(m_model->edgeIds().size(), 0);
m_undo.redo();
QCOMPARE(m_model->edgeIds().size(), 1);
}
void moveNodes_undoRestoresPositions()
{
const QUuid a = m_model->addNode(QStringLiteral("pump"), {0, 0});
const QUuid b = m_model->addNode(QStringLiteral("pump"), {100, 0});
m_undo.push(new MoveNodesCommand(
m_model, {{a, {0, 0}, {40, 40}}, {b, {100, 0}, {140, 40}}}));
QCOMPARE(m_model->node(a)->pos, QPointF(40, 40));
m_undo.undo();
QCOMPARE(m_model->node(a)->pos, QPointF(0, 0));
QCOMPARE(m_model->node(b)->pos, QPointF(100, 0));
}
void removeItems_undoRestoresEdgesAndProps()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {400, 0});
const QUuid e1 = m_model->addEdge(src, "out", pump, "in");
const QUuid e2 = m_model->addEdge(pump, "out", c, "in");
m_model->setEdgeProperty(e1, "diameter", 150.0);
m_model->setNodeProperty(pump, "title", QStringLiteral("P-1"));
// Deleting the pump takes both attached edges with it.
m_undo.push(new RemoveItemsCommand(m_model, {pump}, {}));
QCOMPARE(m_model->nodeIds().size(), 2);
QCOMPARE(m_model->edgeIds().size(), 0);
m_undo.undo();
QCOMPARE(m_model->nodeIds().size(), 3);
QCOMPARE(m_model->edgeIds().size(), 2);
QVERIFY(m_model->edge(e2));
QCOMPARE(m_model->edgeProperty(e1, "diameter").toDouble(), 150.0);
QCOMPARE(m_model->nodeProperty(pump, "title").toString(), QStringLiteral("P-1"));
}
void setProperty_undoRedo()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
m_undo.push(new SetPropertyCommand(m_model, SetPropertyCommand::NodeTarget, src,
QStringLiteral("supply"), 25.0));
QCOMPARE(m_model->nodeProperty(src, "supply").toDouble(), 25.0);
m_undo.undo();
QCOMPARE(m_model->nodeProperty(src, "supply").toDouble(), 10.0); // spec default
}
void paste_remapsIdsAndOffsets()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
m_model->addEdge(src, "out", pump, "in");
const QJsonObject frag = fragmentFromSelection(*m_model, {src, pump}, {});
auto* cmd = new PasteCommand(m_model, frag, {20, 20});
m_undo.push(cmd);
QCOMPARE(m_model->nodeIds().size(), 4);
QCOMPARE(m_model->edgeIds().size(), 2); // inner edge duplicated
const QList<QUuid> pasted = cmd->pastedNodes();
QCOMPARE(pasted.size(), 2);
QVERIFY(!pasted.contains(src));
QCOMPARE(m_model->node(pasted.first())->pos,
m_model->node(src)->pos + QPointF(20, 20));
m_undo.undo();
QCOMPARE(m_model->nodeIds().size(), 2);
QCOMPARE(m_model->edgeIds().size(), 1);
m_undo.redo();
QCOMPARE(m_model->nodeIds().size(), 4);
}
void copyFragment_skipsDanglingEdges()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
m_model->addEdge(src, "out", pump, "in");
// Only the pump is copied: the edge to the source must not paste.
const QJsonObject frag = fragmentFromSelection(*m_model, {pump}, {});
auto* cmd = new PasteCommand(m_model, frag, {0, 40});
m_undo.push(cmd);
QCOMPARE(m_model->nodeIds().size(), 3);
QCOMPARE(m_model->edgeIds().size(), 1);
}
private:
TypeRegistry m_reg;
NetworkModel* m_model = nullptr;
QUndoStack m_undo;
};
QTEST_GUILESS_MAIN(TestUndo)
#include "tst_undo.moc"