feat(core): add network model with typed ports, relations and grouped properties
Data-driven node types (SVG + port config) loaded from JSON, relation-based connection validation, and a property system supporting string, int, float, enum, catalogue item(s), color and value-list types grouped by category. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
36dbbebe76
commit
26905a4ba0
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
build/
|
||||||
|
*.user
|
||||||
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
14
CMakeLists.txt
Normal file
14
CMakeLists.txt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
project(PipeDiagram VERSION 0.1 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
set(CMAKE_AUTORCC ON)
|
||||||
|
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Svg Test)
|
||||||
|
|
||||||
|
add_subdirectory(src)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# PipeDiagram — Pipeline Network Diagram Editor
|
||||||
|
|
||||||
|
Qt 6 editor for drawing pipeline networks (water, power, …): SVG nodes with typed
|
||||||
|
ports, grid-aligned orthogonal connectors with arc jump-overs, grouped editable
|
||||||
|
properties, styleable edges, and a mock flow simulation with animated flow.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build -G Ninja
|
||||||
|
cmake --build build
|
||||||
|
./build/src/pipediagram
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --test-dir build --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
See `docs/plan/plan.md` for architecture and the feature checklist.
|
||||||
11
src/CMakeLists.txt
Normal file
11
src/CMakeLists.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Core: document model, routing, solver — no Widgets dependency, headless-testable.
|
||||||
|
add_library(diagcore STATIC
|
||||||
|
core/Types.h
|
||||||
|
core/Types.cpp
|
||||||
|
core/TypeRegistry.h
|
||||||
|
core/TypeRegistry.cpp
|
||||||
|
core/NetworkModel.h
|
||||||
|
core/NetworkModel.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(diagcore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(diagcore PUBLIC Qt6::Core Qt6::Gui)
|
||||||
252
src/core/NetworkModel.cpp
Normal file
252
src/core/NetworkModel.cpp
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
#include "NetworkModel.h"
|
||||||
|
|
||||||
|
namespace diag {
|
||||||
|
|
||||||
|
NetworkModel::NetworkModel(TypeRegistry* registry, QObject* parent)
|
||||||
|
: QObject(parent), m_registry(registry ? registry : &TypeRegistry::instance())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid NetworkModel::addNode(const QString& typeId, QPointF pos, QUuid id)
|
||||||
|
{
|
||||||
|
const NodeType* type = m_registry->nodeType(typeId);
|
||||||
|
if (!type)
|
||||||
|
return {};
|
||||||
|
Node n;
|
||||||
|
n.id = id.isNull() ? QUuid::createUuid() : id;
|
||||||
|
n.typeId = typeId;
|
||||||
|
n.pos = pos;
|
||||||
|
m_nodes.insert(n.id, n);
|
||||||
|
m_nodeOrder.append(n.id);
|
||||||
|
emit nodeAdded(n.id);
|
||||||
|
return n.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModel::removeNode(QUuid id)
|
||||||
|
{
|
||||||
|
if (!m_nodes.contains(id))
|
||||||
|
return;
|
||||||
|
const QList<QUuid> attached = edgesOfNode(id);
|
||||||
|
for (QUuid e : attached)
|
||||||
|
removeEdge(e);
|
||||||
|
m_nodes.remove(id);
|
||||||
|
m_nodeOrder.removeOne(id);
|
||||||
|
emit nodeRemoved(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModel::moveNode(QUuid id, QPointF pos)
|
||||||
|
{
|
||||||
|
auto it = m_nodes.find(id);
|
||||||
|
if (it == m_nodes.end() || it->pos == pos)
|
||||||
|
return;
|
||||||
|
it->pos = pos;
|
||||||
|
emit nodeMoved(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node* NetworkModel::node(QUuid id) const
|
||||||
|
{
|
||||||
|
auto it = m_nodes.constFind(id);
|
||||||
|
return it == m_nodes.constEnd() ? nullptr : &it.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QUuid> NetworkModel::nodeIds() const
|
||||||
|
{
|
||||||
|
return m_nodeOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeType* NetworkModel::nodeTypeOf(QUuid id) const
|
||||||
|
{
|
||||||
|
const Node* n = node(id);
|
||||||
|
return n ? m_registry->nodeType(n->typeId) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkModel::canConnect(QUuid nodeA, const QString& portA, QUuid nodeB,
|
||||||
|
const QString& portB, QString* reason) const
|
||||||
|
{
|
||||||
|
auto fail = [reason](const QString& why) {
|
||||||
|
if (reason)
|
||||||
|
*reason = why;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeType* typeA = nodeTypeOf(nodeA);
|
||||||
|
const NodeType* typeB = nodeTypeOf(nodeB);
|
||||||
|
if (!typeA || !typeB)
|
||||||
|
return fail(tr("Unknown node"));
|
||||||
|
const PortSpec* pa = typeA->port(portA);
|
||||||
|
const PortSpec* pb = typeB->port(portB);
|
||||||
|
if (!pa || !pb)
|
||||||
|
return fail(tr("Unknown port"));
|
||||||
|
if (nodeA == nodeB && portA == portB)
|
||||||
|
return fail(tr("Cannot connect a port to itself"));
|
||||||
|
if (pa->relation != pb->relation)
|
||||||
|
return fail(tr("Incompatible port relations (%1 vs %2)").arg(pa->relation, pb->relation));
|
||||||
|
if (pa->direction == PortDirection::In && pb->direction == PortDirection::In)
|
||||||
|
return fail(tr("Cannot connect two inputs"));
|
||||||
|
if (pa->direction == PortDirection::Out && pb->direction == PortDirection::Out)
|
||||||
|
return fail(tr("Cannot connect two outputs"));
|
||||||
|
if (pa->maxConnections > 0 && connectionCount(nodeA, portA) >= pa->maxConnections)
|
||||||
|
return fail(tr("Port %1 is already occupied").arg(portA));
|
||||||
|
if (pb->maxConnections > 0 && connectionCount(nodeB, portB) >= pb->maxConnections)
|
||||||
|
return fail(tr("Port %1 is already occupied").arg(portB));
|
||||||
|
for (const auto& e : m_edges) {
|
||||||
|
const bool same = (e.fromNode == nodeA && e.fromPort == portA && e.toNode == nodeB
|
||||||
|
&& e.toPort == portB)
|
||||||
|
|| (e.fromNode == nodeB && e.fromPort == portB && e.toNode == nodeA
|
||||||
|
&& e.toPort == portA);
|
||||||
|
if (same)
|
||||||
|
return fail(tr("Ports are already connected"));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid NetworkModel::addEdge(QUuid fromNode, const QString& fromPort, QUuid toNode,
|
||||||
|
const QString& toPort, QUuid id)
|
||||||
|
{
|
||||||
|
if (!canConnect(fromNode, fromPort, toNode, toPort))
|
||||||
|
return {};
|
||||||
|
const NodeType* typeA = nodeTypeOf(fromNode);
|
||||||
|
const PortSpec* pa = typeA->port(fromPort);
|
||||||
|
const PortSpec* pb = nodeTypeOf(toNode)->port(toPort);
|
||||||
|
|
||||||
|
Edge e;
|
||||||
|
e.id = id.isNull() ? QUuid::createUuid() : id;
|
||||||
|
// Normalize direction so the edge always leaves an Out port when one is present.
|
||||||
|
if (pa->direction == PortDirection::In || pb->direction == PortDirection::Out) {
|
||||||
|
e.fromNode = toNode;
|
||||||
|
e.fromPort = toPort;
|
||||||
|
e.toNode = fromNode;
|
||||||
|
e.toPort = fromPort;
|
||||||
|
} else {
|
||||||
|
e.fromNode = fromNode;
|
||||||
|
e.fromPort = fromPort;
|
||||||
|
e.toNode = toNode;
|
||||||
|
e.toPort = toPort;
|
||||||
|
}
|
||||||
|
e.relation = pa->relation;
|
||||||
|
m_edges.insert(e.id, e);
|
||||||
|
m_edgeOrder.append(e.id);
|
||||||
|
emit edgeAdded(e.id);
|
||||||
|
return e.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModel::removeEdge(QUuid id)
|
||||||
|
{
|
||||||
|
if (!m_edges.remove(id))
|
||||||
|
return;
|
||||||
|
m_edgeOrder.removeOne(id);
|
||||||
|
emit edgeRemoved(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Edge* NetworkModel::edge(QUuid id) const
|
||||||
|
{
|
||||||
|
auto it = m_edges.constFind(id);
|
||||||
|
return it == m_edges.constEnd() ? nullptr : &it.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QUuid> NetworkModel::edgeIds() const
|
||||||
|
{
|
||||||
|
return m_edgeOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QUuid> NetworkModel::edgesOfNode(QUuid nodeId) const
|
||||||
|
{
|
||||||
|
QList<QUuid> out;
|
||||||
|
for (QUuid id : m_edgeOrder) {
|
||||||
|
const Edge& e = m_edges[id];
|
||||||
|
if (e.fromNode == nodeId || e.toNode == nodeId)
|
||||||
|
out.append(id);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NetworkModel::connectionCount(QUuid nodeId, const QString& portId) const
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& e : m_edges) {
|
||||||
|
if (e.fromNode == nodeId && e.fromPort == portId)
|
||||||
|
++count;
|
||||||
|
if (e.toNode == nodeId && e.toPort == portId)
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant NetworkModel::defaultFor(const QList<PropertySpec>& specs, const QString& key)
|
||||||
|
{
|
||||||
|
for (const auto& s : specs)
|
||||||
|
if (s.id == key)
|
||||||
|
return s.defaultValue;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModel::setNodeProperty(QUuid id, const QString& key, const QVariant& value)
|
||||||
|
{
|
||||||
|
auto it = m_nodes.find(id);
|
||||||
|
if (it == m_nodes.end() || it->props.value(key) == value)
|
||||||
|
return;
|
||||||
|
it->props.insert(key, value);
|
||||||
|
emit nodeChanged(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant NetworkModel::nodeProperty(QUuid id, const QString& key) const
|
||||||
|
{
|
||||||
|
const Node* n = node(id);
|
||||||
|
if (!n)
|
||||||
|
return {};
|
||||||
|
if (n->props.contains(key))
|
||||||
|
return n->props.value(key);
|
||||||
|
return defaultFor(m_registry->nodeProperties(n->typeId), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModel::setEdgeProperty(QUuid id, const QString& key, const QVariant& value)
|
||||||
|
{
|
||||||
|
auto it = m_edges.find(id);
|
||||||
|
if (it == m_edges.end() || it->props.value(key) == value)
|
||||||
|
return;
|
||||||
|
it->props.insert(key, value);
|
||||||
|
emit edgeChanged(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant NetworkModel::edgeProperty(QUuid id, const QString& key) const
|
||||||
|
{
|
||||||
|
const Edge* e = edge(id);
|
||||||
|
if (!e)
|
||||||
|
return {};
|
||||||
|
if (e->props.contains(key))
|
||||||
|
return e->props.value(key);
|
||||||
|
return defaultFor(m_registry->edgeProperties(e->relation), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModel::setEdgeFlow(QUuid id, double rate)
|
||||||
|
{
|
||||||
|
auto it = m_edges.find(id);
|
||||||
|
if (it == m_edges.end() || qFuzzyCompare(it->flowRate + 1.0, rate + 1.0))
|
||||||
|
return;
|
||||||
|
it->flowRate = rate;
|
||||||
|
emit flowChanged(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF NetworkModel::portScenePos(QUuid nodeId, const QString& portId) const
|
||||||
|
{
|
||||||
|
const Node* n = node(nodeId);
|
||||||
|
const NodeType* t = nodeTypeOf(nodeId);
|
||||||
|
if (!n || !t)
|
||||||
|
return {};
|
||||||
|
const PortSpec* p = t->port(portId);
|
||||||
|
if (!p)
|
||||||
|
return n->pos;
|
||||||
|
return n->pos
|
||||||
|
+ QPointF(p->pos.x() * t->size.width(), p->pos.y() * t->size.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModel::clear()
|
||||||
|
{
|
||||||
|
m_nodes.clear();
|
||||||
|
m_edges.clear();
|
||||||
|
m_nodeOrder.clear();
|
||||||
|
m_edgeOrder.clear();
|
||||||
|
emit modelReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace diag
|
||||||
75
src/core/NetworkModel.h
Normal file
75
src/core/NetworkModel.h
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "TypeRegistry.h"
|
||||||
|
#include "Types.h"
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
namespace diag {
|
||||||
|
|
||||||
|
// The document model: nodes placed on the canvas and edges connecting their
|
||||||
|
// ports. All edits are signalled so views stay in sync. Pure Qt Core/Gui —
|
||||||
|
// usable headless in tests.
|
||||||
|
class NetworkModel : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit NetworkModel(TypeRegistry* registry = nullptr, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
TypeRegistry* registry() const { return m_registry; }
|
||||||
|
|
||||||
|
// --- nodes ---
|
||||||
|
QUuid addNode(const QString& typeId, QPointF pos, QUuid id = {});
|
||||||
|
void removeNode(QUuid id); // cascades to attached edges
|
||||||
|
void moveNode(QUuid id, QPointF pos);
|
||||||
|
const Node* node(QUuid id) const;
|
||||||
|
QList<QUuid> nodeIds() const;
|
||||||
|
const NodeType* nodeTypeOf(QUuid id) const;
|
||||||
|
|
||||||
|
// --- edges ---
|
||||||
|
bool canConnect(QUuid nodeA, const QString& portA, QUuid nodeB, const QString& portB,
|
||||||
|
QString* reason = nullptr) const;
|
||||||
|
QUuid addEdge(QUuid fromNode, const QString& fromPort, QUuid toNode, const QString& toPort,
|
||||||
|
QUuid id = {});
|
||||||
|
void removeEdge(QUuid id);
|
||||||
|
const Edge* edge(QUuid id) const;
|
||||||
|
QList<QUuid> edgeIds() const;
|
||||||
|
QList<QUuid> edgesOfNode(QUuid nodeId) const;
|
||||||
|
int connectionCount(QUuid nodeId, const QString& portId) const;
|
||||||
|
|
||||||
|
// --- properties (values fall back to the spec default when unset) ---
|
||||||
|
void setNodeProperty(QUuid id, const QString& key, const QVariant& value);
|
||||||
|
QVariant nodeProperty(QUuid id, const QString& key) const;
|
||||||
|
void setEdgeProperty(QUuid id, const QString& key, const QVariant& value);
|
||||||
|
QVariant edgeProperty(QUuid id, const QString& key) const;
|
||||||
|
|
||||||
|
// Solver results (does not go through the undo stack).
|
||||||
|
void setEdgeFlow(QUuid id, double rate);
|
||||||
|
|
||||||
|
// Scene position of a port (node pos + relative port pos * size).
|
||||||
|
QPointF portScenePos(QUuid nodeId, const QString& portId) const;
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void nodeAdded(QUuid id);
|
||||||
|
void nodeRemoved(QUuid id);
|
||||||
|
void nodeMoved(QUuid id);
|
||||||
|
void nodeChanged(QUuid id);
|
||||||
|
void edgeAdded(QUuid id);
|
||||||
|
void edgeRemoved(QUuid id);
|
||||||
|
void edgeChanged(QUuid id);
|
||||||
|
void flowChanged(QUuid id);
|
||||||
|
void modelReset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QVariant defaultFor(const QList<PropertySpec>& specs, const QString& key);
|
||||||
|
|
||||||
|
TypeRegistry* m_registry;
|
||||||
|
QHash<QUuid, Node> m_nodes;
|
||||||
|
QHash<QUuid, Edge> m_edges;
|
||||||
|
QList<QUuid> m_nodeOrder; // stable iteration/serialization order
|
||||||
|
QList<QUuid> m_edgeOrder;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace diag
|
||||||
338
src/core/TypeRegistry.cpp
Normal file
338
src/core/TypeRegistry.cpp
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
#include "TypeRegistry.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
namespace diag {
|
||||||
|
|
||||||
|
TypeRegistry::TypeRegistry() = default;
|
||||||
|
|
||||||
|
TypeRegistry& TypeRegistry::instance()
|
||||||
|
{
|
||||||
|
static TypeRegistry reg;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TypeRegistry::clear()
|
||||||
|
{
|
||||||
|
m_relations.clear();
|
||||||
|
m_nodeTypes.clear();
|
||||||
|
m_catalogues.clear();
|
||||||
|
m_edgeProps.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TypeRegistry::addRelation(const Relation& r)
|
||||||
|
{
|
||||||
|
m_relations.append(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TypeRegistry::addNodeType(const NodeType& t)
|
||||||
|
{
|
||||||
|
m_nodeTypes.append(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TypeRegistry::addCatalogue(const Catalogue& c)
|
||||||
|
{
|
||||||
|
m_catalogues.append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TypeRegistry::setEdgeProperties(const QString& relationId, const QList<PropertySpec>& specs)
|
||||||
|
{
|
||||||
|
m_edgeProps.insert(relationId, specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Relation* TypeRegistry::relation(const QString& id) const
|
||||||
|
{
|
||||||
|
for (const auto& r : m_relations)
|
||||||
|
if (r.id == id)
|
||||||
|
return &r;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Relation> TypeRegistry::relations() const
|
||||||
|
{
|
||||||
|
return m_relations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeType* TypeRegistry::nodeType(const QString& id) const
|
||||||
|
{
|
||||||
|
for (const auto& t : m_nodeTypes)
|
||||||
|
if (t.id == id)
|
||||||
|
return &t;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<const NodeType*> TypeRegistry::nodeTypes() const
|
||||||
|
{
|
||||||
|
QList<const NodeType*> out;
|
||||||
|
out.reserve(m_nodeTypes.size());
|
||||||
|
for (const auto& t : m_nodeTypes)
|
||||||
|
out.append(&t);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList TypeRegistry::nodeGroups() const
|
||||||
|
{
|
||||||
|
QStringList groups;
|
||||||
|
for (const auto& t : m_nodeTypes)
|
||||||
|
if (!groups.contains(t.group))
|
||||||
|
groups.append(t.group);
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Catalogue* TypeRegistry::catalogue(const QString& id) const
|
||||||
|
{
|
||||||
|
for (const auto& c : m_catalogues)
|
||||||
|
if (c.id == id)
|
||||||
|
return &c;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<const Catalogue*> TypeRegistry::catalogues() const
|
||||||
|
{
|
||||||
|
QList<const Catalogue*> out;
|
||||||
|
out.reserve(m_catalogues.size());
|
||||||
|
for (const auto& c : m_catalogues)
|
||||||
|
out.append(&c);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<PropertySpec> TypeRegistry::commonNodeProperties()
|
||||||
|
{
|
||||||
|
PropertySpec title;
|
||||||
|
title.id = QStringLiteral("title");
|
||||||
|
title.name = QStringLiteral("Title");
|
||||||
|
title.group = QStringLiteral("General");
|
||||||
|
title.type = PropertyType::String;
|
||||||
|
title.defaultValue = QString();
|
||||||
|
|
||||||
|
PropertySpec labelColor;
|
||||||
|
labelColor.id = QStringLiteral("labelColor");
|
||||||
|
labelColor.name = QStringLiteral("Label color");
|
||||||
|
labelColor.group = QStringLiteral("Presentation");
|
||||||
|
labelColor.type = PropertyType::Color;
|
||||||
|
labelColor.defaultValue = QString(); // empty = theme default
|
||||||
|
|
||||||
|
PropertySpec tags;
|
||||||
|
tags.id = QStringLiteral("tags");
|
||||||
|
tags.name = QStringLiteral("Tags");
|
||||||
|
tags.group = QStringLiteral("General");
|
||||||
|
tags.type = PropertyType::ValueList;
|
||||||
|
tags.defaultValue = QStringList();
|
||||||
|
|
||||||
|
return {title, tags, labelColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<PropertySpec> TypeRegistry::commonEdgeProperties()
|
||||||
|
{
|
||||||
|
QList<PropertySpec> out;
|
||||||
|
|
||||||
|
PropertySpec title;
|
||||||
|
title.id = QStringLiteral("title");
|
||||||
|
title.name = QStringLiteral("Title");
|
||||||
|
title.group = QStringLiteral("General");
|
||||||
|
title.type = PropertyType::String;
|
||||||
|
title.defaultValue = QString();
|
||||||
|
out << title;
|
||||||
|
|
||||||
|
PropertySpec lineStyle;
|
||||||
|
lineStyle.id = QStringLiteral("lineStyle");
|
||||||
|
lineStyle.name = QStringLiteral("Line style");
|
||||||
|
lineStyle.group = QStringLiteral("Presentation");
|
||||||
|
lineStyle.type = PropertyType::Enum;
|
||||||
|
lineStyle.enumValues = {QStringLiteral("Solid"), QStringLiteral("Dashed"),
|
||||||
|
QStringLiteral("Dotted"), QStringLiteral("DashDot")};
|
||||||
|
lineStyle.defaultValue = QStringLiteral("Solid");
|
||||||
|
out << lineStyle;
|
||||||
|
|
||||||
|
PropertySpec width;
|
||||||
|
width.id = QStringLiteral("lineWidth");
|
||||||
|
width.name = QStringLiteral("Line width");
|
||||||
|
width.group = QStringLiteral("Presentation");
|
||||||
|
width.type = PropertyType::Float;
|
||||||
|
width.defaultValue = 2.0;
|
||||||
|
width.min = 0.5;
|
||||||
|
width.max = 10.0;
|
||||||
|
out << width;
|
||||||
|
|
||||||
|
PropertySpec color;
|
||||||
|
color.id = QStringLiteral("color");
|
||||||
|
color.name = QStringLiteral("Color");
|
||||||
|
color.group = QStringLiteral("Presentation");
|
||||||
|
color.type = PropertyType::Color;
|
||||||
|
color.defaultValue = QString(); // empty = relation color
|
||||||
|
out << color;
|
||||||
|
|
||||||
|
const QStringList decorations = {QStringLiteral("None"), QStringLiteral("Arrow"),
|
||||||
|
QStringLiteral("Circle"), QStringLiteral("Diamond"),
|
||||||
|
QStringLiteral("Bar")};
|
||||||
|
|
||||||
|
PropertySpec head;
|
||||||
|
head.id = QStringLiteral("headDecoration");
|
||||||
|
head.name = QStringLiteral("Head decoration");
|
||||||
|
head.group = QStringLiteral("Presentation");
|
||||||
|
head.type = PropertyType::Enum;
|
||||||
|
head.enumValues = decorations;
|
||||||
|
head.defaultValue = QStringLiteral("Arrow");
|
||||||
|
out << head;
|
||||||
|
|
||||||
|
PropertySpec tail;
|
||||||
|
tail.id = QStringLiteral("tailDecoration");
|
||||||
|
tail.name = QStringLiteral("Tail decoration");
|
||||||
|
tail.group = QStringLiteral("Presentation");
|
||||||
|
tail.type = PropertyType::Enum;
|
||||||
|
tail.enumValues = decorations;
|
||||||
|
tail.defaultValue = QStringLiteral("None");
|
||||||
|
out << tail;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<PropertySpec> TypeRegistry::nodeProperties(const QString& typeId) const
|
||||||
|
{
|
||||||
|
QList<PropertySpec> out = commonNodeProperties();
|
||||||
|
if (const NodeType* t = nodeType(typeId))
|
||||||
|
out.append(t->properties);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<PropertySpec> TypeRegistry::edgeProperties(const QString& relationId) const
|
||||||
|
{
|
||||||
|
QList<PropertySpec> out = commonEdgeProperties();
|
||||||
|
out.append(m_edgeProps.value(relationId));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertySpec propertySpecFromJson(const QJsonObject& o)
|
||||||
|
{
|
||||||
|
PropertySpec s;
|
||||||
|
s.id = o.value(QLatin1String("id")).toString();
|
||||||
|
s.name = o.value(QLatin1String("name")).toString(s.id);
|
||||||
|
s.group = o.value(QLatin1String("group")).toString(QStringLiteral("General"));
|
||||||
|
s.type = propertyTypeFromString(o.value(QLatin1String("type")).toString());
|
||||||
|
s.unit = o.value(QLatin1String("unit")).toString();
|
||||||
|
s.catalogueId = o.value(QLatin1String("catalogue")).toString();
|
||||||
|
s.min = o.value(QLatin1String("min")).toDouble();
|
||||||
|
s.max = o.value(QLatin1String("max")).toDouble();
|
||||||
|
s.readOnly = o.value(QLatin1String("readOnly")).toBool();
|
||||||
|
for (const auto& v : o.value(QLatin1String("values")).toArray())
|
||||||
|
s.enumValues.append(v.toString());
|
||||||
|
const QJsonValue def = o.value(QLatin1String("default"));
|
||||||
|
switch (s.type) {
|
||||||
|
case PropertyType::Int:
|
||||||
|
s.defaultValue = def.toInt();
|
||||||
|
break;
|
||||||
|
case PropertyType::Float:
|
||||||
|
s.defaultValue = def.toDouble();
|
||||||
|
break;
|
||||||
|
case PropertyType::CatalogueItems:
|
||||||
|
case PropertyType::ValueList: {
|
||||||
|
QStringList list;
|
||||||
|
for (const auto& v : def.toArray())
|
||||||
|
list.append(v.toString());
|
||||||
|
s.defaultValue = list;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
s.defaultValue = def.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QPointF pointFromJson(const QJsonValue& v, QPointF fallback = {})
|
||||||
|
{
|
||||||
|
const QJsonArray a = v.toArray();
|
||||||
|
if (a.size() == 2)
|
||||||
|
return {a.at(0).toDouble(), a.at(1).toDouble()};
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeRegistry::loadFromJson(const QByteArray& jsonData, QString* error)
|
||||||
|
{
|
||||||
|
QJsonParseError perr;
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonData, &perr);
|
||||||
|
if (doc.isNull()) {
|
||||||
|
if (error)
|
||||||
|
*error = perr.errorString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
clear();
|
||||||
|
const QJsonObject root = doc.object();
|
||||||
|
|
||||||
|
for (const auto& rv : root.value(QLatin1String("relations")).toArray()) {
|
||||||
|
const QJsonObject o = rv.toObject();
|
||||||
|
Relation r;
|
||||||
|
r.id = o.value(QLatin1String("id")).toString();
|
||||||
|
r.name = o.value(QLatin1String("name")).toString(r.id);
|
||||||
|
r.color = QColor(o.value(QLatin1String("color")).toString());
|
||||||
|
r.darkColor = QColor(o.value(QLatin1String("darkColor")).toString(
|
||||||
|
o.value(QLatin1String("color")).toString()));
|
||||||
|
addRelation(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& cv : root.value(QLatin1String("catalogues")).toArray()) {
|
||||||
|
const QJsonObject o = cv.toObject();
|
||||||
|
Catalogue c;
|
||||||
|
c.id = o.value(QLatin1String("id")).toString();
|
||||||
|
c.name = o.value(QLatin1String("name")).toString(c.id);
|
||||||
|
for (const auto& iv : o.value(QLatin1String("items")).toArray()) {
|
||||||
|
const QJsonObject io = iv.toObject();
|
||||||
|
CatalogueItem item;
|
||||||
|
item.id = io.value(QLatin1String("id")).toString();
|
||||||
|
item.name = io.value(QLatin1String("name")).toString(item.id);
|
||||||
|
item.attributes = io.value(QLatin1String("attributes")).toObject().toVariantMap();
|
||||||
|
c.items.append(item);
|
||||||
|
}
|
||||||
|
addCatalogue(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject edgeProps = root.value(QLatin1String("edgeProperties")).toObject();
|
||||||
|
for (auto it = edgeProps.begin(); it != edgeProps.end(); ++it) {
|
||||||
|
QList<PropertySpec> specs;
|
||||||
|
for (const auto& pv : it.value().toArray())
|
||||||
|
specs.append(propertySpecFromJson(pv.toObject()));
|
||||||
|
setEdgeProperties(it.key(), specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& tv : root.value(QLatin1String("nodeTypes")).toArray()) {
|
||||||
|
const QJsonObject o = tv.toObject();
|
||||||
|
NodeType t;
|
||||||
|
t.id = o.value(QLatin1String("id")).toString();
|
||||||
|
t.name = o.value(QLatin1String("name")).toString(t.id);
|
||||||
|
t.group = o.value(QLatin1String("group")).toString(QStringLiteral("Misc"));
|
||||||
|
t.svgPath = o.value(QLatin1String("svg")).toString();
|
||||||
|
const QPointF sz = pointFromJson(o.value(QLatin1String("size")), {60, 60});
|
||||||
|
t.size = QSizeF(sz.x(), sz.y());
|
||||||
|
for (const auto& pv : o.value(QLatin1String("ports")).toArray()) {
|
||||||
|
const QJsonObject po = pv.toObject();
|
||||||
|
PortSpec p;
|
||||||
|
p.id = po.value(QLatin1String("id")).toString();
|
||||||
|
p.relation = po.value(QLatin1String("relation")).toString();
|
||||||
|
p.direction = portDirectionFromString(po.value(QLatin1String("direction")).toString());
|
||||||
|
p.pos = pointFromJson(po.value(QLatin1String("pos")), {0.5, 0.5});
|
||||||
|
p.side = sideFromString(po.value(QLatin1String("side")).toString());
|
||||||
|
p.maxConnections = po.value(QLatin1String("maxConnections")).toInt(1);
|
||||||
|
t.ports.append(p);
|
||||||
|
}
|
||||||
|
for (const auto& pv : o.value(QLatin1String("properties")).toArray())
|
||||||
|
t.properties.append(propertySpecFromJson(pv.toObject()));
|
||||||
|
addNodeType(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TypeRegistry::loadFromFile(const QString& path, QString* error)
|
||||||
|
{
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::ReadOnly)) {
|
||||||
|
if (error)
|
||||||
|
*error = f.errorString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return loadFromJson(f.readAll(), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace diag
|
||||||
55
src/core/TypeRegistry.h
Normal file
55
src/core/TypeRegistry.h
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Types.h"
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace diag {
|
||||||
|
|
||||||
|
// Holds relation, node-type, catalogue and edge-property definitions.
|
||||||
|
// Definitions are data-driven: loaded from a JSON config (see
|
||||||
|
// resources/config/nodetypes.json). Common presentation/general property
|
||||||
|
// specs for nodes and edges are built in.
|
||||||
|
class TypeRegistry {
|
||||||
|
public:
|
||||||
|
TypeRegistry();
|
||||||
|
|
||||||
|
static TypeRegistry& instance();
|
||||||
|
|
||||||
|
bool loadFromJson(const QByteArray& jsonData, QString* error = nullptr);
|
||||||
|
bool loadFromFile(const QString& path, QString* error = nullptr);
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
void addRelation(const Relation& r);
|
||||||
|
void addNodeType(const NodeType& t);
|
||||||
|
void addCatalogue(const Catalogue& c);
|
||||||
|
void setEdgeProperties(const QString& relationId, const QList<PropertySpec>& specs);
|
||||||
|
|
||||||
|
const Relation* relation(const QString& id) const;
|
||||||
|
QList<Relation> relations() const;
|
||||||
|
|
||||||
|
const NodeType* nodeType(const QString& id) const;
|
||||||
|
QList<const NodeType*> nodeTypes() const;
|
||||||
|
QStringList nodeGroups() const; // in definition order
|
||||||
|
|
||||||
|
const Catalogue* catalogue(const QString& id) const;
|
||||||
|
QList<const Catalogue*> catalogues() const;
|
||||||
|
|
||||||
|
// Full spec list: common specs + type/relation specific ones.
|
||||||
|
QList<PropertySpec> nodeProperties(const QString& typeId) const;
|
||||||
|
QList<PropertySpec> edgeProperties(const QString& relationId) const;
|
||||||
|
|
||||||
|
static QList<PropertySpec> commonNodeProperties();
|
||||||
|
static QList<PropertySpec> commonEdgeProperties();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<Relation> m_relations;
|
||||||
|
QList<NodeType> m_nodeTypes;
|
||||||
|
QList<Catalogue> m_catalogues;
|
||||||
|
QHash<QString, QList<PropertySpec>> m_edgeProps; // by relation id
|
||||||
|
};
|
||||||
|
|
||||||
|
PropertySpec propertySpecFromJson(const QJsonObject& o);
|
||||||
|
|
||||||
|
} // namespace diag
|
||||||
107
src/core/Types.cpp
Normal file
107
src/core/Types.cpp
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
#include "Types.h"
|
||||||
|
|
||||||
|
namespace diag {
|
||||||
|
|
||||||
|
const PortSpec* NodeType::port(const QString& portId) const
|
||||||
|
{
|
||||||
|
for (const auto& p : ports)
|
||||||
|
if (p.id == portId)
|
||||||
|
return &p;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CatalogueItem* Catalogue::item(const QString& itemId) const
|
||||||
|
{
|
||||||
|
for (const auto& i : items)
|
||||||
|
if (i.id == itemId)
|
||||||
|
return &i;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF sideVector(Side s)
|
||||||
|
{
|
||||||
|
switch (s) {
|
||||||
|
case Side::Left: return {-1, 0};
|
||||||
|
case Side::Right: return {1, 0};
|
||||||
|
case Side::Top: return {0, -1};
|
||||||
|
case Side::Bottom: return {0, 1};
|
||||||
|
}
|
||||||
|
return {1, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString portDirectionToString(PortDirection d)
|
||||||
|
{
|
||||||
|
switch (d) {
|
||||||
|
case PortDirection::In: return QStringLiteral("in");
|
||||||
|
case PortDirection::Out: return QStringLiteral("out");
|
||||||
|
case PortDirection::InOut: return QStringLiteral("inout");
|
||||||
|
}
|
||||||
|
return QStringLiteral("inout");
|
||||||
|
}
|
||||||
|
|
||||||
|
PortDirection portDirectionFromString(const QString& s)
|
||||||
|
{
|
||||||
|
if (s == QLatin1String("in"))
|
||||||
|
return PortDirection::In;
|
||||||
|
if (s == QLatin1String("out"))
|
||||||
|
return PortDirection::Out;
|
||||||
|
return PortDirection::InOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString sideToString(Side s)
|
||||||
|
{
|
||||||
|
switch (s) {
|
||||||
|
case Side::Left: return QStringLiteral("left");
|
||||||
|
case Side::Right: return QStringLiteral("right");
|
||||||
|
case Side::Top: return QStringLiteral("top");
|
||||||
|
case Side::Bottom: return QStringLiteral("bottom");
|
||||||
|
}
|
||||||
|
return QStringLiteral("left");
|
||||||
|
}
|
||||||
|
|
||||||
|
Side sideFromString(const QString& s)
|
||||||
|
{
|
||||||
|
if (s == QLatin1String("right"))
|
||||||
|
return Side::Right;
|
||||||
|
if (s == QLatin1String("top"))
|
||||||
|
return Side::Top;
|
||||||
|
if (s == QLatin1String("bottom"))
|
||||||
|
return Side::Bottom;
|
||||||
|
return Side::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString propertyTypeToString(PropertyType t)
|
||||||
|
{
|
||||||
|
switch (t) {
|
||||||
|
case PropertyType::String: return QStringLiteral("string");
|
||||||
|
case PropertyType::Int: return QStringLiteral("int");
|
||||||
|
case PropertyType::Float: return QStringLiteral("float");
|
||||||
|
case PropertyType::Enum: return QStringLiteral("enum");
|
||||||
|
case PropertyType::CatalogueItem: return QStringLiteral("catalogueItem");
|
||||||
|
case PropertyType::CatalogueItems: return QStringLiteral("catalogueItems");
|
||||||
|
case PropertyType::Color: return QStringLiteral("color");
|
||||||
|
case PropertyType::ValueList: return QStringLiteral("valueList");
|
||||||
|
}
|
||||||
|
return QStringLiteral("string");
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyType propertyTypeFromString(const QString& s)
|
||||||
|
{
|
||||||
|
if (s == QLatin1String("int"))
|
||||||
|
return PropertyType::Int;
|
||||||
|
if (s == QLatin1String("float"))
|
||||||
|
return PropertyType::Float;
|
||||||
|
if (s == QLatin1String("enum"))
|
||||||
|
return PropertyType::Enum;
|
||||||
|
if (s == QLatin1String("catalogueItem"))
|
||||||
|
return PropertyType::CatalogueItem;
|
||||||
|
if (s == QLatin1String("catalogueItems"))
|
||||||
|
return PropertyType::CatalogueItems;
|
||||||
|
if (s == QLatin1String("color"))
|
||||||
|
return PropertyType::Color;
|
||||||
|
if (s == QLatin1String("valueList"))
|
||||||
|
return PropertyType::ValueList;
|
||||||
|
return PropertyType::String;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace diag
|
||||||
118
src/core/Types.h
Normal file
118
src/core/Types.h
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QSizeF>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
namespace diag {
|
||||||
|
|
||||||
|
// Which side of the node rectangle a port sits on; defines the exit
|
||||||
|
// direction of the first routed segment.
|
||||||
|
enum class Side { Left, Right, Top, Bottom };
|
||||||
|
|
||||||
|
enum class PortDirection { In, Out, InOut };
|
||||||
|
|
||||||
|
// A connection domain (water, power, ...). Edges may only join ports of the
|
||||||
|
// same relation. Colors are per color scheme.
|
||||||
|
struct Relation {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QColor color; // light scheme
|
||||||
|
QColor darkColor; // dark / high-contrast schemes
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PortSpec {
|
||||||
|
QString id;
|
||||||
|
QString relation;
|
||||||
|
PortDirection direction = PortDirection::InOut;
|
||||||
|
QPointF pos; // relative [0..1] within the node rect
|
||||||
|
Side side = Side::Left;
|
||||||
|
int maxConnections = 1; // 0 = unlimited
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class PropertyType {
|
||||||
|
String,
|
||||||
|
Int,
|
||||||
|
Float,
|
||||||
|
Enum,
|
||||||
|
CatalogueItem, // value: item id (QString)
|
||||||
|
CatalogueItems, // value: item ids (QStringList)
|
||||||
|
Color, // value: "#rrggbb" string; empty = automatic
|
||||||
|
ValueList, // value: QStringList of free-form values
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PropertySpec {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QString group; // "General", "Presentation", "Physics", ...
|
||||||
|
PropertyType type = PropertyType::String;
|
||||||
|
QVariant defaultValue;
|
||||||
|
QString unit;
|
||||||
|
QStringList enumValues;
|
||||||
|
QString catalogueId;
|
||||||
|
double min = 0.0;
|
||||||
|
double max = 0.0; // min == max means unbounded
|
||||||
|
bool readOnly = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NodeType {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QString group; // palette group
|
||||||
|
QString svgPath; // resource path of the vector image
|
||||||
|
QSizeF size{60, 60};
|
||||||
|
QList<PortSpec> ports;
|
||||||
|
QList<PropertySpec> properties; // in addition to common node properties
|
||||||
|
|
||||||
|
const PortSpec* port(const QString& portId) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CatalogueItem {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QVariantMap attributes; // e.g. {"diameter": 50, "material": "steel"}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Catalogue {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QList<CatalogueItem> items;
|
||||||
|
|
||||||
|
const CatalogueItem* item(const QString& itemId) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
QUuid id;
|
||||||
|
QString typeId;
|
||||||
|
QPointF pos; // scene coordinates of the node rect's top-left corner
|
||||||
|
QVariantMap props;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Edge {
|
||||||
|
QUuid id;
|
||||||
|
QUuid fromNode;
|
||||||
|
QString fromPort;
|
||||||
|
QUuid toNode;
|
||||||
|
QString toPort;
|
||||||
|
QString relation;
|
||||||
|
QVariantMap props;
|
||||||
|
double flowRate = 0.0; // solver result; positive flows from -> to
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unit vector pointing away from the node for a given side.
|
||||||
|
QPointF sideVector(Side s);
|
||||||
|
|
||||||
|
QString portDirectionToString(PortDirection d);
|
||||||
|
PortDirection portDirectionFromString(const QString& s);
|
||||||
|
QString sideToString(Side s);
|
||||||
|
Side sideFromString(const QString& s);
|
||||||
|
QString propertyTypeToString(PropertyType t);
|
||||||
|
PropertyType propertyTypeFromString(const QString& s);
|
||||||
|
|
||||||
|
} // namespace diag
|
||||||
10
tests/CMakeLists.txt
Normal file
10
tests/CMakeLists.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Test targets are added per module; all run offscreen via ctest.
|
||||||
|
function(diag_add_test NAME)
|
||||||
|
add_executable(${NAME} ${NAME}.cpp)
|
||||||
|
target_link_libraries(${NAME} PRIVATE ${ARGN} Qt6::Test)
|
||||||
|
add_test(NAME ${NAME} COMMAND ${NAME})
|
||||||
|
set_tests_properties(${NAME} PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
diag_add_test(tst_model diagcore)
|
||||||
|
diag_add_test(tst_properties diagcore)
|
||||||
135
tests/TestFixtures.h
Normal file
135
tests/TestFixtures.h
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Shared in-code registry fixture: a minimal water + power schema mirroring
|
||||||
|
// the shape of the shipped JSON config, without resource dependencies.
|
||||||
|
|
||||||
|
#include "core/TypeRegistry.h"
|
||||||
|
|
||||||
|
namespace fixtures {
|
||||||
|
|
||||||
|
inline diag::PropertySpec floatSpec(const QString& id, const QString& name, const QString& group,
|
||||||
|
double def, const QString& unit = {})
|
||||||
|
{
|
||||||
|
diag::PropertySpec s;
|
||||||
|
s.id = id;
|
||||||
|
s.name = name;
|
||||||
|
s.group = group;
|
||||||
|
s.type = diag::PropertyType::Float;
|
||||||
|
s.defaultValue = def;
|
||||||
|
s.unit = unit;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline diag::PortSpec port(const QString& id, const QString& relation, diag::PortDirection dir,
|
||||||
|
QPointF pos, diag::Side side, int maxConnections = 1)
|
||||||
|
{
|
||||||
|
diag::PortSpec p;
|
||||||
|
p.id = id;
|
||||||
|
p.relation = relation;
|
||||||
|
p.direction = dir;
|
||||||
|
p.pos = pos;
|
||||||
|
p.side = side;
|
||||||
|
p.maxConnections = maxConnections;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void fillRegistry(diag::TypeRegistry& reg)
|
||||||
|
{
|
||||||
|
using namespace diag;
|
||||||
|
reg.clear();
|
||||||
|
|
||||||
|
reg.addRelation({QStringLiteral("water"), QStringLiteral("Water"), QColor("#1c78c0"),
|
||||||
|
QColor("#4aa3e8")});
|
||||||
|
reg.addRelation({QStringLiteral("power"), QStringLiteral("Power"), QColor("#d99000"),
|
||||||
|
QColor("#f0b429")});
|
||||||
|
|
||||||
|
Catalogue pipes;
|
||||||
|
pipes.id = QStringLiteral("pipe_series");
|
||||||
|
pipes.name = QStringLiteral("Pipe series");
|
||||||
|
pipes.items = {{QStringLiteral("dn50"), QStringLiteral("DN50 Steel"),
|
||||||
|
{{QStringLiteral("diameter"), 50}}},
|
||||||
|
{QStringLiteral("dn100"), QStringLiteral("DN100 Steel"),
|
||||||
|
{{QStringLiteral("diameter"), 100}}}};
|
||||||
|
reg.addCatalogue(pipes);
|
||||||
|
|
||||||
|
QList<PropertySpec> waterEdge;
|
||||||
|
waterEdge << floatSpec(QStringLiteral("length"), QStringLiteral("Length"),
|
||||||
|
QStringLiteral("Physics"), 10.0, QStringLiteral("m"));
|
||||||
|
waterEdge << floatSpec(QStringLiteral("diameter"), QStringLiteral("Diameter"),
|
||||||
|
QStringLiteral("Physics"), 100.0, QStringLiteral("mm"));
|
||||||
|
PropertySpec flowType;
|
||||||
|
flowType.id = QStringLiteral("flowType");
|
||||||
|
flowType.name = QStringLiteral("Flow type");
|
||||||
|
flowType.group = QStringLiteral("Physics");
|
||||||
|
flowType.type = PropertyType::Enum;
|
||||||
|
flowType.enumValues = {QStringLiteral("Laminar"), QStringLiteral("Turbulent")};
|
||||||
|
flowType.defaultValue = QStringLiteral("Laminar");
|
||||||
|
waterEdge << flowType;
|
||||||
|
PropertySpec series;
|
||||||
|
series.id = QStringLiteral("series");
|
||||||
|
series.name = QStringLiteral("Pipe series");
|
||||||
|
series.group = QStringLiteral("Physics");
|
||||||
|
series.type = PropertyType::CatalogueItem;
|
||||||
|
series.catalogueId = QStringLiteral("pipe_series");
|
||||||
|
waterEdge << series;
|
||||||
|
reg.setEdgeProperties(QStringLiteral("water"), waterEdge);
|
||||||
|
|
||||||
|
NodeType source;
|
||||||
|
source.id = QStringLiteral("source");
|
||||||
|
source.name = QStringLiteral("Source");
|
||||||
|
source.group = QStringLiteral("Water");
|
||||||
|
source.size = {60, 60};
|
||||||
|
source.ports = {port(QStringLiteral("out"), QStringLiteral("water"), PortDirection::Out,
|
||||||
|
{1.0, 0.5}, Side::Right, 0)};
|
||||||
|
source.properties = {floatSpec(QStringLiteral("supply"), QStringLiteral("Supply"),
|
||||||
|
QStringLiteral("Physics"), 10.0, QStringLiteral("m³/h"))};
|
||||||
|
reg.addNodeType(source);
|
||||||
|
|
||||||
|
NodeType pump;
|
||||||
|
pump.id = QStringLiteral("pump");
|
||||||
|
pump.name = QStringLiteral("Pump");
|
||||||
|
pump.group = QStringLiteral("Water");
|
||||||
|
pump.size = {60, 60};
|
||||||
|
pump.ports = {port(QStringLiteral("in"), QStringLiteral("water"), PortDirection::In,
|
||||||
|
{0.0, 0.5}, Side::Left),
|
||||||
|
port(QStringLiteral("out"), QStringLiteral("water"), PortDirection::Out,
|
||||||
|
{1.0, 0.5}, Side::Right)};
|
||||||
|
reg.addNodeType(pump);
|
||||||
|
|
||||||
|
NodeType junction;
|
||||||
|
junction.id = QStringLiteral("junction");
|
||||||
|
junction.name = QStringLiteral("Junction");
|
||||||
|
junction.group = QStringLiteral("Water");
|
||||||
|
junction.size = {40, 40};
|
||||||
|
junction.ports = {port(QStringLiteral("w"), QStringLiteral("water"), PortDirection::InOut,
|
||||||
|
{0.0, 0.5}, Side::Left, 0),
|
||||||
|
port(QStringLiteral("e"), QStringLiteral("water"), PortDirection::InOut,
|
||||||
|
{1.0, 0.5}, Side::Right, 0),
|
||||||
|
port(QStringLiteral("n"), QStringLiteral("water"), PortDirection::InOut,
|
||||||
|
{0.5, 0.0}, Side::Top, 0),
|
||||||
|
port(QStringLiteral("s"), QStringLiteral("water"), PortDirection::InOut,
|
||||||
|
{0.5, 1.0}, Side::Bottom, 0)};
|
||||||
|
reg.addNodeType(junction);
|
||||||
|
|
||||||
|
NodeType consumer;
|
||||||
|
consumer.id = QStringLiteral("consumer");
|
||||||
|
consumer.name = QStringLiteral("Consumer");
|
||||||
|
consumer.group = QStringLiteral("Water");
|
||||||
|
consumer.size = {60, 60};
|
||||||
|
consumer.ports = {port(QStringLiteral("in"), QStringLiteral("water"), PortDirection::In,
|
||||||
|
{0.0, 0.5}, Side::Left)};
|
||||||
|
consumer.properties = {floatSpec(QStringLiteral("demand"), QStringLiteral("Demand"),
|
||||||
|
QStringLiteral("Physics"), 10.0, QStringLiteral("m³/h"))};
|
||||||
|
reg.addNodeType(consumer);
|
||||||
|
|
||||||
|
NodeType generator;
|
||||||
|
generator.id = QStringLiteral("generator");
|
||||||
|
generator.name = QStringLiteral("Generator");
|
||||||
|
generator.group = QStringLiteral("Power");
|
||||||
|
generator.size = {60, 60};
|
||||||
|
generator.ports = {port(QStringLiteral("out"), QStringLiteral("power"), PortDirection::Out,
|
||||||
|
{1.0, 0.5}, Side::Right, 0)};
|
||||||
|
reg.addNodeType(generator);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fixtures
|
||||||
145
tests/tst_model.cpp
Normal file
145
tests/tst_model.cpp
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
#include "TestFixtures.h"
|
||||||
|
#include "core/NetworkModel.h"
|
||||||
|
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
using namespace diag;
|
||||||
|
|
||||||
|
class TestModel : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
void init()
|
||||||
|
{
|
||||||
|
fixtures::fillRegistry(m_reg);
|
||||||
|
delete m_model;
|
||||||
|
m_model = new NetworkModel(&m_reg, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addNode_unknownTypeFails()
|
||||||
|
{
|
||||||
|
QVERIFY(m_model->addNode(QStringLiteral("nope"), {0, 0}).isNull());
|
||||||
|
QCOMPARE(m_model->nodeIds().size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addNode_emitsAndStores()
|
||||||
|
{
|
||||||
|
QSignalSpy spy(m_model, &NetworkModel::nodeAdded);
|
||||||
|
const QUuid id = m_model->addNode(QStringLiteral("pump"), {100, 40});
|
||||||
|
QVERIFY(!id.isNull());
|
||||||
|
QCOMPARE(spy.count(), 1);
|
||||||
|
QCOMPARE(m_model->node(id)->pos, QPointF(100, 40));
|
||||||
|
QCOMPARE(m_model->nodeTypeOf(id)->id, QStringLiteral("pump"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect_validOutToIn()
|
||||||
|
{
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
||||||
|
QVERIFY(m_model->canConnect(src, "out", pump, "in"));
|
||||||
|
const QUuid e = m_model->addEdge(src, "out", pump, "in");
|
||||||
|
QVERIFY(!e.isNull());
|
||||||
|
QCOMPARE(m_model->edge(e)->relation, QStringLiteral("water"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect_directionNormalized()
|
||||||
|
{
|
||||||
|
// Dragging from the In port must still produce an Out->In edge.
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
||||||
|
const QUuid e = m_model->addEdge(pump, "in", src, "out");
|
||||||
|
QVERIFY(!e.isNull());
|
||||||
|
QCOMPARE(m_model->edge(e)->fromNode, src);
|
||||||
|
QCOMPARE(m_model->edge(e)->toNode, pump);
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect_relationMismatchFails()
|
||||||
|
{
|
||||||
|
const QUuid gen = m_model->addNode(QStringLiteral("generator"), {0, 0});
|
||||||
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
||||||
|
QString reason;
|
||||||
|
QVERIFY(!m_model->canConnect(gen, "out", pump, "in", &reason));
|
||||||
|
QVERIFY(reason.contains(QStringLiteral("relation")));
|
||||||
|
QVERIFY(m_model->addEdge(gen, "out", pump, "in").isNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect_twoInputsFails()
|
||||||
|
{
|
||||||
|
const QUuid a = m_model->addNode(QStringLiteral("pump"), {0, 0});
|
||||||
|
const QUuid b = m_model->addNode(QStringLiteral("consumer"), {200, 0});
|
||||||
|
QVERIFY(!m_model->canConnect(a, "in", b, "in"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect_selfPortFails()
|
||||||
|
{
|
||||||
|
const QUuid j = m_model->addNode(QStringLiteral("junction"), {0, 0});
|
||||||
|
QVERIFY(!m_model->canConnect(j, "w", j, "w"));
|
||||||
|
// Different ports on the same node are allowed.
|
||||||
|
QVERIFY(m_model->canConnect(j, "w", j, "e"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect_occupiedPortFails()
|
||||||
|
{
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
const QUuid p1 = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
||||||
|
const QUuid p2 = m_model->addNode(QStringLiteral("pump"), {200, 100});
|
||||||
|
QVERIFY(!m_model->addEdge(src, "out", p1, "in").isNull());
|
||||||
|
QString reason;
|
||||||
|
QVERIFY(!m_model->canConnect(p2, "out", p1, "in", &reason));
|
||||||
|
QVERIFY(reason.contains(QStringLiteral("occupied")));
|
||||||
|
// Source "out" is unlimited (maxConnections = 0).
|
||||||
|
QVERIFY(m_model->canConnect(src, "out", p2, "in"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void connect_duplicateEdgeFails()
|
||||||
|
{
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
const QUuid j = m_model->addNode(QStringLiteral("junction"), {200, 0});
|
||||||
|
QVERIFY(!m_model->addEdge(src, "out", j, "w").isNull());
|
||||||
|
QVERIFY(!m_model->canConnect(src, "out", j, "w"));
|
||||||
|
QVERIFY(!m_model->canConnect(j, "w", src, "out"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void junction_allowsManyConnections()
|
||||||
|
{
|
||||||
|
const QUuid j = m_model->addNode(QStringLiteral("junction"), {0, 0});
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {-200, 0});
|
||||||
|
const QUuid c1 = m_model->addNode(QStringLiteral("consumer"), {200, -100});
|
||||||
|
const QUuid c2 = m_model->addNode(QStringLiteral("consumer"), {200, 100});
|
||||||
|
QVERIFY(!m_model->addEdge(src, "out", j, "w").isNull());
|
||||||
|
QVERIFY(!m_model->addEdge(j, "e", c1, "in").isNull());
|
||||||
|
QVERIFY(!m_model->addEdge(j, "e", c2, "in").isNull());
|
||||||
|
QCOMPARE(m_model->connectionCount(j, "e"), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeNode_cascadesEdges()
|
||||||
|
{
|
||||||
|
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});
|
||||||
|
m_model->addEdge(src, "out", pump, "in");
|
||||||
|
m_model->addEdge(pump, "out", c, "in");
|
||||||
|
QSignalSpy edgeSpy(m_model, &NetworkModel::edgeRemoved);
|
||||||
|
m_model->removeNode(pump);
|
||||||
|
QCOMPARE(edgeSpy.count(), 2);
|
||||||
|
QCOMPARE(m_model->edgeIds().size(), 0);
|
||||||
|
QCOMPARE(m_model->nodeIds().size(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveNode_emitsAndUpdatesPortPos()
|
||||||
|
{
|
||||||
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {0, 0});
|
||||||
|
QSignalSpy spy(m_model, &NetworkModel::nodeMoved);
|
||||||
|
m_model->moveNode(pump, {100, 100});
|
||||||
|
QCOMPARE(spy.count(), 1);
|
||||||
|
// Port "out" is at relative (1.0, 0.5) of a 60x60 node.
|
||||||
|
QCOMPARE(m_model->portScenePos(pump, "out"), QPointF(160, 130));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
TypeRegistry m_reg;
|
||||||
|
NetworkModel* m_model = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(TestModel)
|
||||||
|
#include "tst_model.moc"
|
||||||
134
tests/tst_properties.cpp
Normal file
134
tests/tst_properties.cpp
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#include "TestFixtures.h"
|
||||||
|
#include "core/NetworkModel.h"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
using namespace diag;
|
||||||
|
|
||||||
|
class TestProperties : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
void init()
|
||||||
|
{
|
||||||
|
fixtures::fillRegistry(m_reg);
|
||||||
|
delete m_model;
|
||||||
|
m_model = new NetworkModel(&m_reg, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nodeProperty_fallsBackToDefault()
|
||||||
|
{
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
QCOMPARE(m_model->nodeProperty(src, "supply").toDouble(), 10.0);
|
||||||
|
QCOMPARE(m_model->nodeProperty(src, "title").toString(), QString());
|
||||||
|
m_model->setNodeProperty(src, "supply", 42.5);
|
||||||
|
QCOMPARE(m_model->nodeProperty(src, "supply").toDouble(), 42.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void edgeProperty_commonAndRelationSpecs()
|
||||||
|
{
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
||||||
|
const QUuid e = m_model->addEdge(src, "out", pump, "in");
|
||||||
|
|
||||||
|
const auto specs = m_reg.edgeProperties(QStringLiteral("water"));
|
||||||
|
QStringList ids;
|
||||||
|
for (const auto& s : specs)
|
||||||
|
ids << s.id;
|
||||||
|
// Common presentation specs plus water physics specs.
|
||||||
|
for (const char* expected : {"title", "lineStyle", "lineWidth", "color",
|
||||||
|
"headDecoration", "tailDecoration", "length", "diameter",
|
||||||
|
"flowType", "series"})
|
||||||
|
QVERIFY2(ids.contains(QLatin1String(expected)), expected);
|
||||||
|
|
||||||
|
QCOMPARE(m_model->edgeProperty(e, "lineStyle").toString(), QStringLiteral("Solid"));
|
||||||
|
QCOMPARE(m_model->edgeProperty(e, "diameter").toDouble(), 100.0);
|
||||||
|
QCOMPARE(m_model->edgeProperty(e, "flowType").toString(), QStringLiteral("Laminar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void propertyGroups_present()
|
||||||
|
{
|
||||||
|
const auto specs = m_reg.edgeProperties(QStringLiteral("water"));
|
||||||
|
QStringList groups;
|
||||||
|
for (const auto& s : specs)
|
||||||
|
if (!groups.contains(s.group))
|
||||||
|
groups << s.group;
|
||||||
|
QVERIFY(groups.contains(QStringLiteral("General")));
|
||||||
|
QVERIFY(groups.contains(QStringLiteral("Presentation")));
|
||||||
|
QVERIFY(groups.contains(QStringLiteral("Physics")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void allPropertyTypes_storeAndRead()
|
||||||
|
{
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
|
||||||
|
const QUuid e = m_model->addEdge(src, "out", pump, "in");
|
||||||
|
|
||||||
|
m_model->setNodeProperty(src, "title", QStringLiteral("Main intake")); // String
|
||||||
|
m_model->setNodeProperty(src, "tags", QStringList{"district-1", "hp"}); // ValueList
|
||||||
|
m_model->setNodeProperty(src, "labelColor", QStringLiteral("#ff8800")); // Color
|
||||||
|
m_model->setEdgeProperty(e, "length", 125.5); // Float
|
||||||
|
m_model->setEdgeProperty(e, "flowType", QStringLiteral("Turbulent")); // Enum
|
||||||
|
m_model->setEdgeProperty(e, "series", QStringLiteral("dn50")); // CatalogueItem
|
||||||
|
|
||||||
|
QCOMPARE(m_model->nodeProperty(src, "title").toString(), QStringLiteral("Main intake"));
|
||||||
|
QCOMPARE(m_model->nodeProperty(src, "tags").toStringList(),
|
||||||
|
(QStringList{"district-1", "hp"}));
|
||||||
|
QCOMPARE(m_model->nodeProperty(src, "labelColor").toString(), QStringLiteral("#ff8800"));
|
||||||
|
QCOMPARE(m_model->edgeProperty(e, "length").toDouble(), 125.5);
|
||||||
|
QCOMPARE(m_model->edgeProperty(e, "flowType").toString(), QStringLiteral("Turbulent"));
|
||||||
|
QCOMPARE(m_model->edgeProperty(e, "series").toString(), QStringLiteral("dn50"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void catalogue_lookup()
|
||||||
|
{
|
||||||
|
const Catalogue* c = m_reg.catalogue(QStringLiteral("pipe_series"));
|
||||||
|
QVERIFY(c);
|
||||||
|
QCOMPARE(c->items.size(), 2);
|
||||||
|
const CatalogueItem* dn50 = c->item(QStringLiteral("dn50"));
|
||||||
|
QVERIFY(dn50);
|
||||||
|
QCOMPARE(dn50->attributes.value(QStringLiteral("diameter")).toInt(), 50);
|
||||||
|
QVERIFY(!c->item(QStringLiteral("missing")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void propertySpec_parsesFromJson()
|
||||||
|
{
|
||||||
|
const auto doc = QJsonDocument::fromJson(R"({
|
||||||
|
"id": "materials", "name": "Materials", "group": "Physics",
|
||||||
|
"type": "catalogueItems", "catalogue": "pipe_series",
|
||||||
|
"default": ["dn50"], "readOnly": false
|
||||||
|
})");
|
||||||
|
const PropertySpec s = propertySpecFromJson(doc.object());
|
||||||
|
QCOMPARE(s.type, PropertyType::CatalogueItems);
|
||||||
|
QCOMPARE(s.catalogueId, QStringLiteral("pipe_series"));
|
||||||
|
QCOMPARE(s.defaultValue.toStringList(), QStringList{"dn50"});
|
||||||
|
|
||||||
|
const auto doc2 = QJsonDocument::fromJson(R"({
|
||||||
|
"id": "mode", "type": "enum", "values": ["A", "B"], "default": "B",
|
||||||
|
"min": 1, "max": 5
|
||||||
|
})");
|
||||||
|
const PropertySpec s2 = propertySpecFromJson(doc2.object());
|
||||||
|
QCOMPARE(s2.enumValues, (QStringList{"A", "B"}));
|
||||||
|
QCOMPARE(s2.defaultValue.toString(), QStringLiteral("B"));
|
||||||
|
QCOMPARE(s2.min, 1.0);
|
||||||
|
QCOMPARE(s2.max, 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void propertyChange_emitsSignal()
|
||||||
|
{
|
||||||
|
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
|
||||||
|
QSignalSpy spy(m_model, &NetworkModel::nodeChanged);
|
||||||
|
m_model->setNodeProperty(src, "title", QStringLiteral("A"));
|
||||||
|
m_model->setNodeProperty(src, "title", QStringLiteral("A")); // no-op
|
||||||
|
QCOMPARE(spy.count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
TypeRegistry m_reg;
|
||||||
|
NetworkModel* m_model = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(TestProperties)
|
||||||
|
#include "tst_properties.moc"
|
||||||
Loading…
x
Reference in New Issue
Block a user