From 12a2ea0cc0d35708ef6f6db5aafebeab7fdec9fc Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Tue, 24 Mar 2026 09:49:47 +0100 Subject: [PATCH 01/26] Improve connection and grid rendering quality - Add antialiasing to connection painting and grid background - Use floating-point pen widths with round cap/join for connections - Apply half-pixel offset to grid lines for crisp rendering - Set cosmetic pen flag on grid lines so width stays constant across zoom --- src/DefaultConnectionPainter.cpp | 34 +++++++++++++++++++++----------- src/GraphicsView.cpp | 23 +++++++++++++++++++-- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/DefaultConnectionPainter.cpp b/src/DefaultConnectionPainter.cpp index d9c11333..f44e1fd6 100644 --- a/src/DefaultConnectionPainter.cpp +++ b/src/DefaultConnectionPainter.cpp @@ -11,6 +11,20 @@ namespace QtNodes { +namespace { + +QPen make_connection_pen(QColor const &color, qreal width, Qt::PenStyle style = Qt::SolidLine) +{ + QPen pen(color); + pen.setWidthF(width); + pen.setStyle(style); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + return pen; +} + +} // namespace + QPainterPath DefaultConnectionPainter::cubicPath(ConnectionGraphicsObject const &connection) const { QPointF const &in = connection.endPoint(PortType::In); @@ -34,10 +48,9 @@ void DefaultConnectionPainter::drawSketchLine(QPainter *painter, if (state.requiresPort() || state.frozen()) { auto const &connectionStyle = QtNodes::StyleCollection::connectionStyle(); - QPen pen; - pen.setWidth(static_cast(connectionStyle.constructionLineWidth())); - pen.setColor(connectionStyle.constructionColor()); - pen.setStyle(Qt::DashLine); + QPen pen = make_connection_pen(connectionStyle.constructionColor(), + connectionStyle.constructionLineWidth(), + Qt::DashLine); painter->setPen(pen); painter->setBrush(Qt::NoBrush); @@ -61,10 +74,9 @@ void DefaultConnectionPainter::drawHoveredOrSelected(QPainter *painter, double const lineWidth = connectionStyle.lineWidth(); - QPen pen; - pen.setWidth(static_cast(2 * lineWidth)); - pen.setColor(selected ? connectionStyle.selectedHaloColor() - : connectionStyle.hoveredColor()); + QPen pen = make_connection_pen(selected ? connectionStyle.selectedHaloColor() + : connectionStyle.hoveredColor(), + 2.0 * lineWidth); painter->setPen(pen); painter->setBrush(Qt::NoBrush); @@ -123,9 +135,7 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, double const lineWidth = connectionStyle.lineWidth(); // draw normal line - QPen p; - - p.setWidth(lineWidth); + QPen p = make_connection_pen(normalColorOut, lineWidth); bool const selected = cgo.isSelected(); @@ -181,6 +191,8 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, void DefaultConnectionPainter::paint(QPainter *painter, ConnectionGraphicsObject const &cgo) const { + painter->setRenderHint(QPainter::Antialiasing, true); + drawHoveredOrSelected(painter, cgo); drawSketchLine(painter, cgo); diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 42d2854a..ab5cadf5 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -420,6 +420,21 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) { QGraphicsView::drawBackground(painter, r); + painter->setRenderHint(QPainter::Antialiasing, true); + + qreal x_offset = 0.0; + qreal y_offset = 0.0; + + QTransform const view_transform = transform(); + qreal const scale_x = std::abs(view_transform.m11()); + qreal const scale_y = std::abs(view_transform.m22()); + if (scale_x > 0.0) { + x_offset = 0.5 / scale_x; + } + if (scale_y > 0.0) { + y_offset = 0.5 / scale_y; + } + auto drawGrid = [&](double gridStep) { QRect windowRect = rect(); QPointF tl = mapToScene(windowRect.topLeft()); @@ -432,14 +447,16 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) // vertical lines for (int xi = int(left); xi <= int(right); ++xi) { - QLineF line(xi * gridStep, bottom * gridStep, xi * gridStep, top * gridStep); + qreal const x = xi * gridStep + x_offset; + QLineF line(x, bottom * gridStep, x, top * gridStep); painter->drawLine(line); } // horizontal lines for (int yi = int(bottom); yi <= int(top); ++yi) { - QLineF line(left * gridStep, yi * gridStep, right * gridStep, yi * gridStep); + qreal const y = yi * gridStep + y_offset; + QLineF line(left * gridStep, y, right * gridStep, y); painter->drawLine(line); } }; @@ -447,11 +464,13 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) auto const &flowViewStyle = StyleCollection::flowViewStyle(); QPen pfine(flowViewStyle.FineGridColor, 1.0); + pfine.setCosmetic(true); painter->setPen(pfine); drawGrid(15); QPen p(flowViewStyle.CoarseGridColor, 1.0); + p.setCosmetic(true); painter->setPen(p); drawGrid(150); From bb53900978b3977319b6448e34f0cb1825bbc1b0 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Tue, 24 Mar 2026 10:04:18 +0100 Subject: [PATCH 02/26] Add smooth velocity-based zoom to GraphicsView Replace the abrupt 1.2x discrete zoom steps with a velocity/friction model: each wheel notch adds impulse, a 16ms timer decays velocity by 0.75x per frame while applying incremental scale changes around the original wheel-event pivot point. --- include/QtNodes/internal/GraphicsView.hpp | 10 +++ src/GraphicsView.cpp | 89 +++++++++++++++++++++-- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/include/QtNodes/internal/GraphicsView.hpp b/include/QtNodes/internal/GraphicsView.hpp index fdc5af7c..77d1a8b8 100644 --- a/include/QtNodes/internal/GraphicsView.hpp +++ b/include/QtNodes/internal/GraphicsView.hpp @@ -82,6 +82,8 @@ public Q_SLOTS: void showEvent(QShowEvent *event) override; + void timerEvent(QTimerEvent *event) override; + protected: BasicGraphicsScene *nodeScene(); @@ -98,5 +100,13 @@ public Q_SLOTS: QPointF _clickPos; ScaleRange _scaleRange; + + void applyZoomStep(); + void applyZoomFactor(double factor); + void stopZoomTimer(); + + double _zoomVelocity = 0.0; + QPointF _zoomPivot; + int _zoomTimerId = 0; }; } // namespace QtNodes diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index ab5cadf5..15d8b840 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -24,8 +24,20 @@ #include #include +#include + +#include #include +namespace { +constexpr double zoom_friction = 0.75; +constexpr double zoom_impulse_per_step = 1.0; +constexpr double zoom_max_velocity = 5.0; +constexpr double zoom_per_notch = 1.05; +constexpr int zoom_timer_interval_ms = 16; +constexpr double zoom_velocity_epsilon = 0.001; +} // namespace + using QtNodes::BasicGraphicsScene; using QtNodes::DataFlowGraphModel; using QtNodes::GraphicsView; @@ -246,12 +258,79 @@ void GraphicsView::wheelEvent(QWheelEvent *event) return; } - double const d = delta.y() / std::abs(delta.y()); + double const steps = delta.y() / 120.0; + _zoomVelocity = std::clamp(_zoomVelocity + steps * zoom_impulse_per_step, + -zoom_max_velocity, zoom_max_velocity); + _zoomPivot = event->position(); + + if (_zoomTimerId == 0) { + _zoomTimerId = startTimer(zoom_timer_interval_ms); + } +} + +void GraphicsView::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == _zoomTimerId) { + applyZoomStep(); + } else { + QGraphicsView::timerEvent(event); + } +} - if (d > 0.0) - scaleUp(); - else - scaleDown(); +void GraphicsView::applyZoomStep() +{ + if (std::abs(_zoomVelocity) < zoom_velocity_epsilon) { + stopZoomTimer(); + return; + } + + static double const base_k = std::pow(zoom_per_notch, + (1.0 - zoom_friction) / zoom_impulse_per_step); + double const factor = std::pow(base_k, _zoomVelocity); + double const current_scale = transform().m11(); + double const new_scale = current_scale * factor; + + if (_scaleRange.maximum > 0 && new_scale > _scaleRange.maximum) { + applyZoomFactor(_scaleRange.maximum / current_scale); + stopZoomTimer(); + return; + } + if (_scaleRange.minimum > 0 && new_scale < _scaleRange.minimum) { + applyZoomFactor(_scaleRange.minimum / current_scale); + stopZoomTimer(); + return; + } + + applyZoomFactor(factor); + _zoomVelocity *= zoom_friction; +} + +void GraphicsView::applyZoomFactor(double factor) +{ + QPointF const scenePivot = mapToScene(_zoomPivot.toPoint()); + + auto const savedAnchor = transformationAnchor(); + setTransformationAnchor(QGraphicsView::NoAnchor); + + scale(factor, factor); + + QPointF const newPivot = mapFromScene(scenePivot); + QPointF const shift = newPivot - _zoomPivot; + horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(shift.x())); + verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(shift.y())); + + setTransformationAnchor(savedAnchor); + + Q_EMIT scaleChanged(transform().m11()); +} + +void GraphicsView::stopZoomTimer() +{ + if (_zoomTimerId != 0) { + killTimer(_zoomTimerId); + _zoomTimerId = 0; + } + _zoomVelocity = 0.0; } double GraphicsView::getScale() const From abda92f4274fbea8989ab8616ceeae56eca92041 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Tue, 24 Mar 2026 10:13:22 +0100 Subject: [PATCH 03/26] Disable node cache during zoom animation DeviceCoordinateCache on NodeGraphicsObject snaps rendering to integer device pixels, causing visible jitter during smooth zoom. Temporarily switch nodes to NoCache while the zoom timer is active and restore DeviceCoordinateCache when it stops. --- src/GraphicsView.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 15d8b840..0d3a0cde 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -264,6 +264,13 @@ void GraphicsView::wheelEvent(QWheelEvent *event) _zoomPivot = event->position(); if (_zoomTimerId == 0) { + if (scene()) { + for (QGraphicsItem *item : scene()->items()) { + if (qgraphicsitem_cast(item)) { + item->setCacheMode(QGraphicsItem::NoCache); + } + } + } _zoomTimerId = startTimer(zoom_timer_interval_ms); } } @@ -329,6 +336,14 @@ void GraphicsView::stopZoomTimer() if (_zoomTimerId != 0) { killTimer(_zoomTimerId); _zoomTimerId = 0; + + if (scene()) { + for (QGraphicsItem *item : scene()->items()) { + if (qgraphicsitem_cast(item)) { + item->setCacheMode(QGraphicsItem::DeviceCoordinateCache); + } + } + } } _zoomVelocity = 0.0; } From 0b851e46a83df2f5149df1d6d27dd7eca3a65386 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Tue, 24 Mar 2026 10:26:15 +0100 Subject: [PATCH 04/26] Use sub-pixel transform offset during zoom animation Scrollbar values are integers, so adjusting them during smooth zoom causes the entire scene to jump by whole pixels each frame. Fix by splitting the desired offset into integer scrollbar values and a fractional translation baked into the view transform. The sub-pixel translation is removed when the zoom animation stops. --- src/GraphicsView.cpp | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 0d3a0cde..8e85e254 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -315,20 +315,33 @@ void GraphicsView::applyZoomStep() void GraphicsView::applyZoomFactor(double factor) { QPointF const scenePivot = mapToScene(_zoomPivot.toPoint()); + double const newScale = transform().m11() * factor; - auto const savedAnchor = transformationAnchor(); - setTransformationAnchor(QGraphicsView::NoAnchor); + // Compute total offset needed so scenePivot appears at _zoomPivot. + // Mapping: widgetPos = transform * scenePos - scrollOffset + // We need: _zoomPivot = newScale * scenePivot + (tx,ty) - (hbar,vbar) + // Split into integer scrollbar values and sub-pixel transform translation + // to avoid the whole-pixel jumps that integer scrollbars cause. + QPointF const fullOffset(newScale * scenePivot.x() - _zoomPivot.x(), + newScale * scenePivot.y() - _zoomPivot.y()); - scale(factor, factor); + int const hval = qRound(fullOffset.x()); + int const vval = qRound(fullOffset.y()); + double const tx = hval - fullOffset.x(); + double const ty = vval - fullOffset.y(); - QPointF const newPivot = mapFromScene(scenePivot); - QPointF const shift = newPivot - _zoomPivot; - horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(shift.x())); - verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(shift.y())); + QTransform t; + t.translate(tx, ty); + t.scale(newScale, newScale); + auto const savedAnchor = transformationAnchor(); + setTransformationAnchor(QGraphicsView::NoAnchor); + setTransform(t, false); + horizontalScrollBar()->setValue(hval); + verticalScrollBar()->setValue(vval); setTransformationAnchor(savedAnchor); - Q_EMIT scaleChanged(transform().m11()); + Q_EMIT scaleChanged(newScale); } void GraphicsView::stopZoomTimer() @@ -337,6 +350,13 @@ void GraphicsView::stopZoomTimer() killTimer(_zoomTimerId); _zoomTimerId = 0; + // Remove sub-pixel translation from transform so normal interaction + // (hit testing, panning) uses a clean scale-only transform. + double const s = transform().m11(); + QTransform clean; + clean.scale(s, s); + setTransform(clean, false); + if (scene()) { for (QGraphicsItem *item : scene()->items()) { if (qgraphicsitem_cast(item)) { From ed2ac88e5f00040b97dd6ad5476a80ef54c74a20 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Tue, 24 Mar 2026 11:17:36 +0100 Subject: [PATCH 05/26] Smooth zoom rendering and interaction fixes --- include/QtNodes/internal/GraphicsView.hpp | 28 +++ src/DefaultConnectionPainter.cpp | 8 +- src/DefaultNodePainter.cpp | 115 +++++++++++- src/GraphicsView.cpp | 207 +++++++++++++++++----- src/NodeGraphicsObject.cpp | 21 ++- test/src/TestZoomFeatures.cpp | 46 +++++ 6 files changed, 365 insertions(+), 60 deletions(-) diff --git a/include/QtNodes/internal/GraphicsView.hpp b/include/QtNodes/internal/GraphicsView.hpp index 77d1a8b8..682ca3c6 100644 --- a/include/QtNodes/internal/GraphicsView.hpp +++ b/include/QtNodes/internal/GraphicsView.hpp @@ -21,6 +21,19 @@ class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView double maximum = 0; }; + enum class TextRenderingPolicy + { + QtText, + PathWhenZooming, + PathAlways, + }; + + enum class RasterizationPolicy + { + Crisp, + Consistent, + }; + public: GraphicsView(QWidget *parent = Q_NULLPTR); GraphicsView(BasicGraphicsScene *scene, QWidget *parent = Q_NULLPTR); @@ -43,6 +56,18 @@ class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView double getScale() const; + bool isZoomAnimating() const; + + TextRenderingPolicy textRenderingPolicy() const; + + void setTextRenderingPolicy(TextRenderingPolicy policy); + + RasterizationPolicy rasterizationPolicy() const; + + void setRasterizationPolicy(RasterizationPolicy policy); + + void stopZoomAnimation(); + public Q_SLOTS: void scaleUp(); @@ -104,9 +129,12 @@ public Q_SLOTS: void applyZoomStep(); void applyZoomFactor(double factor); void stopZoomTimer(); + void apply_rasterization_policy(); double _zoomVelocity = 0.0; QPointF _zoomPivot; int _zoomTimerId = 0; + TextRenderingPolicy _textRenderingPolicy = TextRenderingPolicy::PathAlways; + RasterizationPolicy _rasterizationPolicy = RasterizationPolicy::Consistent; }; } // namespace QtNodes diff --git a/src/DefaultConnectionPainter.cpp b/src/DefaultConnectionPainter.cpp index f44e1fd6..c7fd5d60 100644 --- a/src/DefaultConnectionPainter.cpp +++ b/src/DefaultConnectionPainter.cpp @@ -171,9 +171,11 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, QIcon icon(":convert.png"); QPixmap pixmap = icon.pixmap(QSize(22, 22)); - painter->drawPixmap(cubic.pointAtPercent(0.50) - - QPoint(pixmap.width() / 2, pixmap.height() / 2), - pixmap); + QRectF const targetRect(cubic.pointAtPercent(0.50).x() - pixmap.width() / 2.0, + cubic.pointAtPercent(0.50).y() - pixmap.height() / 2.0, + pixmap.width(), + pixmap.height()); + painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); } } else { p.setColor(normalColorOut); diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 1d90ae5c..225cb0a9 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -6,17 +6,104 @@ #include "ConnectionGraphicsObject.hpp" #include "ConnectionIdUtils.hpp" #include "DataFlowGraphModel.hpp" +#include "GraphicsView.hpp" #include "NodeDelegateModel.hpp" #include "NodeGraphicsObject.hpp" #include "NodeState.hpp" #include "StyleCollection.hpp" #include +#include #include namespace QtNodes { +namespace { + +GraphicsView *graphics_view(NodeGraphicsObject &ngo) +{ + if (!ngo.scene()) { + return nullptr; + } + + QList const views = ngo.scene()->views(); + for (QGraphicsView *view : views) { + if (auto *graphicsView = qobject_cast(view)) { + return graphicsView; + } + } + + return nullptr; +} + +bool is_zoom_animating(NodeGraphicsObject &ngo) +{ + if (auto *view = graphics_view(ngo)) { + return view->isZoomAnimating(); + } + return false; +} + +bool should_draw_text_as_path(NodeGraphicsObject &ngo) +{ + auto *view = graphics_view(ngo); + if (!view) { + return false; + } + + switch (view->textRenderingPolicy()) { + case GraphicsView::TextRenderingPolicy::QtText: + return false; + case GraphicsView::TextRenderingPolicy::PathWhenZooming: + return view->isZoomAnimating(); + case GraphicsView::TextRenderingPolicy::PathAlways: + return true; + } + + return false; +} + +void configure_text_painter(QPainter *painter, NodeGraphicsObject &ngo) +{ + painter->setRenderHint(QPainter::TextAntialiasing, true); + + if (should_draw_text_as_path(ngo)) { + return; + } + + if (!is_zoom_animating(ngo)) { + return; + } + + QFont font = painter->font(); + font.setHintingPreference(QFont::PreferNoHinting); + painter->setFont(font); +} + +void draw_text( + QPainter *painter, + NodeGraphicsObject &ngo, + QPointF const &position, + QString const &text, + QColor const &color, + QFont const &font) +{ + if (should_draw_text_as_path(ngo)) { + QPainterPath path; + path.addText(position, font, text); + painter->setPen(Qt::NoPen); + painter->fillPath(path, color); + return; + } + + painter->setFont(font); + painter->setPen(color); + painter->drawText(position, text); +} + +} // namespace + void DefaultNodePainter::paint(QPainter *painter, NodeGraphicsObject &ngo) const { // TODO? @@ -230,15 +317,20 @@ void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject & QFont f = painter->font(); f.setBold(true); + if (!should_draw_text_as_path(ngo) && is_zoom_animating(ngo)) { + f.setHintingPreference(QFont::PreferNoHinting); + } + else { + f.setHintingPreference(QFont::PreferDefaultHinting); + } QPointF position = geometry.captionPosition(nodeId); QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); NodeStyle nodeStyle(json.object()); - painter->setFont(f); - painter->setPen(nodeStyle.FontColor); - painter->drawText(position, name); + painter->setRenderHint(QPainter::TextAntialiasing, true); + draw_text(painter, ngo, position, name, nodeStyle.FontColor, f); f.setBold(false); painter->setFont(f); @@ -246,6 +338,8 @@ void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject & void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const { + configure_text_painter(painter, ngo); + AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); @@ -279,7 +373,9 @@ void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject & s = portData.value().name; } - painter->drawText(p, s); + QColor const textColor = connected.empty() ? nodeStyle.FontColorFaded + : nodeStyle.FontColor; + draw_text(painter, ngo, p, s, textColor, painter->font()); } } } @@ -339,8 +435,8 @@ void DefaultNodePainter::drawProcessingIndicator(QPainter *painter, NodeGraphics x = size.width() - iconSize - margin; } - QRect r(x, size.height() - iconSize - margin, iconSize, iconSize); - painter->drawPixmap(r, pixmap); + QRectF const targetRect(x, size.height() - iconSize - margin, iconSize, iconSize); + painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); } void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const @@ -377,8 +473,11 @@ void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObjec QPointF center(size.width(), 0.0); center += QPointF(iconSize.width() / 2.0, -iconSize.height() / 2.0); - painter->drawPixmap(center.toPoint() - QPoint(iconSize.width() / 2, iconSize.height() / 2), - pixmap); + QRectF const targetRect(center.x() - iconSize.width() / 2.0, + center.y() - iconSize.height() / 2.0, + iconSize.width(), + iconSize.height()); + painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); } } // namespace QtNodes diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 8e85e254..7118f101 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -36,6 +36,30 @@ constexpr double zoom_max_velocity = 5.0; constexpr double zoom_per_notch = 1.05; constexpr int zoom_timer_interval_ms = 16; constexpr double zoom_velocity_epsilon = 0.001; + +QPointF map_to_scene_exact(QGraphicsView const &view, QPointF const &viewPoint) +{ + bool invertible = false; + QTransform const invertedTransform = view.viewportTransform().inverted(&invertible); + if (!invertible) { + return view.mapToScene(viewPoint.toPoint()); + } + + return invertedTransform.map(viewPoint); +} + +void set_node_cache_mode(QGraphicsScene *scene, QGraphicsItem::CacheMode mode) +{ + if (!scene) { + return; + } + + for (QGraphicsItem *item : scene->items()) { + if (qgraphicsitem_cast(item)) { + item->setCacheMode(mode); + } + } +} } // namespace using QtNodes::BasicGraphicsScene; @@ -74,6 +98,8 @@ GraphicsView::GraphicsView(QWidget *parent) // re-calculation when expanding the all QGraphicsItems common rect. int maxSize = 32767; setSceneRect(-maxSize, -maxSize, (maxSize * 2), (maxSize * 2)); + + apply_rasterization_policy(); } GraphicsView::GraphicsView(BasicGraphicsScene *scene, QWidget *parent) @@ -110,6 +136,8 @@ void GraphicsView::setScene(BasicGraphicsScene *scene) return; } + apply_rasterization_policy(); + { // setup actions delete _clearSelectionAction; @@ -263,16 +291,13 @@ void GraphicsView::wheelEvent(QWheelEvent *event) -zoom_max_velocity, zoom_max_velocity); _zoomPivot = event->position(); - if (_zoomTimerId == 0) { - if (scene()) { - for (QGraphicsItem *item : scene()->items()) { - if (qgraphicsitem_cast(item)) { - item->setCacheMode(QGraphicsItem::NoCache); - } - } - } + applyZoomStep(); + + if (_zoomTimerId == 0 && std::abs(_zoomVelocity) >= zoom_velocity_epsilon) { _zoomTimerId = startTimer(zoom_timer_interval_ms); } + + event->accept(); } void GraphicsView::timerEvent(QTimerEvent *event) @@ -314,31 +339,29 @@ void GraphicsView::applyZoomStep() void GraphicsView::applyZoomFactor(double factor) { - QPointF const scenePivot = mapToScene(_zoomPivot.toPoint()); + QPointF const scenePivot = map_to_scene_exact(*this, _zoomPivot); double const newScale = transform().m11() * factor; - // Compute total offset needed so scenePivot appears at _zoomPivot. - // Mapping: widgetPos = transform * scenePos - scrollOffset - // We need: _zoomPivot = newScale * scenePivot + (tx,ty) - (hbar,vbar) - // Split into integer scrollbar values and sub-pixel transform translation - // to avoid the whole-pixel jumps that integer scrollbars cause. - QPointF const fullOffset(newScale * scenePivot.x() - _zoomPivot.x(), - newScale * scenePivot.y() - _zoomPivot.y()); + auto const savedAnchor = transformationAnchor(); + setTransformationAnchor(QGraphicsView::NoAnchor); - int const hval = qRound(fullOffset.x()); - int const vval = qRound(fullOffset.y()); - double const tx = hval - fullOffset.x(); - double const ty = vval - fullOffset.y(); + QTransform scaledTransform; + scaledTransform.scale(newScale, newScale); + setTransform(scaledTransform, false); - QTransform t; - t.translate(tx, ty); - t.scale(newScale, newScale); + QPointF const pivotAfterScale = viewportTransform().map(scenePivot); + QPointF const shift = pivotAfterScale - _zoomPivot; + horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(shift.x())); + verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(shift.y())); + + QPointF const pivotAfterScroll = viewportTransform().map(scenePivot); + QPointF const residual = _zoomPivot - pivotAfterScroll; + + QTransform preciseTransform; + preciseTransform.translate(residual.x(), residual.y()); + preciseTransform.scale(newScale, newScale); + setTransform(preciseTransform, false); - auto const savedAnchor = transformationAnchor(); - setTransformationAnchor(QGraphicsView::NoAnchor); - setTransform(t, false); - horizontalScrollBar()->setValue(hval); - verticalScrollBar()->setValue(vval); setTransformationAnchor(savedAnchor); Q_EMIT scaleChanged(newScale); @@ -346,25 +369,26 @@ void GraphicsView::applyZoomFactor(double factor) void GraphicsView::stopZoomTimer() { + bool const hadFractionalOffset = std::abs(transform().dx()) > 1e-6 || std::abs(transform().dy()) > 1e-6; + if (_zoomTimerId != 0) { killTimer(_zoomTimerId); _zoomTimerId = 0; + } - // Remove sub-pixel translation from transform so normal interaction - // (hit testing, panning) uses a clean scale-only transform. + if (hadFractionalOffset && _rasterizationPolicy == RasterizationPolicy::Crisp) { + QPointF const viewOrigin(0.0, 0.0); + QPointF const sceneAtOrigin = map_to_scene_exact(*this, viewOrigin); double const s = transform().m11(); QTransform clean; clean.scale(s, s); setTransform(clean, false); - if (scene()) { - for (QGraphicsItem *item : scene()->items()) { - if (qgraphicsitem_cast(item)) { - item->setCacheMode(QGraphicsItem::DeviceCoordinateCache); - } - } - } + QPointF const originAfterCleanup = viewportTransform().map(sceneAtOrigin); + horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(originAfterCleanup.x())); + verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(originAfterCleanup.y())); } + _zoomVelocity = 0.0; } @@ -373,6 +397,46 @@ double GraphicsView::getScale() const return transform().m11(); } +bool GraphicsView::isZoomAnimating() const +{ + return _zoomTimerId != 0; +} + +GraphicsView::TextRenderingPolicy GraphicsView::textRenderingPolicy() const +{ + return _textRenderingPolicy; +} + +void GraphicsView::setTextRenderingPolicy(TextRenderingPolicy policy) +{ + if (_textRenderingPolicy == policy) { + return; + } + + _textRenderingPolicy = policy; + viewport()->update(); +} + +GraphicsView::RasterizationPolicy GraphicsView::rasterizationPolicy() const +{ + return _rasterizationPolicy; +} + +void GraphicsView::setRasterizationPolicy(RasterizationPolicy policy) +{ + if (_rasterizationPolicy == policy) { + return; + } + + _rasterizationPolicy = policy; + apply_rasterization_policy(); +} + +void GraphicsView::stopZoomAnimation() +{ + stopZoomTimer(); +} + void GraphicsView::setScaleRange(double minimum, double maximum) { if (maximum < minimum) @@ -392,6 +456,8 @@ void GraphicsView::setScaleRange(ScaleRange range) void GraphicsView::scaleUp() { + stopZoomTimer(); + double const step = 1.2; double const factor = std::pow(step, 1.0); @@ -410,6 +476,8 @@ void GraphicsView::scaleUp() void GraphicsView::scaleDown() { + stopZoomTimer(); + double const step = 1.2; double const factor = std::pow(step, -1.0); @@ -428,6 +496,8 @@ void GraphicsView::scaleDown() void GraphicsView::setupScale(double scale) { + stopZoomTimer(); + scale = std::max(_scaleRange.minimum, std::min(_scaleRange.maximum, scale)); if (scale <= 0) @@ -508,6 +578,10 @@ void GraphicsView::keyReleaseEvent(QKeyEvent *event) void GraphicsView::mousePressEvent(QMouseEvent *event) { + if (event->button() != Qt::NoButton) { + stopZoomTimer(); + } + QGraphicsView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { _clickPos = mapToScene(event->pos()); @@ -538,15 +612,18 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) qreal x_offset = 0.0; qreal y_offset = 0.0; - - QTransform const view_transform = transform(); - qreal const scale_x = std::abs(view_transform.m11()); - qreal const scale_y = std::abs(view_transform.m22()); - if (scale_x > 0.0) { - x_offset = 0.5 / scale_x; - } - if (scale_y > 0.0) { - y_offset = 0.5 / scale_y; + bool const crisp_grid = (_rasterizationPolicy == RasterizationPolicy::Crisp); + + if (crisp_grid) { + QTransform const view_transform = transform(); + qreal const scale_x = std::abs(view_transform.m11()); + qreal const scale_y = std::abs(view_transform.m22()); + if (scale_x > 0.0) { + x_offset = 0.5 / scale_x; + } + if (scale_y > 0.0) { + y_offset = 0.5 / scale_y; + } } auto drawGrid = [&](double gridStep) { @@ -578,13 +655,13 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) auto const &flowViewStyle = StyleCollection::flowViewStyle(); QPen pfine(flowViewStyle.FineGridColor, 1.0); - pfine.setCosmetic(true); + pfine.setCosmetic(crisp_grid); painter->setPen(pfine); drawGrid(15); QPen p(flowViewStyle.CoarseGridColor, 1.0); - p.setCosmetic(true); + p.setCosmetic(crisp_grid); painter->setPen(p); drawGrid(150); @@ -615,11 +692,15 @@ QPointF GraphicsView::scenePastePosition() void GraphicsView::zoomFitAll() { + stopZoomTimer(); + fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio); } void GraphicsView::zoomFitSelected() { + stopZoomTimer(); + if (scene()->selectedItems().count() > 0) { QRectF unitedBoundingRect{}; @@ -631,3 +712,33 @@ void GraphicsView::zoomFitSelected() fitInView(unitedBoundingRect, Qt::KeepAspectRatio); } } + +void GraphicsView::apply_rasterization_policy() +{ + if (_rasterizationPolicy == RasterizationPolicy::Consistent) { + setCacheMode(QGraphicsView::CacheNone); + set_node_cache_mode(scene(), QGraphicsItem::NoCache); + } + else { + if (std::abs(transform().dx()) > 1e-6 || std::abs(transform().dy()) > 1e-6) { + QPointF const viewOrigin(0.0, 0.0); + QPointF const sceneAtOrigin = map_to_scene_exact(*this, viewOrigin); + double const s = transform().m11(); + QTransform clean; + clean.scale(s, s); + setTransform(clean, false); + + QPointF const originAfterCleanup = viewportTransform().map(sceneAtOrigin); + horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qRound(originAfterCleanup.x())); + verticalScrollBar()->setValue(verticalScrollBar()->value() + qRound(originAfterCleanup.y())); + } + + setCacheMode(QGraphicsView::CacheBackground); + set_node_cache_mode(scene(), QGraphicsItem::DeviceCoordinateCache); + } + + if (scene()) { + scene()->update(); + } + viewport()->update(); +} diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 61babbb4..742d4567 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -5,6 +5,7 @@ #include "BasicGraphicsScene.hpp" #include "ConnectionGraphicsObject.hpp" #include "ConnectionIdUtils.hpp" +#include "GraphicsView.hpp" #include "NodeConnectionInteraction.hpp" #include "NodeDelegateModel.hpp" #include "NodeGroup.hpp" @@ -19,6 +20,24 @@ namespace QtNodes { +namespace { + +QGraphicsItem::CacheMode initial_cache_mode(BasicGraphicsScene &scene) +{ + QList const views = scene.views(); + for (QGraphicsView *view : views) { + if (auto *graphicsView = qobject_cast(view)) { + return graphicsView->rasterizationPolicy() == GraphicsView::RasterizationPolicy::Consistent + ? QGraphicsItem::NoCache + : QGraphicsItem::DeviceCoordinateCache; + } + } + + return QGraphicsItem::DeviceCoordinateCache; +} + +} // namespace + NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) : _nodeId(nodeId) , _graphModel(scene.graphModel()) @@ -36,7 +55,7 @@ NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) setLockedState(); - setCacheMode(QGraphicsItem::DeviceCoordinateCache); + setCacheMode(initial_cache_mode(scene)); QJsonObject nodeStyleJson = _graphModel.nodeData(_nodeId, NodeRole::Style).toJsonObject(); diff --git a/test/src/TestZoomFeatures.cpp b/test/src/TestZoomFeatures.cpp index 90fd2e3a..9d3282d4 100644 --- a/test/src/TestZoomFeatures.cpp +++ b/test/src/TestZoomFeatures.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include using QtNodes::BasicGraphicsScene; using QtNodes::GraphicsView; @@ -69,6 +71,50 @@ TEST_CASE("GraphicsView scale range", "[zoom]") view.setupScale(10.0); CHECK(view.getScale() <= 4.0); } + + SECTION("Smooth wheel zoom keeps the cursor anchor stable") + { + view.setupScale(1.0); + + QPointF const pivot(321.25, 247.75); + bool invertible = false; + QTransform const invertedTransform = view.viewportTransform().inverted(&invertible); + REQUIRE(invertible); + + QPointF const trackedScenePoint = invertedTransform.map(pivot); + + QWheelEvent wheelEvent(pivot, + view.mapToGlobal(pivot.toPoint()), + QPoint(0, 0), + QPoint(0, 120), + Qt::NoButton, + Qt::NoModifier, + Qt::ScrollPhase::NoScrollPhase, + false); + QApplication::sendEvent(view.viewport(), &wheelEvent); + QTest::qWait(20); + + QPointF const mappedPivot = view.viewportTransform().map(trackedScenePoint); + CHECK(mappedPivot.x() == Approx(pivot.x()).margin(1.0)); + CHECK(mappedPivot.y() == Approx(pivot.y()).margin(1.0)); + } + + SECTION("Smooth wheel zoom settles back to a clean transform") + { + QWheelEvent wheelEvent(QPointF(320.0, 240.0), + view.mapToGlobal(QPoint(320, 240)), + QPoint(0, 0), + QPoint(0, 120), + Qt::NoButton, + Qt::NoModifier, + Qt::ScrollPhase::NoScrollPhase, + false); + QApplication::sendEvent(view.viewport(), &wheelEvent); + QTest::qWait(500); + + CHECK(view.transform().dx() == Approx(0.0).margin(0.01)); + CHECK(view.transform().dy() == Approx(0.0).margin(0.01)); + } } TEST_CASE("scaleChanged signal", "[zoom]") From aa7a00eedb37fc59001fcbc18ffe330a5bf01b19 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Tue, 24 Mar 2026 14:55:53 +0100 Subject: [PATCH 06/26] Optimize node and connection rendering paths --- .../internal/ConnectionGraphicsObject.hpp | 26 +++ .../internal/DefaultConnectionPainter.hpp | 5 + .../QtNodes/internal/DefaultNodePainter.hpp | 14 +- .../QtNodes/internal/GroupGraphicsObject.hpp | 8 + .../QtNodes/internal/NodeDelegateModel.hpp | 5 + .../QtNodes/internal/NodeGraphicsObject.hpp | 6 + include/QtNodes/internal/NodeGroup.hpp | 2 +- resources/DefaultStyle.json | 2 +- src/BasicGraphicsScene.cpp | 10 ++ src/ConnectionGraphicsObject.cpp | 95 +++++++--- src/DefaultConnectionPainter.cpp | 61 ++----- src/DefaultNodePainter.cpp | 164 +++++++++++------- src/GraphicsView.cpp | 85 ++++++++- src/GroupGraphicsObject.cpp | 35 +++- src/NodeDelegateModel.cpp | 44 ++++- src/NodeGraphicsObject.cpp | 36 +++- src/NodeGroup.cpp | 13 +- test/src/TestNodeGroup.cpp | 57 ++++++ test/src/TestZoomFeatures.cpp | 69 ++++++++ 19 files changed, 562 insertions(+), 175 deletions(-) diff --git a/include/QtNodes/internal/ConnectionGraphicsObject.hpp b/include/QtNodes/internal/ConnectionGraphicsObject.hpp index 03c06a03..b1c0b716 100644 --- a/include/QtNodes/internal/ConnectionGraphicsObject.hpp +++ b/include/QtNodes/internal/ConnectionGraphicsObject.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include +#include #include #include "ConnectionState.hpp" @@ -49,6 +51,18 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject std::pair pointsC1C2() const; + /// Cached cubic bezier path, rebuilt only when endpoints change. + QPainterPath const &cachedCubicPath() const; + + /// Cached painter stroke for hit testing, rebuilt only when endpoints change. + QPainterPath const &cachedStrokePath() const; + + int cachedSamplePointCount() const { return k_path_sample_count; } + + QPointF const &cachedSamplePoint(int index) const; + + QPointF const &cachedMidPoint() const; + void setEndPoint(PortType portType, QPointF const &point); /// Updates the position of both ends @@ -83,6 +97,11 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject std::pair pointsC1C2Vertical() const; private: + void rebuildCachedGeometry() const; + +private: + static constexpr int k_path_sample_count = 61; + ConnectionId _connectionId; AbstractGraphModel &_graphModel; @@ -91,6 +110,13 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject mutable QPointF _out; mutable QPointF _in; + + mutable bool _geometryDirty = true; + mutable QRectF _cachedBoundingRect; + mutable QPainterPath _cachedCubicPath; + mutable QPainterPath _cachedStrokePath; + mutable std::array _cachedSamplePoints{}; + mutable QPointF _cachedMidPoint; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultConnectionPainter.hpp b/include/QtNodes/internal/DefaultConnectionPainter.hpp index b2c44d10..f2a0f523 100644 --- a/include/QtNodes/internal/DefaultConnectionPainter.hpp +++ b/include/QtNodes/internal/DefaultConnectionPainter.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include "AbstractConnectionPainter.hpp" #include "Definitions.hpp" @@ -24,6 +26,9 @@ class DefaultConnectionPainter : public AbstractConnectionPainter #ifdef NODE_DEBUG_DRAWING void debugDrawing(QPainter *painter, ConnectionGraphicsObject const &cgo) const; #endif + +private: + QPixmap _convertPixmap{QIcon(QStringLiteral(":/convert.png")).pixmap(QSize(22, 22))}; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultNodePainter.hpp b/include/QtNodes/internal/DefaultNodePainter.hpp index dbea2a3c..40fc6451 100644 --- a/include/QtNodes/internal/DefaultNodePainter.hpp +++ b/include/QtNodes/internal/DefaultNodePainter.hpp @@ -5,10 +5,12 @@ #include "AbstractNodePainter.hpp" #include "Definitions.hpp" +#include "NodeStyle.hpp" namespace QtNodes { class BasicGraphicsScene; +class GraphicsView; class GraphModel; class NodeGeometry; class NodeGraphicsObject; @@ -20,21 +22,21 @@ class NODE_EDITOR_PUBLIC DefaultNodePainter : public AbstractNodePainter public: void paint(QPainter *painter, NodeGraphicsObject &ngo) const override; - void drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; - void drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; - void drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; - void drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style, GraphicsView *view) const; - void drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style, GraphicsView *view) const; void drawResizeRect(QPainter *painter, NodeGraphicsObject &ngo) const; void drawProcessingIndicator(QPainter *painter, NodeGraphicsObject &ngo) const; - void drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const; + void drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &style) const; private: QIcon _toolTipIcon{":/info-tooltip.svg"}; diff --git a/include/QtNodes/internal/GroupGraphicsObject.hpp b/include/QtNodes/internal/GroupGraphicsObject.hpp index 1b635fbd..57760ee5 100644 --- a/include/QtNodes/internal/GroupGraphicsObject.hpp +++ b/include/QtNodes/internal/GroupGraphicsObject.hpp @@ -107,6 +107,11 @@ class GroupGraphicsObject */ void moveConnections(); + /// Recomputes the group rect and icon positions. Call when child + /// nodes move or the possible-child changes instead of deferring + /// geometry work to paint(). + void updateGroupGeometry(); + /** * @brief Moves the position of all the nodes of this group by the amount given. * @param offset 2D vector representing the amount by which the group has moved. @@ -210,6 +215,9 @@ class GroupGraphicsObject */ QPen _borderPen; +private: + QRectF compute_group_rect() const; + private: /** * @brief _scene Reference to the scene object in which this object is included. diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index 0ba70911..bfb1baf2 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -196,6 +196,11 @@ public Q_SLOTS: NodeValidationState _nodeValidationState; NodeProcessingStatus _processingStatus{NodeProcessingStatus::NoStatus}; + + mutable bool _processingStatusIconDirty{true}; + mutable NodeProcessingStatus _cachedProcessingStatus{NodeProcessingStatus::NoStatus}; + mutable int _cachedProcessingStatusResolution{0}; + mutable QPixmap _cachedProcessingStatusIcon; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeGraphicsObject.hpp b/include/QtNodes/internal/NodeGraphicsObject.hpp index e3226802..cade1bcc 100644 --- a/include/QtNodes/internal/NodeGraphicsObject.hpp +++ b/include/QtNodes/internal/NodeGraphicsObject.hpp @@ -17,6 +17,7 @@ namespace QtNodes { class BasicGraphicsScene; class AbstractGraphModel; +class GraphicsView; class NodeGroup; class NodeDelegateModel; class GroupGraphicsObject; @@ -48,10 +49,14 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject NodeState const &nodeState() const { return _nodeState; } + GraphicsView *currentGraphicsView() const { return _currentGraphicsView; } + QRectF boundingRect() const override; void setGeometryChanged(); + void updateValidationTooltip(); + /// Visits all attached connections and corrects /// their corresponding end points. void moveConnections() const; @@ -115,5 +120,6 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject QGraphicsProxyWidget *_proxyWidget; std::weak_ptr _nodeGroup{}; + GraphicsView *_currentGraphicsView = nullptr; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeGroup.hpp b/include/QtNodes/internal/NodeGroup.hpp index f8accc0a..f9f6c5ab 100644 --- a/include/QtNodes/internal/NodeGroup.hpp +++ b/include/QtNodes/internal/NodeGroup.hpp @@ -121,7 +121,7 @@ public Q_SLOTS: * @brief Identifier of this group. It is the only unique identifier of * the group. */ - GroupId _id; + GroupId _groupId; // data /** diff --git a/resources/DefaultStyle.json b/resources/DefaultStyle.json index 2df69bfd..e793d3b6 100644 --- a/resources/DefaultStyle.json +++ b/resources/DefaultStyle.json @@ -12,7 +12,7 @@ "GradientColor2": [64, 64, 64], "GradientColor3": [58, 58, 58], "ShadowColor": [20, 20, 20], - "ShadowEnabled": true, + "ShadowEnabled": false, "FontColor" : "white", "FontColorFaded" : "gray", "ConnectionPointColor": [169, 169, 169], diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index adf92827..724baa52 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -309,6 +309,7 @@ void BasicGraphicsScene::traverseGraphAndPopulateGraphicsObjects() // First create all the nodes. for (NodeId const nodeId : allNodeIds) { _nodeGraphicsObjects[nodeId] = std::make_unique(*this, nodeId); + _nodeGraphicsObjects[nodeId]->updateValidationTooltip(); } // Then for each node check output connections and insert them. @@ -332,6 +333,7 @@ void BasicGraphicsScene::updateAttachedNodes(ConnectionId const connectionId, auto node = nodeGraphicsObject(getNodeId(portType, connectionId)); if (node) { + node->updateValidationTooltip(); node->update(); } } @@ -379,6 +381,7 @@ void BasicGraphicsScene::onNodeDeleted(NodeId const nodeId) void BasicGraphicsScene::onNodeCreated(NodeId const nodeId) { _nodeGraphicsObjects[nodeId] = std::make_unique(*this, nodeId); + _nodeGraphicsObjects[nodeId]->updateValidationTooltip(); Q_EMIT modified(this); } @@ -388,6 +391,9 @@ void BasicGraphicsScene::onNodePositionUpdated(NodeId const nodeId) auto node = nodeGraphicsObject(nodeId); if (node) { node->setPos(_graphModel.nodeData(nodeId, NodeRole::Position).value()); + if (auto group = node->nodeGroup().lock()) { + group->groupGraphicsObject().updateGroupGeometry(); + } node->update(); _nodeDrag = true; } @@ -402,6 +408,10 @@ void BasicGraphicsScene::onNodeUpdated(NodeId const nodeId) _nodeGeometry->recomputeSize(nodeId); + node->updateValidationTooltip(); + if (auto group = node->nodeGroup().lock()) { + group->groupGraphicsObject().updateGroupGeometry(); + } node->updateQWidgetEmbedPos(); node->update(); node->moveConnections(); diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp index 05ae46b3..a840e474 100644 --- a/src/ConnectionGraphicsObject.cpp +++ b/src/ConnectionGraphicsObject.cpp @@ -12,6 +12,8 @@ #include "StyleCollection.hpp" #include "locateNode.hpp" +#include +#include #include #include #include @@ -96,40 +98,81 @@ ConnectionId const &ConnectionGraphicsObject::connectionId() const return _connectionId; } -QRectF ConnectionGraphicsObject::boundingRect() const +void ConnectionGraphicsObject::rebuildCachedGeometry() const { - auto points = pointsC1C2(); + if (!_geometryDirty) { + return; + } + _geometryDirty = false; - // `normalized()` fixes inverted rects. - QRectF basicRect = QRectF(_out, _in).normalized(); + // Cubic path + auto const c1c2 = pointsC1C2(); + _cachedCubicPath = QPainterPath(_out); + _cachedCubicPath.cubicTo(c1c2.first, c1c2.second, _in); - QRectF c1c2Rect = QRectF(points.first, points.second).normalized(); + for (int i = 0; i < k_path_sample_count; ++i) { + double const ratio = double(i) / (k_path_sample_count - 1); + _cachedSamplePoints[i] = _cachedCubicPath.pointAtPercent(ratio); + } + _cachedMidPoint = _cachedSamplePoints[k_path_sample_count / 2]; + // Bounding rect + QRectF basicRect = QRectF(_out, _in).normalized(); + QRectF c1c2Rect = QRectF(c1c2.first, c1c2.second).normalized(); QRectF commonRect = basicRect.united(c1c2Rect); auto const &connectionStyle = StyleCollection::connectionStyle(); float const diam = connectionStyle.pointDiameter(); QPointF const cornerOffset(diam, diam); - - // Expand rect by port circle diameter commonRect.setTopLeft(commonRect.topLeft() - cornerOffset); commonRect.setBottomRight(commonRect.bottomRight() + 2 * cornerOffset); + _cachedBoundingRect = commonRect; - return commonRect; + // Stroke path for hit testing + QPainterPath linearized(_out); + for (int i = 1; i < k_path_sample_count; ++i) { + linearized.lineTo(_cachedSamplePoints[i]); + } + QPainterPathStroker stroker; + stroker.setWidth(10.0); + _cachedStrokePath = stroker.createStroke(linearized); +} + +QRectF ConnectionGraphicsObject::boundingRect() const +{ + rebuildCachedGeometry(); + return _cachedBoundingRect; } QPainterPath ConnectionGraphicsObject::shape() const { -#ifdef DEBUG_DRAWING + rebuildCachedGeometry(); + return _cachedStrokePath; +} - //QPainterPath path; +QPainterPath const &ConnectionGraphicsObject::cachedCubicPath() const +{ + rebuildCachedGeometry(); + return _cachedCubicPath; +} - //path.addRect(boundingRect()); - //return path; +QPainterPath const &ConnectionGraphicsObject::cachedStrokePath() const +{ + rebuildCachedGeometry(); + return _cachedStrokePath; +} -#else - return nodeScene()->connectionPainter().getPainterStroke(*this); -#endif +QPointF const &ConnectionGraphicsObject::cachedSamplePoint(int index) const +{ + rebuildCachedGeometry(); + Q_ASSERT(index >= 0 && index < k_path_sample_count); + return _cachedSamplePoints[index]; +} + +QPointF const &ConnectionGraphicsObject::cachedMidPoint() const +{ + rebuildCachedGeometry(); + return _cachedMidPoint; } QPointF const &ConnectionGraphicsObject::endPoint(PortType portType) const @@ -145,11 +188,16 @@ void ConnectionGraphicsObject::setEndPoint(PortType portType, QPointF const &poi _in = point; else _out = point; + + _geometryDirty = true; } void ConnectionGraphicsObject::move() { - auto moveEnd = [this](ConnectionId cId, PortType portType) { + QPointF newOut = _out; + QPointF newIn = _in; + + auto moveEnd = [this](ConnectionId cId, PortType portType, QPointF &endPoint) { NodeId nodeId = getNodeId(portType, cId); if (nodeId == InvalidNodeId) @@ -165,16 +213,21 @@ void ConnectionGraphicsObject::move() getPortIndex(portType, cId), ngo->sceneTransform()); - QPointF connectionPos = sceneTransform().inverted().map(scenePos); - - setEndPoint(portType, connectionPos); + endPoint = sceneTransform().inverted().map(scenePos); } }; - moveEnd(_connectionId, PortType::Out); - moveEnd(_connectionId, PortType::In); + moveEnd(_connectionId, PortType::Out, newOut); + moveEnd(_connectionId, PortType::In, newIn); + + if (newOut == _out && newIn == _in) { + return; + } prepareGeometryChange(); + _out = newOut; + _in = newIn; + _geometryDirty = true; update(); } diff --git a/src/DefaultConnectionPainter.cpp b/src/DefaultConnectionPainter.cpp index c7fd5d60..8dbec19c 100644 --- a/src/DefaultConnectionPainter.cpp +++ b/src/DefaultConnectionPainter.cpp @@ -7,8 +7,6 @@ #include "NodeData.hpp" #include "StyleCollection.hpp" -#include - namespace QtNodes { namespace { @@ -27,17 +25,7 @@ QPen make_connection_pen(QColor const &color, qreal width, Qt::PenStyle style = QPainterPath DefaultConnectionPainter::cubicPath(ConnectionGraphicsObject const &connection) const { - QPointF const &in = connection.endPoint(PortType::In); - QPointF const &out = connection.endPoint(PortType::Out); - - auto const c1c2 = connection.pointsC1C2(); - - // cubic spline - QPainterPath cubic(out); - - cubic.cubicTo(c1c2.first, c1c2.second, in); - - return cubic; + return connection.cachedCubicPath(); } void DefaultConnectionPainter::drawSketchLine(QPainter *painter, @@ -139,8 +127,8 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, bool const selected = cgo.isSelected(); - auto cubic = cubicPath(cgo); - if (useGradientColor) { + auto const cubic = cubicPath(cgo); + if (useGradientColor) { painter->setBrush(Qt::NoBrush); QColor cOut = normalColorOut; @@ -150,13 +138,8 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, p.setColor(cOut); painter->setPen(p); - unsigned int constexpr segments = 60; - - for (unsigned int i = 0ul; i < segments; ++i) { - double ratioPrev = double(i) / segments; - double ratio = double(i + 1) / segments; - - if (i == segments / 2) { + for (int i = 0; i < cgo.cachedSamplePointCount() - 1; ++i) { + if (i == (cgo.cachedSamplePointCount() - 1) / 2) { QColor cIn = normalColorIn; if (selected) cIn = cIn.darker(200); @@ -164,19 +147,14 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, p.setColor(cIn); painter->setPen(p); } - painter->drawLine(cubic.pointAtPercent(ratioPrev), cubic.pointAtPercent(ratio)); + painter->drawLine(cgo.cachedSamplePoint(i), cgo.cachedSamplePoint(i + 1)); } - { - QIcon icon(":convert.png"); - - QPixmap pixmap = icon.pixmap(QSize(22, 22)); - QRectF const targetRect(cubic.pointAtPercent(0.50).x() - pixmap.width() / 2.0, - cubic.pointAtPercent(0.50).y() - pixmap.height() / 2.0, - pixmap.width(), - pixmap.height()); - painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); - } + QRectF const targetRect(cgo.cachedMidPoint().x() - _convertPixmap.width() / 2.0, + cgo.cachedMidPoint().y() - _convertPixmap.height() / 2.0, + _convertPixmap.width(), + _convertPixmap.height()); + painter->drawPixmap(targetRect, _convertPixmap, QRectF(_convertPixmap.rect())); } else { p.setColor(normalColorOut); @@ -220,22 +198,7 @@ void DefaultConnectionPainter::paint(QPainter *painter, ConnectionGraphicsObject QPainterPath DefaultConnectionPainter::getPainterStroke( ConnectionGraphicsObject const &connection) const { - auto cubic = cubicPath(connection); - - QPointF const &out = connection.endPoint(PortType::Out); - QPainterPath result(out); - - unsigned int constexpr segments = 20; - - for (auto i = 0ul; i < segments; ++i) { - double ratio = double(i + 1) / segments; - result.lineTo(cubic.pointAtPercent(ratio)); - } - - QPainterPathStroker stroker; - stroker.setWidth(10.0); - - return stroker.createStroke(result); + return connection.cachedStrokePath(); } #ifdef NODE_DEBUG_DRAWING diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 225cb0a9..d8fdcdfa 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -12,6 +12,7 @@ #include "NodeState.hpp" #include "StyleCollection.hpp" +#include #include #include @@ -23,6 +24,10 @@ namespace { GraphicsView *graphics_view(NodeGraphicsObject &ngo) { + if (auto *view = ngo.currentGraphicsView()) { + return view; + } + if (!ngo.scene()) { return nullptr; } @@ -37,17 +42,8 @@ GraphicsView *graphics_view(NodeGraphicsObject &ngo) return nullptr; } -bool is_zoom_animating(NodeGraphicsObject &ngo) +bool should_draw_text_as_path(GraphicsView *view) { - if (auto *view = graphics_view(ngo)) { - return view->isZoomAnimating(); - } - return false; -} - -bool should_draw_text_as_path(NodeGraphicsObject &ngo) -{ - auto *view = graphics_view(ngo); if (!view) { return false; } @@ -64,15 +60,15 @@ bool should_draw_text_as_path(NodeGraphicsObject &ngo) return false; } -void configure_text_painter(QPainter *painter, NodeGraphicsObject &ngo) +void configure_text_painter(QPainter *painter, GraphicsView *view) { painter->setRenderHint(QPainter::TextAntialiasing, true); - if (should_draw_text_as_path(ngo)) { + if (should_draw_text_as_path(view)) { return; } - if (!is_zoom_animating(ngo)) { + if (!view || !view->isZoomAnimating()) { return; } @@ -81,19 +77,59 @@ void configure_text_painter(QPainter *painter, NodeGraphicsObject &ngo) painter->setFont(font); } +// Paths are cached at the origin and the painter is translated to the +// draw position. The cache key combines QFont::key() (which encodes +// family, size, weight, style, hinting, etc.) with the text string. +// For typical node scenes the cache holds ~15 entries and never evicts. +QHash s_text_path_cache; +QHash s_validation_icon_cache; + +QPixmap validation_icon(QIcon const &icon, QColor const &color) +{ + auto it = s_validation_icon_cache.constFind(color.rgba()); + if (it != s_validation_icon_cache.constEnd()) { + return *it; + } + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + QPainter imgPainter(&pixmap); + imgPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + imgPainter.fillRect(pixmap.rect(), color); + imgPainter.end(); + + return s_validation_icon_cache.insert(color.rgba(), std::move(pixmap)).value(); +} + void draw_text( QPainter *painter, - NodeGraphicsObject &ngo, + GraphicsView *view, QPointF const &position, QString const &text, QColor const &color, QFont const &font) { - if (should_draw_text_as_path(ngo)) { - QPainterPath path; - path.addText(position, font, text); + if (should_draw_text_as_path(view)) { + QString const key = font.key() + text; + + auto it = s_text_path_cache.constFind(key); + if (it == s_text_path_cache.constEnd()) { + QPainterPath path; + path.addText(QPointF(0, 0), font, text); + it = s_text_path_cache.insert(key, std::move(path)); + + // Prevent unbounded growth for highly dynamic scenes. + if (s_text_path_cache.size() > 500) { + QPainterPath keep = *it; + s_text_path_cache.clear(); + it = s_text_path_cache.insert(key, std::move(keep)); + } + } + painter->setPen(Qt::NoPen); - painter->fillPath(path, color); + painter->translate(position); + painter->fillPath(*it, color); + painter->translate(-position); return; } @@ -106,28 +142,47 @@ void draw_text( void DefaultNodePainter::paint(QPainter *painter, NodeGraphicsObject &ngo) const { - // TODO? - //AbstractNodeGeometry & geometry = ngo.nodeScene()->nodeGeometry(); - //geometry.recomputeSizeIfFontChanged(painter->font()); + AbstractGraphModel &model = ngo.graphModel(); + NodeId const nodeId = ngo.nodeId(); + GraphicsView *view = graphics_view(ngo); + + // Fast path: get NodeStyle directly from the delegate model, avoiding + // the NodeStyle -> JSON -> QVariant -> JSON -> NodeStyle round-trip. + NodeStyle const *stylePtr = nullptr; + if (auto *dfModel = dynamic_cast(&model)) { + if (auto *delegate = dfModel->delegateModel(nodeId)) { + stylePtr = &delegate->nodeStyle(); + } + } + // Fallback: only constructed when the fast path above cannot resolve the + // style. The default NodeStyle() constructor is expensive (loads SVG + // icons and parses JSON from resources), so it must not run on every paint. + NodeStyle fallbackStorage(QJsonObject{}); + if (!stylePtr) { + QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); + fallbackStorage = NodeStyle(json.object()); + stylePtr = &fallbackStorage; + } + NodeStyle const &style = *stylePtr; - drawNodeRect(painter, ngo); + drawNodeRect(painter, ngo, style); - drawConnectionPoints(painter, ngo); + drawConnectionPoints(painter, ngo, style); - drawFilledConnectionPoints(painter, ngo); + drawFilledConnectionPoints(painter, ngo, style); - drawNodeCaption(painter, ngo); + drawNodeCaption(painter, ngo, style, view); - drawEntryLabels(painter, ngo); + drawEntryLabels(painter, ngo, style, view); drawProcessingIndicator(painter, ngo); drawResizeRect(painter, ngo); - drawValidationIcon(painter, ngo); + drawValidationIcon(painter, ngo, style); } -void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); @@ -137,10 +192,6 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo QSize size = geometry.size(nodeId); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - - NodeStyle nodeStyle(json.object()); - QVariant var = model.nodeData(nodeId, NodeRole::ValidationState); bool invalid = false; @@ -167,14 +218,16 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo if (ngo.nodeState().hovered()) { QPen p(color, nodeStyle.HoveredPenWidth); painter->setPen(p); - } else { + } + else { QPen p(color, nodeStyle.PenWidth); painter->setPen(p); } if (invalid) { painter->setBrush(color); - } else { + } + else { QLinearGradient gradient(QPointF(0.0, 0.0), QPointF(2.0, size.height())); gradient.setColorAt(0.0, nodeStyle.GradientColor0); gradient.setColorAt(0.10, nodeStyle.GradientColor1); @@ -190,15 +243,12 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo painter->drawRoundedRect(boundary, radius, radius); } -void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - auto const &connectionStyle = StyleCollection::connectionStyle(); float diameter = nodeStyle.ConnectionPointDiameter; @@ -260,15 +310,12 @@ void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObj } } -void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - auto diameter = nodeStyle.ConnectionPointDiameter; for (PortType portType : {PortType::Out, PortType::In}) { @@ -304,7 +351,7 @@ void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraph } } -void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle, GraphicsView *view) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); @@ -317,7 +364,7 @@ void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject & QFont f = painter->font(); f.setBold(true); - if (!should_draw_text_as_path(ngo) && is_zoom_animating(ngo)) { + if (!should_draw_text_as_path(view) && view && view->isZoomAnimating()) { f.setHintingPreference(QFont::PreferNoHinting); } else { @@ -326,27 +373,21 @@ void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject & QPointF position = geometry.captionPosition(nodeId); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - painter->setRenderHint(QPainter::TextAntialiasing, true); - draw_text(painter, ngo, position, name, nodeStyle.FontColor, f); + draw_text(painter, view, position, name, nodeStyle.FontColor, f); f.setBold(false); painter->setFont(f); } -void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle, GraphicsView *view) const { - configure_text_painter(painter, ngo); + configure_text_painter(painter, view); AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - for (PortType portType : {PortType::Out, PortType::In}) { unsigned int n = model.nodeData(nodeId, (portType == PortType::Out) @@ -375,7 +416,7 @@ void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject & QColor const textColor = connected.empty() ? nodeStyle.FontColorFaded : nodeStyle.FontColor; - draw_text(painter, ngo, p, s, textColor, painter->font()); + draw_text(painter, view, p, s, textColor, painter->font()); } } } @@ -439,7 +480,7 @@ void DefaultNodePainter::drawProcessingIndicator(QPainter *painter, NodeGraphics painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); } -void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const +void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const { AbstractGraphModel &model = ngo.graphModel(); NodeId const nodeId = ngo.nodeId(); @@ -453,22 +494,13 @@ void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObjec if (state._state == NodeValidationState::State::Valid) return; - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - NodeStyle nodeStyle(json.object()); - QSize size = geometry.size(nodeId); - QIcon icon(":/info-tooltip.svg"); - QSize iconSize(16, 16); - QPixmap pixmap = icon.pixmap(iconSize); + QSize const iconSize(16, 16); QColor color = (state._state == NodeValidationState::State::Error) ? nodeStyle.ErrorColor : nodeStyle.WarningColor; - - QPainter imgPainter(&pixmap); - imgPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); - imgPainter.fillRect(pixmap.rect(), color); - imgPainter.end(); + QPixmap const pixmap = validation_icon(_toolTipIcon, color); QPointF center(size.width(), 0.0); center += QPointF(iconSize.width() / 2.0, -iconSize.height() / 2.0); diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 7118f101..20ca7092 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -48,7 +48,50 @@ QPointF map_to_scene_exact(QGraphicsView const &view, QPointF const &viewPoint) return invertedTransform.map(viewPoint); } -void set_node_cache_mode(QGraphicsScene *scene, QGraphicsItem::CacheMode mode) +QList graphics_views(QGraphicsScene *scene) +{ + QList result; + if (!scene) { + return result; + } + + QList const views = scene->views(); + for (QGraphicsView *view : views) { + if (auto *graphicsView = qobject_cast(view)) { + result.push_back(graphicsView); + } + } + + return result; +} + +QGraphicsItem::CacheMode effective_node_cache_mode(QGraphicsScene *scene) +{ + QList const views = graphics_views(scene); + if (views.empty()) { + return QGraphicsItem::DeviceCoordinateCache; + } + + // Item cache mode is shared by all attached views. Mixed-view scenes keep node + // rendering uncached so one view cannot force another into an incompatible mode. + if (views.size() > 1) { + return QGraphicsItem::NoCache; + } + + QtNodes::GraphicsView const &view = *views.front(); + if (view.isZoomAnimating()) { + return QGraphicsItem::NoCache; + } + + return view.rasterizationPolicy() == QtNodes::GraphicsView::RasterizationPolicy::Consistent + ? QGraphicsItem::NoCache + : QGraphicsItem::DeviceCoordinateCache; +} + +void set_node_cache_mode( + QGraphicsScene *scene, + QGraphicsItem::CacheMode mode, + bool invalidate_cached_content = false) { if (!scene) { return; @@ -56,10 +99,21 @@ void set_node_cache_mode(QGraphicsScene *scene, QGraphicsItem::CacheMode mode) for (QGraphicsItem *item : scene->items()) { if (qgraphicsitem_cast(item)) { + if (invalidate_cached_content && mode != QGraphicsItem::NoCache) { + item->setCacheMode(QGraphicsItem::NoCache); + } item->setCacheMode(mode); + item->update(); } } } + +void refresh_node_cache_mode(QGraphicsScene *scene, bool invalidate_cached_content = false) +{ + set_node_cache_mode(scene, + effective_node_cache_mode(scene), + invalidate_cached_content); +} } // namespace using QtNodes::BasicGraphicsScene; @@ -120,7 +174,14 @@ QAction *GraphicsView::deleteSelectionAction() const void GraphicsView::setScene(BasicGraphicsScene *scene) { + QGraphicsScene *oldScene = this->scene(); + QGraphicsView::setScene(scene); + if (oldScene && oldScene != scene) { + refresh_node_cache_mode(oldScene, true); + oldScene->update(); + } + if (!scene) { // Clear actions. delete _clearSelectionAction; @@ -291,12 +352,13 @@ void GraphicsView::wheelEvent(QWheelEvent *event) -zoom_max_velocity, zoom_max_velocity); _zoomPivot = event->position(); - applyZoomStep(); - if (_zoomTimerId == 0 && std::abs(_zoomVelocity) >= zoom_velocity_epsilon) { _zoomTimerId = startTimer(zoom_timer_interval_ms); + refresh_node_cache_mode(scene(), true); } + applyZoomStep(); + event->accept(); } @@ -370,6 +432,7 @@ void GraphicsView::applyZoomFactor(double factor) void GraphicsView::stopZoomTimer() { bool const hadFractionalOffset = std::abs(transform().dx()) > 1e-6 || std::abs(transform().dy()) > 1e-6; + bool const was_zoom_animating = (_zoomTimerId != 0); if (_zoomTimerId != 0) { killTimer(_zoomTimerId); @@ -390,6 +453,14 @@ void GraphicsView::stopZoomTimer() } _zoomVelocity = 0.0; + + if (was_zoom_animating) { + refresh_node_cache_mode(scene(), true); + if (scene()) { + scene()->update(); + } + viewport()->update(); + } } double GraphicsView::getScale() const @@ -414,6 +485,11 @@ void GraphicsView::setTextRenderingPolicy(TextRenderingPolicy policy) } _textRenderingPolicy = policy; + + refresh_node_cache_mode(scene(), true); + if (scene()) { + scene()->update(); + } viewport()->update(); } @@ -717,7 +793,6 @@ void GraphicsView::apply_rasterization_policy() { if (_rasterizationPolicy == RasterizationPolicy::Consistent) { setCacheMode(QGraphicsView::CacheNone); - set_node_cache_mode(scene(), QGraphicsItem::NoCache); } else { if (std::abs(transform().dx()) > 1e-6 || std::abs(transform().dy()) > 1e-6) { @@ -734,9 +809,9 @@ void GraphicsView::apply_rasterization_policy() } setCacheMode(QGraphicsView::CacheBackground); - set_node_cache_mode(scene(), QGraphicsItem::DeviceCoordinateCache); } + refresh_node_cache_mode(scene(), true); if (scene()) { scene()->update(); } diff --git a/src/GroupGraphicsObject.cpp b/src/GroupGraphicsObject.cpp index f9c995fb..4cbae9b1 100644 --- a/src/GroupGraphicsObject.cpp +++ b/src/GroupGraphicsObject.cpp @@ -57,6 +57,8 @@ GroupGraphicsObject::GroupGraphicsObject(BasicGraphicsScene &scene, NodeGroup &n setZValue(-_groupAreaZValue); setAcceptHoverEvents(true); + + updateGroupGeometry(); } GroupGraphicsObject::~GroupGraphicsObject() @@ -75,6 +77,11 @@ NodeGroup const &GroupGraphicsObject::group() const } QRectF GroupGraphicsObject::boundingRect() const +{ + return QGraphicsRectItem::boundingRect(); +} + +QRectF GroupGraphicsObject::compute_group_rect() const { QRectF ret{}; for (auto &node : _group.childNodes()) { @@ -112,6 +119,7 @@ void GroupGraphicsObject::moveNodes(const QPointF &offset) node->setPos(newPosition); node->update(); } + updateGroupGeometry(); } void GroupGraphicsObject::lock(bool locked) @@ -133,12 +141,11 @@ bool GroupGraphicsObject::locked() const void GroupGraphicsObject::positionLockedIcon() { - _lockedGraphicsItem->setPos( - boundingRect().topRight() - + QPointF(-(_roundedBorderRadius + IconGraphicsItem::iconSize()), _roundedBorderRadius)); - _unlockedGraphicsItem->setPos( - boundingRect().topRight() - + QPointF(-(_roundedBorderRadius + IconGraphicsItem::iconSize()), _roundedBorderRadius)); + QPointF const icon_pos = rect().topRight() + + QPointF(-(_roundedBorderRadius + IconGraphicsItem::iconSize()), + _roundedBorderRadius); + _lockedGraphicsItem->setPos(icon_pos); + _unlockedGraphicsItem->setPos(icon_pos); } void GroupGraphicsObject::setHovered(bool hovered) @@ -157,11 +164,13 @@ void GroupGraphicsObject::setHovered(bool hovered) void GroupGraphicsObject::setPossibleChild(QtNodes::NodeGraphicsObject *possibleChild) { _possibleChild = possibleChild; + updateGroupGeometry(); } void GroupGraphicsObject::unsetPossibleChild() { _possibleChild = nullptr; + updateGroupGeometry(); } std::vector> GroupGraphicsObject::connections() const @@ -204,14 +213,22 @@ void GroupGraphicsObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) lock(!locked()); } +void GroupGraphicsObject::updateGroupGeometry() +{ + QRectF const newRect = compute_group_rect(); + if (newRect != rect()) { + prepareGeometryChange(); + setRect(newRect); + } + positionLockedIcon(); + update(); +} + void GroupGraphicsObject::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(widget); - prepareGeometryChange(); - setRect(boundingRect()); - positionLockedIcon(); painter->setClipRect(option->exposedRect); painter->setBrush(_currentFillColor); diff --git a/src/NodeDelegateModel.cpp b/src/NodeDelegateModel.cpp index fc8f3e8d..97f9a7af 100644 --- a/src/NodeDelegateModel.cpp +++ b/src/NodeDelegateModel.cpp @@ -54,29 +54,51 @@ NodeStyle const &NodeDelegateModel::nodeStyle() const void NodeDelegateModel::setNodeStyle(NodeStyle const &style) { _nodeStyle = style; + _processingStatusIconDirty = true; } QPixmap NodeDelegateModel::processingStatusIcon() const { - int resolution = _nodeStyle.processingIconStyle._resolution; + int const resolution = _nodeStyle.processingIconStyle._resolution; + + if (_processingStatus == NodeProcessingStatus::NoStatus) { + return {}; + } + + if (!_processingStatusIconDirty && _cachedProcessingStatus == _processingStatus + && _cachedProcessingStatusResolution == resolution) { + return _cachedProcessingStatusIcon; + } + switch (_processingStatus) { case NodeProcessingStatus::NoStatus: - return {}; + _cachedProcessingStatusIcon = {}; + break; case NodeProcessingStatus::Updated: - return _nodeStyle.statusUpdated.pixmap(resolution); + _cachedProcessingStatusIcon = _nodeStyle.statusUpdated.pixmap(resolution); + break; case NodeProcessingStatus::Processing: - return _nodeStyle.statusProcessing.pixmap(resolution); + _cachedProcessingStatusIcon = _nodeStyle.statusProcessing.pixmap(resolution); + break; case NodeProcessingStatus::Pending: - return _nodeStyle.statusPending.pixmap(resolution); + _cachedProcessingStatusIcon = _nodeStyle.statusPending.pixmap(resolution); + break; case NodeProcessingStatus::Empty: - return _nodeStyle.statusEmpty.pixmap(resolution); + _cachedProcessingStatusIcon = _nodeStyle.statusEmpty.pixmap(resolution); + break; case NodeProcessingStatus::Failed: - return _nodeStyle.statusInvalid.pixmap(resolution); + _cachedProcessingStatusIcon = _nodeStyle.statusInvalid.pixmap(resolution); + break; case NodeProcessingStatus::Partial: - return _nodeStyle.statusPartial.pixmap(resolution); + _cachedProcessingStatusIcon = _nodeStyle.statusPartial.pixmap(resolution); + break; } - return {}; + _cachedProcessingStatus = _processingStatus; + _cachedProcessingStatusResolution = resolution; + _processingStatusIconDirty = false; + + return _cachedProcessingStatusIcon; } void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap &pixmap) @@ -103,16 +125,20 @@ void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap _nodeStyle.statusPartial = QIcon(pixmap); break; } + + _processingStatusIconDirty = true; } void NodeDelegateModel::setStatusIconStyle(const ProcessingIconStyle &style) { _nodeStyle.processingIconStyle = style; + _processingStatusIconDirty = true; } void NodeDelegateModel::setNodeProcessingStatus(NodeProcessingStatus status) { _processingStatus = status; + _processingStatusIconDirty = true; } void NodeDelegateModel::setBackgroundColor(QColor const &color) diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 742d4567..a8d76171 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -22,17 +22,38 @@ namespace QtNodes { namespace { +GraphicsView *graphics_view_from_widget(QWidget *widget) +{ + while (widget) { + if (auto *graphicsView = qobject_cast(widget)) { + return graphicsView; + } + widget = widget->parentWidget(); + } + + return nullptr; +} + QGraphicsItem::CacheMode initial_cache_mode(BasicGraphicsScene &scene) { QList const views = scene.views(); + int graphics_view_count = 0; + for (QGraphicsView *view : views) { if (auto *graphicsView = qobject_cast(view)) { - return graphicsView->rasterizationPolicy() == GraphicsView::RasterizationPolicy::Consistent - ? QGraphicsItem::NoCache - : QGraphicsItem::DeviceCoordinateCache; + ++graphics_view_count; + if (graphicsView->rasterizationPolicy() == GraphicsView::RasterizationPolicy::Consistent) { + return QGraphicsItem::NoCache; + } } } + // Cache mode is shared per item, so scenes rendered by multiple views fall back + // to uncached rendering instead of letting one view's policy govern the others. + if (graphics_view_count > 1) { + return QGraphicsItem::NoCache; + } + return QGraphicsItem::DeviceCoordinateCache; } @@ -187,7 +208,7 @@ void NodeGraphicsObject::reactToConnection(ConnectionGraphicsObject const *cgo) update(); } -void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *) +void NodeGraphicsObject::updateValidationTooltip() { QString tooltip; QVariant var = _graphModel.nodeData(_nodeId, NodeRole::ValidationState); @@ -198,10 +219,15 @@ void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const } } setToolTip(tooltip); +} +void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *widget) +{ painter->setClipRect(option->exposedRect); + _currentGraphicsView = graphics_view_from_widget(widget); nodeScene()->nodePainter().paint(painter, *this); + _currentGraphicsView = nullptr; } QVariant NodeGraphicsObject::itemChange(GraphicsItemChange change, const QVariant &value) @@ -329,6 +355,8 @@ void NodeGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent *event) nodeGroup->groupGraphicsObject().moveConnections(); if (nodeGroup->groupGraphicsObject().locked()) { nodeGroup->groupGraphicsObject().moveNodes(diff); + } else { + nodeGroup->groupGraphicsObject().updateGroupGeometry(); } } else { moveConnections(); diff --git a/src/NodeGroup.cpp b/src/NodeGroup.cpp index 901fb5e2..1c214973 100644 --- a/src/NodeGroup.cpp +++ b/src/NodeGroup.cpp @@ -21,7 +21,7 @@ NodeGroup::NodeGroup(std::vector nodes, QObject *parent) : QObject(parent) , _name(std::move(name)) - , _id(groupId) + , _groupId(groupId) , _childNodes(std::move(nodes)) , _groupGraphicsObject(nullptr) { @@ -33,7 +33,7 @@ QByteArray NodeGroup::saveToFile() const QJsonObject groupJson; groupJson["name"] = _name; - groupJson["id"] = static_cast(_id); + groupJson["id"] = static_cast(_groupId); QJsonArray nodesJson; for (auto const &node : _childNodes) { @@ -55,7 +55,7 @@ QByteArray NodeGroup::saveToFile() const QtNodes::GroupId NodeGroup::id() const { - return _id; + return _groupId; } GroupGraphicsObject &NodeGroup::groupGraphicsObject() @@ -112,6 +112,9 @@ void NodeGroup::addNode(NodeGraphicsObject *node) if (_groupGraphicsObject && _groupGraphicsObject->locked()) { node->lock(true); } + if (_groupGraphicsObject) { + _groupGraphicsObject->updateGroupGeometry(); + } } void NodeGroup::removeNode(NodeGraphicsObject *node) @@ -121,6 +124,8 @@ void NodeGroup::removeNode(NodeGraphicsObject *node) if (nodeIt != _childNodes.end()) { (*nodeIt)->unsetNodeGroup(); _childNodes.erase(nodeIt); - groupGraphicsObject().positionLockedIcon(); + if (_groupGraphicsObject) { + _groupGraphicsObject->updateGroupGeometry(); + } } } diff --git a/test/src/TestNodeGroup.cpp b/test/src/TestNodeGroup.cpp index 6599656e..a7815e7d 100644 --- a/test/src/TestNodeGroup.cpp +++ b/test/src/TestNodeGroup.cpp @@ -82,6 +82,17 @@ std::set toNodeIdSet(std::vector const &ids) return {ids.begin(), ids.end()}; } +QRectF groupSceneRect(NodeGroup const &group) +{ + auto const &groupGraphics = group.groupGraphicsObject(); + return groupGraphics.mapRectToScene(groupGraphics.rect()).boundingRect(); +} + +QRectF nodeSceneRect(NodeGraphicsObject const &node) +{ + return node.mapRectToScene(node.boundingRect()).boundingRect(); +} + } // namespace TEST_CASE("Node group creation", "[node-group]") @@ -200,6 +211,52 @@ TEST_CASE("Adding and removing nodes from a group", "[node-group]") CHECK(nodeGroup->id() == group->id()); } + SECTION("Group bounds follow added removed and moved nodes") + { + NodeId firstNodeId = createNode(model, scene); + NodeId secondNodeId = createNode(model, scene); + + model.setNodeData(firstNodeId, NodeRole::Position, QPointF(0.0, 0.0)); + model.setNodeData(secondNodeId, NodeRole::Position, QPointF(600.0, 0.0)); + QCoreApplication::processEvents(); + + auto *firstNode = scene.nodeGraphicsObject(firstNodeId); + auto *secondNode = scene.nodeGraphicsObject(secondNodeId); + REQUIRE(firstNode != nullptr); + REQUIRE(secondNode != nullptr); + + std::vector nodes{firstNode}; + auto group = scene.createGroup(nodes, QStringLiteral("BoundsGroup")).lock(); + REQUIRE(group); + + QRectF initialGroupRect = groupSceneRect(*group); + CHECK(initialGroupRect.contains(nodeSceneRect(*firstNode))); + CHECK_FALSE(initialGroupRect.contains(nodeSceneRect(*secondNode))); + + scene.addNodeToGroup(secondNodeId, group->id()); + QCoreApplication::processEvents(); + + QRectF expandedGroupRect = groupSceneRect(*group); + CHECK(expandedGroupRect.contains(nodeSceneRect(*firstNode))); + CHECK(expandedGroupRect.contains(nodeSceneRect(*secondNode))); + CHECK(expandedGroupRect.width() > initialGroupRect.width()); + + model.setNodeData(secondNodeId, NodeRole::Position, QPointF(900.0, 120.0)); + QCoreApplication::processEvents(); + + QRectF movedGroupRect = groupSceneRect(*group); + CHECK(movedGroupRect.contains(nodeSceneRect(*secondNode))); + CHECK(movedGroupRect.width() >= expandedGroupRect.width()); + + scene.removeNodeFromGroup(secondNodeId); + QCoreApplication::processEvents(); + + QRectF shrunkGroupRect = groupSceneRect(*group); + CHECK(shrunkGroupRect.contains(nodeSceneRect(*firstNode))); + CHECK_FALSE(shrunkGroupRect.contains(nodeSceneRect(*secondNode))); + CHECK(shrunkGroupRect.width() < movedGroupRect.width()); + } + SECTION("Removing nodes from a group and clearing empty groups") { std::vector nodes; diff --git a/test/src/TestZoomFeatures.cpp b/test/src/TestZoomFeatures.cpp index 9d3282d4..9c6078eb 100644 --- a/test/src/TestZoomFeatures.cpp +++ b/test/src/TestZoomFeatures.cpp @@ -117,6 +117,75 @@ TEST_CASE("GraphicsView scale range", "[zoom]") } } +TEST_CASE("GraphicsView node cache policy", "[zoom]") +{ + auto app = applicationSetup(); + + auto model = std::make_shared(); + BasicGraphicsScene scene(*model); + GraphicsView view(&scene); + + view.resize(800, 600); + view.show(); + REQUIRE(QTest::qWaitForWindowExposed(&view)); + + NodeId const nodeId = model->addNode("Node1"); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + SECTION("Rasterization policy toggles node cache mode") + { + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + QCoreApplication::processEvents(); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::DeviceCoordinateCache); + + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Consistent); + QCoreApplication::processEvents(); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::NoCache); + } + + SECTION("Smooth zoom disables node cache while animating") + { + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + QCoreApplication::processEvents(); + REQUIRE(nodeGraphics->cacheMode() == QGraphicsItem::DeviceCoordinateCache); + + QWheelEvent wheelEvent(QPointF(320.0, 240.0), + view.mapToGlobal(QPoint(320, 240)), + QPoint(0, 0), + QPoint(0, 120), + Qt::NoButton, + Qt::NoModifier, + Qt::ScrollPhase::NoScrollPhase, + false); + QApplication::sendEvent(view.viewport(), &wheelEvent); + QCoreApplication::processEvents(); + + CHECK(view.isZoomAnimating()); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::NoCache); + + QTest::qWait(500); + CHECK_FALSE(view.isZoomAnimating()); + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::DeviceCoordinateCache); + } + + SECTION("Shared scenes keep node cache disabled") + { + GraphicsView secondView(&scene); + secondView.resize(800, 600); + secondView.show(); + REQUIRE(QTest::qWaitForWindowExposed(&secondView)); + + view.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + secondView.setRasterizationPolicy(GraphicsView::RasterizationPolicy::Crisp); + QCoreApplication::processEvents(); + + CHECK(nodeGraphics->cacheMode() == QGraphicsItem::NoCache); + } +} + TEST_CASE("scaleChanged signal", "[zoom]") { auto app = applicationSetup(); From 7eac299601b3feb7bae52914bae9552d8329fbcd Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Tue, 24 Mar 2026 23:36:02 +0100 Subject: [PATCH 07/26] Replace QGraphicsDropShadowEffect with 9-slice painter shadow Generate a blurred rounded-rect atlas once per (shadow color, DPR) and draw it as 9 stretched tiles per node. This gives smooth soft falloff at the same cost as the previous 3-rect approximation (~0.85ms release) and is ~19x cheaper than QGraphicsDropShadowEffect. Remove the QGraphicsDropShadowEffect code path from NodeGraphicsObject. Re-enable ShadowEnabled in DefaultStyle.json. Expand bounding rect margins in both geometry classes to accommodate the shadow extent. --- resources/DefaultStyle.json | 2 +- src/DefaultHorizontalNodeGeometry.cpp | 7 +- src/DefaultNodePainter.cpp | 251 +++++++++++++++++++++++++- src/DefaultVerticalNodeGeometry.cpp | 5 +- src/NodeGraphicsObject.cpp | 10 - 5 files changed, 251 insertions(+), 24 deletions(-) diff --git a/resources/DefaultStyle.json b/resources/DefaultStyle.json index e793d3b6..2df69bfd 100644 --- a/resources/DefaultStyle.json +++ b/resources/DefaultStyle.json @@ -12,7 +12,7 @@ "GradientColor2": [64, 64, 64], "GradientColor3": [58, 58, 58], "ShadowColor": [20, 20, 20], - "ShadowEnabled": false, + "ShadowEnabled": true, "FontColor" : "white", "FontColorFaded" : "gray", "ConnectionPointColor": [169, 169, 169], diff --git a/src/DefaultHorizontalNodeGeometry.cpp b/src/DefaultHorizontalNodeGeometry.cpp index 58679511..c18d7cd0 100644 --- a/src/DefaultHorizontalNodeGeometry.cpp +++ b/src/DefaultHorizontalNodeGeometry.cpp @@ -27,8 +27,11 @@ QRectF DefaultHorizontalNodeGeometry::boundingRect(NodeId const nodeId) const { QSize s = size(nodeId); - qreal marginSize = 2.0 * _portSpasing; - QMargins margins(marginSize, marginSize, marginSize, marginSize); + // Margin must accommodate port circles and the painter-based shadow. + // The shadow extends further to the right and bottom due to its offset. + constexpr qreal base = 20.0; + constexpr qreal shadow_extra = 20.0; + QMarginsF margins(base, base, base + shadow_extra, base + shadow_extra); QRectF r(QPointF(0, 0), s); diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index d8fdcdfa..d6aba0eb 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -13,15 +13,242 @@ #include "StyleCollection.hpp" #include -#include +#include #include +#include #include +#include namespace QtNodes { namespace { +// ============================================================================ +// 9-slice shadow atlas +// ============================================================================ + +// Fixed geometry for the shadow. +constexpr double k_shadow_offset_x = 2.0; +constexpr double k_shadow_offset_y = 2.0; +constexpr double k_node_radius = 3.0; +// Box blur: radius per pass, number of passes. 3 passes of radius 5 +// approximate a gaussian with sigma ~5. +constexpr int k_blur_radius = 5; +constexpr int k_blur_passes = 3; +// The 9-slice margin must cover the FULL blur transition: both the +// outer decay to zero AND the inward ramp from the shape edge up to +// the plateau. With 3 passes of radius 5 the effective spread is +// ~15px in each direction. The tile boundary is placed at +// (outer + inner) from the atlas edge, i.e. well inside the plateau. +constexpr int k_outer_margin = 18; // outer decay to zero +constexpr int k_inner_margin = 16; // inward ramp to plateau +constexpr int k_shadow_margin = k_outer_margin + k_inner_margin; +// Interior body: must be large relative to the blur spread so the +// plateau reaches near-full alpha after blur. With ~15px spread, +// a 64px body ensures the center is well above the blur threshold. +constexpr int k_body_size = 64; +constexpr int k_atlas_size = k_body_size + 2 * k_shadow_margin; +// Global shadow opacity (0-255) applied after blur. +constexpr int k_shadow_opacity = 210; + +void box_blur_alpha(QImage &img, int radius) +{ + const int w = img.width(); + const int h = img.height(); + if (w == 0 || h == 0 || radius <= 0) { + return; + } + + const int span = 2 * radius + 1; + std::vector buf(static_cast(w) * h); + + // Read alpha from premultiplied ARGB scanlines. + auto alpha_at = [&](int x, int y) -> int { + return qAlpha(reinterpret_cast(img.constScanLine(y))[x]); + }; + + // Horizontal pass → buf + for (int y = 0; y < h; ++y) { + int sum = 0; + for (int x = -radius; x <= radius; ++x) { + sum += alpha_at(std::clamp(x, 0, w - 1), y); + } + buf[y * w] = static_cast(sum / span); + for (int x = 1; x < w; ++x) { + sum += alpha_at(std::min(x + radius, w - 1), y); + sum -= alpha_at(std::max(x - radius - 1, 0), y); + buf[y * w + x] = static_cast(sum / span); + } + } + + // Vertical pass → back into image + for (int x = 0; x < w; ++x) { + int sum = 0; + for (int y = -radius; y <= radius; ++y) { + sum += buf[std::clamp(y, 0, h - 1) * w + x]; + } + reinterpret_cast(img.scanLine(0))[x] = qPremultiply(qRgba(0, 0, 0, sum / span)); + for (int y = 1; y < h; ++y) { + sum += buf[std::min(y + radius, h - 1) * w + x]; + sum -= buf[std::max(y - radius - 1, 0) * w + x]; + reinterpret_cast(img.scanLine(y))[x] = qPremultiply(qRgba(0, 0, 0, sum / span)); + } + } +} + +QPixmap generate_shadow_atlas(QColor shadow_color, qreal dpr) +{ + const int phys = static_cast(std::ceil(k_atlas_size * dpr)); + + QImage img(phys, phys, QImage::Format_ARGB32_Premultiplied); + img.setDevicePixelRatio(dpr); + img.fill(Qt::transparent); + + // Draw an opaque rounded rect in the center of the atlas. + { + QPainter p(&img); + p.setRenderHint(QPainter::Antialiasing, true); + p.setPen(Qt::NoPen); + p.setBrush(Qt::white); + QRectF body(k_shadow_margin, k_shadow_margin, k_body_size, k_body_size); + p.drawRoundedRect(body, k_node_radius, k_node_radius); + } + + // Multi-pass box blur on the alpha channel. + const int phys_blur = std::max(1, static_cast(std::round(k_blur_radius * dpr))); + for (int pass = 0; pass < k_blur_passes; ++pass) { + box_blur_alpha(img, phys_blur); + } + + // Tint with shadow color and scale alpha by shadow opacity. + const int sr = shadow_color.red(); + const int sg = shadow_color.green(); + const int sb = shadow_color.blue(); + for (int y = 0; y < phys; ++y) { + auto *line = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < phys; ++x) { + const int a = (qAlpha(line[x]) * k_shadow_opacity) / 255; + line[x] = qPremultiply(qRgba(sr, sg, sb, a)); + } + } + + return QPixmap::fromImage(std::move(img)); +} + +struct Shadow_cache_key +{ + QRgb color; + int dpr_micro; // dpr * 1e6 as int for reliable comparison + + bool operator==(Shadow_cache_key const &o) const + { + return color == o.color && dpr_micro == o.dpr_micro; + } +}; + +struct Shadow_cache_key_hash +{ + std::size_t operator()(Shadow_cache_key const &k) const + { + return std::hash()( + (static_cast(k.color) << 32) | + static_cast(k.dpr_micro)); + } +}; + +std::unordered_map s_shadow_cache; + +QPixmap const &cached_shadow_atlas(QColor shadow_color, qreal dpr) +{ + Shadow_cache_key key{shadow_color.rgba(), + static_cast(dpr * 1000000.0)}; + auto it = s_shadow_cache.find(key); + if (it != s_shadow_cache.end()) { + return it->second; + } + + if (s_shadow_cache.size() > 16) { + s_shadow_cache.clear(); + } + + return s_shadow_cache.emplace(key, generate_shadow_atlas(shadow_color, dpr)) + .first->second; +} + +void draw_nine_slice_shadow( + QPainter *painter, + QColor shadow_color, + QRectF const &node_rect) +{ + const qreal dpr = painter->device() + ? static_cast(painter->device()->devicePixelRatio()) + : 1.0; + QPixmap const &atlas = cached_shadow_atlas(shadow_color, dpr); + if (atlas.isNull()) { + return; + } + + // Margin in logical coords — covers the full blur transition + // (outer falloff + inward ramp to plateau). + const qreal m = k_shadow_margin; + + // Destination rect: node rect shifted by shadow offset, expanded by margin. + const qreal dx = node_rect.x() + k_shadow_offset_x - m; + const qreal dy = node_rect.y() + k_shadow_offset_y - m; + const qreal dw = node_rect.width() + 2.0 * m; + const qreal dh = node_rect.height() + 2.0 * m; + const qreal inner_w = dw - 2.0 * m; + const qreal inner_h = dh - 2.0 * m; + + if (inner_w <= 0.0 || inner_h <= 0.0) { + return; + } + + // Source margin/body in logical atlas coords (atlas has DPR set). + const qreal sm = m; // source margin + const qreal sb = static_cast(k_body_size); // source body + + // Snap target rects to device pixels to prevent hairline gaps. + QTransform const &dt = painter->deviceTransform(); + bool inv_ok = false; + QTransform const inv = dt.inverted(&inv_ok); + auto snap = [&](qreal lx, qreal ly, qreal lw, qreal lh) -> QRectF { + if (!inv_ok) { + return QRectF(lx, ly, lw, lh); + } + QPointF p0 = dt.map(QPointF(lx, ly)); + QPointF p1 = dt.map(QPointF(lx + lw, ly + lh)); + p0 = QPointF(std::round(p0.x()), std::round(p0.y())); + p1 = QPointF(std::round(p1.x()), std::round(p1.y())); + return QRectF(inv.map(p0), inv.map(p1)); + }; + + // 9 source rects (logical coords in the atlas). + const QRectF s_tl(0, 0, sm, sm); + const QRectF s_tc(sm, 0, sb, sm); + const QRectF s_tr(sm + sb, 0, sm, sm); + const QRectF s_ml(0, sm, sm, sb); + const QRectF s_mc(sm, sm, sb, sb); + const QRectF s_mr(sm + sb, sm, sm, sb); + const QRectF s_bl(0, sm + sb, sm, sm); + const QRectF s_bc(sm, sm + sb, sb, sm); + const QRectF s_br(sm + sb, sm + sb, sm, sm); + + // 9 target rects (snapped to device pixels). + painter->drawPixmap(snap(dx, dy, m, m), atlas, s_tl); + painter->drawPixmap(snap(dx + m, dy, inner_w, m), atlas, s_tc); + painter->drawPixmap(snap(dx + m + inner_w, dy, m, m), atlas, s_tr); + painter->drawPixmap(snap(dx, dy + m, m, inner_h), atlas, s_ml); + painter->drawPixmap(snap(dx + m, dy + m, inner_w, inner_h), atlas, s_mc); + painter->drawPixmap(snap(dx + m + inner_w, dy + m, m, inner_h), atlas, s_mr); + painter->drawPixmap(snap(dx, dy + m + inner_h, m, m), atlas, s_bl); + painter->drawPixmap(snap(dx + m, dy + m + inner_h, inner_w, m), atlas, s_bc); + painter->drawPixmap(snap(dx + m + inner_w, dy + m + inner_h, m, m), atlas, s_br); +} + +// ============================================================================ + GraphicsView *graphics_view(NodeGraphicsObject &ngo) { if (auto *view = ngo.currentGraphicsView()) { @@ -215,13 +442,23 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo } } + QRectF boundary(0, 0, size.width(), size.height()); + + double const radius = 3.0; + + // 9-slice shadow: a precomputed blurred atlas is sliced into 9 tiles + // and stretched to fit the node. One atlas per (color, DPR), size- + // independent. Much faster than QGraphicsDropShadowEffect and smoother + // than stacked translucent rounded rects. + if (nodeStyle.ShadowEnabled) { + draw_nine_slice_shadow(painter, nodeStyle.ShadowColor, boundary); + } + if (ngo.nodeState().hovered()) { - QPen p(color, nodeStyle.HoveredPenWidth); - painter->setPen(p); + painter->setPen(QPen(color, nodeStyle.HoveredPenWidth)); } else { - QPen p(color, nodeStyle.PenWidth); - painter->setPen(p); + painter->setPen(QPen(color, nodeStyle.PenWidth)); } if (invalid) { @@ -233,12 +470,8 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo gradient.setColorAt(0.10, nodeStyle.GradientColor1); gradient.setColorAt(0.90, nodeStyle.GradientColor2); gradient.setColorAt(1.0, nodeStyle.GradientColor3); - painter->setBrush(gradient); } - QRectF boundary(0, 0, size.width(), size.height()); - - double const radius = 3.0; painter->drawRoundedRect(boundary, radius, radius); } diff --git a/src/DefaultVerticalNodeGeometry.cpp b/src/DefaultVerticalNodeGeometry.cpp index f20617f2..c675af1b 100644 --- a/src/DefaultVerticalNodeGeometry.cpp +++ b/src/DefaultVerticalNodeGeometry.cpp @@ -27,8 +27,9 @@ QRectF DefaultVerticalNodeGeometry::boundingRect(NodeId const nodeId) const { QSize s = size(nodeId); - qreal marginSize = 2.0 * _portSpasing; - QMargins margins(marginSize, marginSize, marginSize, marginSize); + constexpr qreal base = 20.0; + constexpr qreal shadow_extra = 20.0; + QMarginsF margins(base, base, base + shadow_extra, base + shadow_extra); QRectF r(QPointF(0, 0), s); diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index a8d76171..861a1119 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -13,7 +13,6 @@ #include "UndoCommands.hpp" #include -#include #include #include @@ -82,15 +81,6 @@ NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) NodeStyle nodeStyle(nodeStyleJson); - if (nodeStyle.ShadowEnabled) { - auto effect = new QGraphicsDropShadowEffect; - effect->setOffset(4, 4); - effect->setBlurRadius(20); - effect->setColor(nodeStyle.ShadowColor); - - setGraphicsEffect(effect); - } - setOpacity(nodeStyle.Opacity); setAcceptHoverEvents(true); From d53e8e2a0ea6dff9e877ffa0bcec7e00bec713de Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 05:57:51 +0100 Subject: [PATCH 08/26] Tighten node rendering bounds and cache handling --- CMakeLists.txt | 2 + .../internal/ConnectionGraphicsObject.hpp | 3 - .../internal/DefaultConnectionPainter.hpp | 6 +- .../QtNodes/internal/DefaultNodePainter.hpp | 4 + include/QtNodes/internal/GraphicsView.hpp | 2 +- .../QtNodes/internal/GroupGraphicsObject.hpp | 7 +- .../QtNodes/internal/NodeDelegateModel.hpp | 9 +- .../QtNodes/internal/NodeGraphicsObject.hpp | 2 + .../QtNodes/internal/NodeRenderingUtils.hpp | 54 +++++ src/DefaultConnectionPainter.cpp | 25 ++- src/DefaultHorizontalNodeGeometry.cpp | 12 +- src/DefaultNodePainter.cpp | 208 +++++++++--------- src/DefaultVerticalNodeGeometry.cpp | 10 +- src/GraphicsView.cpp | 8 +- src/GroupGraphicsObject.cpp | 9 +- src/NodeDelegateModel.cpp | 77 ++++--- src/NodeGraphicsObject.cpp | 9 +- src/NodeRenderingUtils.cpp | 57 +++++ test/src/TestBasicGraphicsScene.cpp | 79 +++++++ 19 files changed, 407 insertions(+), 176 deletions(-) create mode 100644 include/QtNodes/internal/NodeRenderingUtils.hpp create mode 100644 src/NodeRenderingUtils.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e63c2e64..7d4f8a2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,7 @@ set(CPP_SOURCE_FILES src/NodeDelegateModel.cpp src/NodeDelegateModelRegistry.cpp src/NodeGraphicsObject.cpp + src/NodeRenderingUtils.cpp src/NodeState.cpp src/NodeStyle.cpp src/StyleCollection.cpp @@ -115,6 +116,7 @@ set(HPP_HEADER_FILES include/QtNodes/internal/NodeDelegateModel.hpp include/QtNodes/internal/NodeDelegateModelRegistry.hpp include/QtNodes/internal/NodeGraphicsObject.hpp + include/QtNodes/internal/NodeRenderingUtils.hpp include/QtNodes/internal/NodeState.hpp include/QtNodes/internal/NodeStyle.hpp include/QtNodes/internal/OperatingSystem.hpp diff --git a/include/QtNodes/internal/ConnectionGraphicsObject.hpp b/include/QtNodes/internal/ConnectionGraphicsObject.hpp index b1c0b716..435972ef 100644 --- a/include/QtNodes/internal/ConnectionGraphicsObject.hpp +++ b/include/QtNodes/internal/ConnectionGraphicsObject.hpp @@ -96,10 +96,7 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject std::pair pointsC1C2Vertical() const; -private: void rebuildCachedGeometry() const; - -private: static constexpr int k_path_sample_count = 61; ConnectionId _connectionId; diff --git a/include/QtNodes/internal/DefaultConnectionPainter.hpp b/include/QtNodes/internal/DefaultConnectionPainter.hpp index f2a0f523..42e79f1b 100644 --- a/include/QtNodes/internal/DefaultConnectionPainter.hpp +++ b/include/QtNodes/internal/DefaultConnectionPainter.hpp @@ -18,6 +18,7 @@ class DefaultConnectionPainter : public AbstractConnectionPainter public: void paint(QPainter *painter, ConnectionGraphicsObject const &cgo) const override; QPainterPath getPainterStroke(ConnectionGraphicsObject const &cgo) const override; + private: QPainterPath cubicPath(ConnectionGraphicsObject const &connection) const; void drawSketchLine(QPainter *painter, ConnectionGraphicsObject const &cgo) const; @@ -28,7 +29,10 @@ class DefaultConnectionPainter : public AbstractConnectionPainter #endif private: - QPixmap _convertPixmap{QIcon(QStringLiteral(":/convert.png")).pixmap(QSize(22, 22))}; + QPixmap const &convertPixmap() const; + + mutable QPixmap _convertPixmap; + mutable bool _convertPixmapInitialized = false; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultNodePainter.hpp b/include/QtNodes/internal/DefaultNodePainter.hpp index 40fc6451..3c763e5b 100644 --- a/include/QtNodes/internal/DefaultNodePainter.hpp +++ b/include/QtNodes/internal/DefaultNodePainter.hpp @@ -17,6 +17,10 @@ class NodeGraphicsObject; class NodeState; /// @ Lightweight class incapsulating paint code. +/// +/// NOTE: Some draw helpers accept GraphicsView* for zoom-aware text rendering. +/// That paint-time coupling is intentional for now, but custom painters need to +/// account for it. class NODE_EDITOR_PUBLIC DefaultNodePainter : public AbstractNodePainter { public: diff --git a/include/QtNodes/internal/GraphicsView.hpp b/include/QtNodes/internal/GraphicsView.hpp index 682ca3c6..7263d2c5 100644 --- a/include/QtNodes/internal/GraphicsView.hpp +++ b/include/QtNodes/internal/GraphicsView.hpp @@ -129,7 +129,7 @@ public Q_SLOTS: void applyZoomStep(); void applyZoomFactor(double factor); void stopZoomTimer(); - void apply_rasterization_policy(); + void applyRasterizationPolicy(); double _zoomVelocity = 0.0; QPointF _zoomPivot; diff --git a/include/QtNodes/internal/GroupGraphicsObject.hpp b/include/QtNodes/internal/GroupGraphicsObject.hpp index 57760ee5..82990891 100644 --- a/include/QtNodes/internal/GroupGraphicsObject.hpp +++ b/include/QtNodes/internal/GroupGraphicsObject.hpp @@ -77,11 +77,6 @@ class GroupGraphicsObject */ NodeGroup const &group() const; - /** - * @copydoc QGraphicsItem::boundingRect() - */ - QRectF boundingRect() const override; - enum { Type = UserType + 3 }; /** @@ -216,7 +211,7 @@ class GroupGraphicsObject QPen _borderPen; private: - QRectF compute_group_rect() const; + QRectF computeGroupRect() const; private: /** diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index bfb1baf2..de0cc073 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include +#include #include #include #include @@ -109,7 +111,8 @@ class NODE_EDITOR_PUBLIC NodeDelegateModel /// Convenience helper to change the node background color. void setBackgroundColor(QColor const &color); - QPixmap processingStatusIcon() const; + QImage processingStatusImage(qreal dpr) const; + ProcessingIconStyle processingIconStyle() const; void setStatusIcon(NodeProcessingStatus status, const QPixmap &pixmap); @@ -200,7 +203,9 @@ public Q_SLOTS: mutable bool _processingStatusIconDirty{true}; mutable NodeProcessingStatus _cachedProcessingStatus{NodeProcessingStatus::NoStatus}; mutable int _cachedProcessingStatusResolution{0}; - mutable QPixmap _cachedProcessingStatusIcon; + mutable qreal _cachedProcessingStatusDpr{0.0}; + mutable QImage _cachedProcessingStatusImage; + mutable std::mutex _processingStatusIconMutex; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeGraphicsObject.hpp b/include/QtNodes/internal/NodeGraphicsObject.hpp index cade1bcc..e8cd640f 100644 --- a/include/QtNodes/internal/NodeGraphicsObject.hpp +++ b/include/QtNodes/internal/NodeGraphicsObject.hpp @@ -120,6 +120,8 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject QGraphicsProxyWidget *_proxyWidget; std::weak_ptr _nodeGroup{}; + // Transient paint-time context: set only while this node is being painted. + // A future cleanup should prefer threading this through explicitly. GraphicsView *_currentGraphicsView = nullptr; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeRenderingUtils.hpp b/include/QtNodes/internal/NodeRenderingUtils.hpp new file mode 100644 index 00000000..4f9f6116 --- /dev/null +++ b/include/QtNodes/internal/NodeRenderingUtils.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "AbstractGraphModel.hpp" +#include "NodeStyle.hpp" + +#include + +#include +#include + +class QIcon; +class QImage; +class QSize; + +namespace QtNodes::node_rendering { + +inline constexpr qreal k_shadow_offset_x = 2.0; +inline constexpr qreal k_shadow_offset_y = 2.0; +inline constexpr qreal k_node_radius = 3.0; +inline constexpr int k_blur_radius = 5; +inline constexpr int k_blur_passes = 3; +inline constexpr int k_outer_margin = 18; +inline constexpr int k_inner_margin = 16; +inline constexpr int k_shadow_margin = k_outer_margin + k_inner_margin; +inline constexpr int k_body_size = 64; +inline constexpr int k_atlas_size = k_body_size + 2 * k_shadow_margin; +inline constexpr int k_shadow_opacity = 210; +inline constexpr qreal k_port_margin = 20.0; + +inline constexpr qreal shadow_left_extent() { return k_shadow_margin - k_shadow_offset_x; } +inline constexpr qreal shadow_top_extent() { return k_shadow_margin - k_shadow_offset_y; } +inline constexpr qreal shadow_right_extent() { return k_shadow_margin + k_shadow_offset_x; } +inline constexpr qreal shadow_bottom_extent() { return k_shadow_margin + k_shadow_offset_y; } + +inline QMarginsF node_visual_margins(bool shadow_enabled) +{ + if (!shadow_enabled) { + return QMarginsF(k_port_margin, k_port_margin, k_port_margin, k_port_margin); + } + + return QMarginsF(std::max(k_port_margin, shadow_left_extent()), + std::max(k_port_margin, shadow_top_extent()), + std::max(k_port_margin, shadow_right_extent()), + std::max(k_port_margin, shadow_bottom_extent())); +} + +NodeStyle const &resolved_node_style( + AbstractGraphModel &model, + NodeId node_id, + std::optional &fallback_storage); + +QImage render_icon_image(QIcon const &icon, QSize const &logical_size, qreal dpr); + +} // namespace QtNodes::node_rendering diff --git a/src/DefaultConnectionPainter.cpp b/src/DefaultConnectionPainter.cpp index 8dbec19c..041cb357 100644 --- a/src/DefaultConnectionPainter.cpp +++ b/src/DefaultConnectionPainter.cpp @@ -23,6 +23,16 @@ QPen make_connection_pen(QColor const &color, qreal width, Qt::PenStyle style = } // namespace +QPixmap const &DefaultConnectionPainter::convertPixmap() const +{ + if (!_convertPixmapInitialized) { + _convertPixmap = QIcon(QStringLiteral(":/convert.png")).pixmap(QSize(22, 22)); + _convertPixmapInitialized = true; + } + + return _convertPixmap; +} + QPainterPath DefaultConnectionPainter::cubicPath(ConnectionGraphicsObject const &connection) const { return connection.cachedCubicPath(); @@ -127,8 +137,8 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, bool const selected = cgo.isSelected(); - auto const cubic = cubicPath(cgo); - if (useGradientColor) { + auto const cubic = cubicPath(cgo); + if (useGradientColor) { painter->setBrush(Qt::NoBrush); QColor cOut = normalColorOut; @@ -150,11 +160,12 @@ void DefaultConnectionPainter::drawNormalLine(QPainter *painter, painter->drawLine(cgo.cachedSamplePoint(i), cgo.cachedSamplePoint(i + 1)); } - QRectF const targetRect(cgo.cachedMidPoint().x() - _convertPixmap.width() / 2.0, - cgo.cachedMidPoint().y() - _convertPixmap.height() / 2.0, - _convertPixmap.width(), - _convertPixmap.height()); - painter->drawPixmap(targetRect, _convertPixmap, QRectF(_convertPixmap.rect())); + QPixmap const &conversionPixmap = convertPixmap(); + QRectF const targetRect(cgo.cachedMidPoint().x() - conversionPixmap.width() / 2.0, + cgo.cachedMidPoint().y() - conversionPixmap.height() / 2.0, + conversionPixmap.width(), + conversionPixmap.height()); + painter->drawPixmap(targetRect, conversionPixmap, QRectF(conversionPixmap.rect())); } else { p.setColor(normalColorOut); diff --git a/src/DefaultHorizontalNodeGeometry.cpp b/src/DefaultHorizontalNodeGeometry.cpp index c18d7cd0..f7f8c632 100644 --- a/src/DefaultHorizontalNodeGeometry.cpp +++ b/src/DefaultHorizontalNodeGeometry.cpp @@ -1,12 +1,15 @@ #include "DefaultHorizontalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" +#include "NodeRenderingUtils.hpp" #include "NodeData.hpp" #include #include #include +#include + namespace QtNodes { DefaultHorizontalNodeGeometry::DefaultHorizontalNodeGeometry(AbstractGraphModel &graphModel) @@ -26,12 +29,9 @@ DefaultHorizontalNodeGeometry::DefaultHorizontalNodeGeometry(AbstractGraphModel QRectF DefaultHorizontalNodeGeometry::boundingRect(NodeId const nodeId) const { QSize s = size(nodeId); - - // Margin must accommodate port circles and the painter-based shadow. - // The shadow extends further to the right and bottom due to its offset. - constexpr qreal base = 20.0; - constexpr qreal shadow_extra = 20.0; - QMarginsF margins(base, base, base + shadow_extra, base + shadow_extra); + std::optional fallback_style; + NodeStyle const &style = node_rendering::resolved_node_style(_graphModel, nodeId, fallback_style); + QMarginsF const margins = node_rendering::node_visual_margins(style.ShadowEnabled); QRectF r(QPointF(0, 0), s); diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index d6aba0eb..9830d1b1 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -7,6 +7,7 @@ #include "ConnectionIdUtils.hpp" #include "DataFlowGraphModel.hpp" #include "GraphicsView.hpp" +#include "NodeRenderingUtils.hpp" #include "NodeDelegateModel.hpp" #include "NodeGraphicsObject.hpp" #include "NodeState.hpp" @@ -18,40 +19,14 @@ #include #include +#include #include +#include namespace QtNodes { namespace { -// ============================================================================ -// 9-slice shadow atlas -// ============================================================================ - -// Fixed geometry for the shadow. -constexpr double k_shadow_offset_x = 2.0; -constexpr double k_shadow_offset_y = 2.0; -constexpr double k_node_radius = 3.0; -// Box blur: radius per pass, number of passes. 3 passes of radius 5 -// approximate a gaussian with sigma ~5. -constexpr int k_blur_radius = 5; -constexpr int k_blur_passes = 3; -// The 9-slice margin must cover the FULL blur transition: both the -// outer decay to zero AND the inward ramp from the shape edge up to -// the plateau. With 3 passes of radius 5 the effective spread is -// ~15px in each direction. The tile boundary is placed at -// (outer + inner) from the atlas edge, i.e. well inside the plateau. -constexpr int k_outer_margin = 18; // outer decay to zero -constexpr int k_inner_margin = 16; // inward ramp to plateau -constexpr int k_shadow_margin = k_outer_margin + k_inner_margin; -// Interior body: must be large relative to the blur spread so the -// plateau reaches near-full alpha after blur. With ~15px spread, -// a 64px body ensures the center is well above the blur threshold. -constexpr int k_body_size = 64; -constexpr int k_atlas_size = k_body_size + 2 * k_shadow_margin; -// Global shadow opacity (0-255) applied after blur. -constexpr int k_shadow_opacity = 210; - void box_blur_alpha(QImage &img, int radius) { const int w = img.width(); @@ -97,9 +72,9 @@ void box_blur_alpha(QImage &img, int radius) } } -QPixmap generate_shadow_atlas(QColor shadow_color, qreal dpr) +QImage generate_shadow_atlas(QColor shadow_color, qreal dpr) { - const int phys = static_cast(std::ceil(k_atlas_size * dpr)); + const int phys = static_cast(std::ceil(node_rendering::k_atlas_size * dpr)); QImage img(phys, phys, QImage::Format_ARGB32_Premultiplied); img.setDevicePixelRatio(dpr); @@ -111,29 +86,37 @@ QPixmap generate_shadow_atlas(QColor shadow_color, qreal dpr) p.setRenderHint(QPainter::Antialiasing, true); p.setPen(Qt::NoPen); p.setBrush(Qt::white); - QRectF body(k_shadow_margin, k_shadow_margin, k_body_size, k_body_size); - p.drawRoundedRect(body, k_node_radius, k_node_radius); + QRectF body(node_rendering::k_shadow_margin, + node_rendering::k_shadow_margin, + node_rendering::k_body_size, + node_rendering::k_body_size); + p.drawRoundedRect(body, node_rendering::k_node_radius, node_rendering::k_node_radius); } // Multi-pass box blur on the alpha channel. - const int phys_blur = std::max(1, static_cast(std::round(k_blur_radius * dpr))); - for (int pass = 0; pass < k_blur_passes; ++pass) { + const int phys_blur = std::max( + 1, + static_cast(std::round(node_rendering::k_blur_radius * dpr))); + for (int pass = 0; pass < node_rendering::k_blur_passes; ++pass) { box_blur_alpha(img, phys_blur); } - // Tint with shadow color and scale alpha by shadow opacity. + // Tint with shadow color while preserving the style alpha and applying the + // configured global strength multiplier. const int sr = shadow_color.red(); const int sg = shadow_color.green(); const int sb = shadow_color.blue(); + const int sa = shadow_color.alpha(); for (int y = 0; y < phys; ++y) { auto *line = reinterpret_cast(img.scanLine(y)); for (int x = 0; x < phys; ++x) { - const int a = (qAlpha(line[x]) * k_shadow_opacity) / 255; + const int blurred_alpha = (qAlpha(line[x]) * sa) / 255; + const int a = (blurred_alpha * node_rendering::k_shadow_opacity) / 255; line[x] = qPremultiply(qRgba(sr, sg, sb, a)); } } - return QPixmap::fromImage(std::move(img)); + return img; } struct Shadow_cache_key @@ -157,10 +140,13 @@ struct Shadow_cache_key_hash } }; -std::unordered_map s_shadow_cache; +std::unordered_map s_shadow_cache; +std::mutex s_shadow_cache_mutex; -QPixmap const &cached_shadow_atlas(QColor shadow_color, qreal dpr) +QImage cached_shadow_atlas(QColor shadow_color, qreal dpr) { + std::lock_guard lock(s_shadow_cache_mutex); + Shadow_cache_key key{shadow_color.rgba(), static_cast(dpr * 1000000.0)}; auto it = s_shadow_cache.find(key); @@ -168,8 +154,9 @@ QPixmap const &cached_shadow_atlas(QColor shadow_color, qreal dpr) return it->second; } - if (s_shadow_cache.size() > 16) { - s_shadow_cache.clear(); + if (s_shadow_cache.size() >= 32) { + // Arbitrary eviction is sufficient here; the cache is tiny. + s_shadow_cache.erase(s_shadow_cache.begin()); } return s_shadow_cache.emplace(key, generate_shadow_atlas(shadow_color, dpr)) @@ -182,20 +169,20 @@ void draw_nine_slice_shadow( QRectF const &node_rect) { const qreal dpr = painter->device() - ? static_cast(painter->device()->devicePixelRatio()) + ? painter->device()->devicePixelRatioF() : 1.0; - QPixmap const &atlas = cached_shadow_atlas(shadow_color, dpr); + QImage const atlas = cached_shadow_atlas(shadow_color, dpr); if (atlas.isNull()) { return; } // Margin in logical coords — covers the full blur transition // (outer falloff + inward ramp to plateau). - const qreal m = k_shadow_margin; + const qreal m = node_rendering::k_shadow_margin; // Destination rect: node rect shifted by shadow offset, expanded by margin. - const qreal dx = node_rect.x() + k_shadow_offset_x - m; - const qreal dy = node_rect.y() + k_shadow_offset_y - m; + const qreal dx = node_rect.x() + node_rendering::k_shadow_offset_x - m; + const qreal dy = node_rect.y() + node_rendering::k_shadow_offset_y - m; const qreal dw = node_rect.width() + 2.0 * m; const qreal dh = node_rect.height() + 2.0 * m; const qreal inner_w = dw - 2.0 * m; @@ -207,7 +194,7 @@ void draw_nine_slice_shadow( // Source margin/body in logical atlas coords (atlas has DPR set). const qreal sm = m; // source margin - const qreal sb = static_cast(k_body_size); // source body + const qreal sb = static_cast(node_rendering::k_body_size); // source body // Snap target rects to device pixels to prevent hairline gaps. QTransform const &dt = painter->deviceTransform(); @@ -236,15 +223,15 @@ void draw_nine_slice_shadow( const QRectF s_br(sm + sb, sm + sb, sm, sm); // 9 target rects (snapped to device pixels). - painter->drawPixmap(snap(dx, dy, m, m), atlas, s_tl); - painter->drawPixmap(snap(dx + m, dy, inner_w, m), atlas, s_tc); - painter->drawPixmap(snap(dx + m + inner_w, dy, m, m), atlas, s_tr); - painter->drawPixmap(snap(dx, dy + m, m, inner_h), atlas, s_ml); - painter->drawPixmap(snap(dx + m, dy + m, inner_w, inner_h), atlas, s_mc); - painter->drawPixmap(snap(dx + m + inner_w, dy + m, m, inner_h), atlas, s_mr); - painter->drawPixmap(snap(dx, dy + m + inner_h, m, m), atlas, s_bl); - painter->drawPixmap(snap(dx + m, dy + m + inner_h, inner_w, m), atlas, s_bc); - painter->drawPixmap(snap(dx + m + inner_w, dy + m + inner_h, m, m), atlas, s_br); + painter->drawImage(snap(dx, dy, m, m), atlas, s_tl); + painter->drawImage(snap(dx + m, dy, inner_w, m), atlas, s_tc); + painter->drawImage(snap(dx + m + inner_w, dy, m, m), atlas, s_tr); + painter->drawImage(snap(dx, dy + m, m, inner_h), atlas, s_ml); + painter->drawImage(snap(dx + m, dy + m, inner_w, inner_h), atlas, s_mc); + painter->drawImage(snap(dx + m + inner_w, dy + m, m, inner_h), atlas, s_mr); + painter->drawImage(snap(dx, dy + m + inner_h, m, m), atlas, s_bl); + painter->drawImage(snap(dx + m, dy + m + inner_h, inner_w, m), atlas, s_bc); + painter->drawImage(snap(dx + m + inner_w, dy + m + inner_h, m, m), atlas, s_br); } // ============================================================================ @@ -309,23 +296,36 @@ void configure_text_painter(QPainter *painter, GraphicsView *view) // family, size, weight, style, hinting, etc.) with the text string. // For typical node scenes the cache holds ~15 entries and never evicts. QHash s_text_path_cache; -QHash s_validation_icon_cache; +std::mutex s_text_path_cache_mutex; +std::unordered_map s_validation_icon_cache; +std::mutex s_validation_icon_cache_mutex; -QPixmap validation_icon(QIcon const &icon, QColor const &color) +QImage validation_icon(QIcon const &icon, QColor const &color, qreal dpr) { - auto it = s_validation_icon_cache.constFind(color.rgba()); - if (it != s_validation_icon_cache.constEnd()) { - return *it; + std::lock_guard lock(s_validation_icon_cache_mutex); + + Shadow_cache_key key{color.rgba(), + static_cast(dpr * 1000000.0)}; + auto it = s_validation_icon_cache.find(key); + if (it != s_validation_icon_cache.end()) { + return it->second; } - QPixmap pixmap = icon.pixmap(QSize(16, 16)); + QImage image = node_rendering::render_icon_image(icon, QSize(16, 16), dpr); + if (image.isNull()) { + return image; + } - QPainter imgPainter(&pixmap); + QPainter imgPainter(&image); imgPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); - imgPainter.fillRect(pixmap.rect(), color); + imgPainter.fillRect(QRect(QPoint(0, 0), QSize(16, 16)), color); imgPainter.end(); - return s_validation_icon_cache.insert(color.rgba(), std::move(pixmap)).value(); + if (s_validation_icon_cache.size() >= 32) { + s_validation_icon_cache.erase(s_validation_icon_cache.begin()); + } + + return s_validation_icon_cache.emplace(key, std::move(image)).first->second; } void draw_text( @@ -338,24 +338,29 @@ void draw_text( { if (should_draw_text_as_path(view)) { QString const key = font.key() + text; + QPainterPath path; + + { + std::lock_guard lock(s_text_path_cache_mutex); - auto it = s_text_path_cache.constFind(key); - if (it == s_text_path_cache.constEnd()) { - QPainterPath path; - path.addText(QPointF(0, 0), font, text); - it = s_text_path_cache.insert(key, std::move(path)); - - // Prevent unbounded growth for highly dynamic scenes. - if (s_text_path_cache.size() > 500) { - QPainterPath keep = *it; - s_text_path_cache.clear(); - it = s_text_path_cache.insert(key, std::move(keep)); + auto it = s_text_path_cache.constFind(key); + if (it == s_text_path_cache.constEnd()) { + if (s_text_path_cache.size() >= 500) { + // Arbitrary eviction keeps insertion cost predictable without LRU bookkeeping. + s_text_path_cache.erase(s_text_path_cache.begin()); + } + + QPainterPath new_path; + new_path.addText(QPointF(0, 0), font, text); + it = s_text_path_cache.insert(key, std::move(new_path)); } + + path = *it; } painter->setPen(Qt::NoPen); painter->translate(position); - painter->fillPath(*it, color); + painter->fillPath(path, color); painter->translate(-position); return; } @@ -373,24 +378,8 @@ void DefaultNodePainter::paint(QPainter *painter, NodeGraphicsObject &ngo) const NodeId const nodeId = ngo.nodeId(); GraphicsView *view = graphics_view(ngo); - // Fast path: get NodeStyle directly from the delegate model, avoiding - // the NodeStyle -> JSON -> QVariant -> JSON -> NodeStyle round-trip. - NodeStyle const *stylePtr = nullptr; - if (auto *dfModel = dynamic_cast(&model)) { - if (auto *delegate = dfModel->delegateModel(nodeId)) { - stylePtr = &delegate->nodeStyle(); - } - } - // Fallback: only constructed when the fast path above cannot resolve the - // style. The default NodeStyle() constructor is expensive (loads SVG - // icons and parses JSON from resources), so it must not run on every paint. - NodeStyle fallbackStorage(QJsonObject{}); - if (!stylePtr) { - QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); - fallbackStorage = NodeStyle(json.object()); - stylePtr = &fallbackStorage; - } - NodeStyle const &style = *stylePtr; + std::optional fallback_style; + NodeStyle const &style = node_rendering::resolved_node_style(model, nodeId, fallback_style); drawNodeRect(painter, ngo, style); @@ -680,19 +669,18 @@ void DefaultNodePainter::drawProcessingIndicator(QPainter *painter, NodeGraphics if (!delegate) return; - // Skip if status is NoStatus - if (delegate->processingStatus() == NodeProcessingStatus::NoStatus) - return; - AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); QSize size = geometry.size(nodeId); - QPixmap pixmap = delegate->processingStatusIcon(); - if (pixmap.isNull()) + qreal const dpr = painter->device() + ? painter->device()->devicePixelRatioF() + : 1.0; + QImage const image = delegate->processingStatusImage(dpr); + if (image.isNull()) return; - ProcessingIconStyle const &iconStyle = delegate->nodeStyle().processingIconStyle; + ProcessingIconStyle const iconStyle = delegate->processingIconStyle(); qreal iconSize = iconStyle._size; qreal margin = iconStyle._margin; @@ -710,7 +698,11 @@ void DefaultNodePainter::drawProcessingIndicator(QPainter *painter, NodeGraphics } QRectF const targetRect(x, size.height() - iconSize - margin, iconSize, iconSize); - painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); + qreal const image_dpr = image.devicePixelRatio(); + QRectF const sourceRect(QPointF(0, 0), + QSizeF(image.width() / image_dpr, + image.height() / image_dpr)); + painter->drawImage(targetRect, image, sourceRect); } void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo, NodeStyle const &nodeStyle) const @@ -733,7 +725,13 @@ void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObjec QColor color = (state._state == NodeValidationState::State::Error) ? nodeStyle.ErrorColor : nodeStyle.WarningColor; - QPixmap const pixmap = validation_icon(_toolTipIcon, color); + qreal const dpr = painter->device() + ? painter->device()->devicePixelRatioF() + : 1.0; + QImage const image = validation_icon(_toolTipIcon, color, dpr); + if (image.isNull()) { + return; + } QPointF center(size.width(), 0.0); center += QPointF(iconSize.width() / 2.0, -iconSize.height() / 2.0); @@ -742,7 +740,7 @@ void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObjec center.y() - iconSize.height() / 2.0, iconSize.width(), iconSize.height()); - painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); + painter->drawImage(targetRect, image, QRectF(QPointF(0, 0), QSizeF(iconSize))); } } // namespace QtNodes diff --git a/src/DefaultVerticalNodeGeometry.cpp b/src/DefaultVerticalNodeGeometry.cpp index c675af1b..08d89b19 100644 --- a/src/DefaultVerticalNodeGeometry.cpp +++ b/src/DefaultVerticalNodeGeometry.cpp @@ -1,12 +1,15 @@ #include "DefaultVerticalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" +#include "NodeRenderingUtils.hpp" #include "NodeData.hpp" #include #include #include +#include + namespace QtNodes { DefaultVerticalNodeGeometry::DefaultVerticalNodeGeometry(AbstractGraphModel &graphModel) @@ -26,10 +29,9 @@ DefaultVerticalNodeGeometry::DefaultVerticalNodeGeometry(AbstractGraphModel &gra QRectF DefaultVerticalNodeGeometry::boundingRect(NodeId const nodeId) const { QSize s = size(nodeId); - - constexpr qreal base = 20.0; - constexpr qreal shadow_extra = 20.0; - QMarginsF margins(base, base, base + shadow_extra, base + shadow_extra); + std::optional fallback_style; + NodeStyle const &style = node_rendering::resolved_node_style(_graphModel, nodeId, fallback_style); + QMarginsF const margins = node_rendering::node_visual_margins(style.ShadowEnabled); QRectF r(QPointF(0, 0), s); diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 20ca7092..70e14420 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -153,7 +153,7 @@ GraphicsView::GraphicsView(QWidget *parent) int maxSize = 32767; setSceneRect(-maxSize, -maxSize, (maxSize * 2), (maxSize * 2)); - apply_rasterization_policy(); + applyRasterizationPolicy(); } GraphicsView::GraphicsView(BasicGraphicsScene *scene, QWidget *parent) @@ -197,7 +197,7 @@ void GraphicsView::setScene(BasicGraphicsScene *scene) return; } - apply_rasterization_policy(); + applyRasterizationPolicy(); { // setup actions @@ -505,7 +505,7 @@ void GraphicsView::setRasterizationPolicy(RasterizationPolicy policy) } _rasterizationPolicy = policy; - apply_rasterization_policy(); + applyRasterizationPolicy(); } void GraphicsView::stopZoomAnimation() @@ -789,7 +789,7 @@ void GraphicsView::zoomFitSelected() } } -void GraphicsView::apply_rasterization_policy() +void GraphicsView::applyRasterizationPolicy() { if (_rasterizationPolicy == RasterizationPolicy::Consistent) { setCacheMode(QGraphicsView::CacheNone); diff --git a/src/GroupGraphicsObject.cpp b/src/GroupGraphicsObject.cpp index 4cbae9b1..a1d6a5d2 100644 --- a/src/GroupGraphicsObject.cpp +++ b/src/GroupGraphicsObject.cpp @@ -76,12 +76,7 @@ NodeGroup const &GroupGraphicsObject::group() const return _group; } -QRectF GroupGraphicsObject::boundingRect() const -{ - return QGraphicsRectItem::boundingRect(); -} - -QRectF GroupGraphicsObject::compute_group_rect() const +QRectF GroupGraphicsObject::computeGroupRect() const { QRectF ret{}; for (auto &node : _group.childNodes()) { @@ -215,7 +210,7 @@ void GroupGraphicsObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) void GroupGraphicsObject::updateGroupGeometry() { - QRectF const newRect = compute_group_rect(); + QRectF const newRect = computeGroupRect(); if (newRect != rect()) { prepareGeometryChange(); setRect(newRect); diff --git a/src/NodeDelegateModel.cpp b/src/NodeDelegateModel.cpp index 97f9a7af..12b55695 100644 --- a/src/NodeDelegateModel.cpp +++ b/src/NodeDelegateModel.cpp @@ -1,9 +1,36 @@ #include "NodeDelegateModel.hpp" +#include "NodeRenderingUtils.hpp" #include "StyleCollection.hpp" namespace QtNodes { +namespace { + +QIcon const &status_icon(NodeStyle const &style, NodeProcessingStatus status) +{ + switch (status) { + case NodeProcessingStatus::Updated: + return style.statusUpdated; + case NodeProcessingStatus::Processing: + return style.statusProcessing; + case NodeProcessingStatus::Pending: + return style.statusPending; + case NodeProcessingStatus::Empty: + return style.statusEmpty; + case NodeProcessingStatus::Failed: + return style.statusInvalid; + case NodeProcessingStatus::Partial: + return style.statusPartial; + case NodeProcessingStatus::NoStatus: + break; + } + + return style.statusEmpty; +} + +} // namespace + NodeDelegateModel::NodeDelegateModel() : _nodeStyle(StyleCollection::nodeStyle()) { @@ -53,12 +80,15 @@ NodeStyle const &NodeDelegateModel::nodeStyle() const void NodeDelegateModel::setNodeStyle(NodeStyle const &style) { + std::lock_guard lock(_processingStatusIconMutex); _nodeStyle = style; _processingStatusIconDirty = true; } -QPixmap NodeDelegateModel::processingStatusIcon() const +QImage NodeDelegateModel::processingStatusImage(qreal dpr) const { + std::lock_guard lock(_processingStatusIconMutex); + int const resolution = _nodeStyle.processingIconStyle._resolution; if (_processingStatus == NodeProcessingStatus::NoStatus) { @@ -66,43 +96,34 @@ QPixmap NodeDelegateModel::processingStatusIcon() const } if (!_processingStatusIconDirty && _cachedProcessingStatus == _processingStatus - && _cachedProcessingStatusResolution == resolution) { - return _cachedProcessingStatusIcon; + && _cachedProcessingStatusResolution == resolution + && qFuzzyCompare(_cachedProcessingStatusDpr, dpr)) { + return _cachedProcessingStatusImage; } - switch (_processingStatus) { - case NodeProcessingStatus::NoStatus: - _cachedProcessingStatusIcon = {}; - break; - case NodeProcessingStatus::Updated: - _cachedProcessingStatusIcon = _nodeStyle.statusUpdated.pixmap(resolution); - break; - case NodeProcessingStatus::Processing: - _cachedProcessingStatusIcon = _nodeStyle.statusProcessing.pixmap(resolution); - break; - case NodeProcessingStatus::Pending: - _cachedProcessingStatusIcon = _nodeStyle.statusPending.pixmap(resolution); - break; - case NodeProcessingStatus::Empty: - _cachedProcessingStatusIcon = _nodeStyle.statusEmpty.pixmap(resolution); - break; - case NodeProcessingStatus::Failed: - _cachedProcessingStatusIcon = _nodeStyle.statusInvalid.pixmap(resolution); - break; - case NodeProcessingStatus::Partial: - _cachedProcessingStatusIcon = _nodeStyle.statusPartial.pixmap(resolution); - break; - } + _cachedProcessingStatusImage = node_rendering::render_icon_image( + status_icon(_nodeStyle, _processingStatus), + QSize(resolution, resolution), + dpr); _cachedProcessingStatus = _processingStatus; _cachedProcessingStatusResolution = resolution; + _cachedProcessingStatusDpr = dpr; _processingStatusIconDirty = false; - return _cachedProcessingStatusIcon; + return _cachedProcessingStatusImage; +} + +ProcessingIconStyle NodeDelegateModel::processingIconStyle() const +{ + std::lock_guard lock(_processingStatusIconMutex); + return _nodeStyle.processingIconStyle; } void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap &pixmap) { + std::lock_guard lock(_processingStatusIconMutex); + switch (status) { case NodeProcessingStatus::NoStatus: break; @@ -131,12 +152,14 @@ void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap void NodeDelegateModel::setStatusIconStyle(const ProcessingIconStyle &style) { + std::lock_guard lock(_processingStatusIconMutex); _nodeStyle.processingIconStyle = style; _processingStatusIconDirty = true; } void NodeDelegateModel::setNodeProcessingStatus(NodeProcessingStatus status) { + std::lock_guard lock(_processingStatusIconMutex); _processingStatus = status; _processingStatusIconDirty = true; } diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 861a1119..98719b04 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -6,6 +6,7 @@ #include "ConnectionGraphicsObject.hpp" #include "ConnectionIdUtils.hpp" #include "GraphicsView.hpp" +#include "NodeRenderingUtils.hpp" #include "NodeConnectionInteraction.hpp" #include "NodeDelegateModel.hpp" #include "NodeGroup.hpp" @@ -16,6 +17,7 @@ #include #include +#include namespace QtNodes { @@ -77,9 +79,10 @@ NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) setCacheMode(initial_cache_mode(scene)); - QJsonObject nodeStyleJson = _graphModel.nodeData(_nodeId, NodeRole::Style).toJsonObject(); - - NodeStyle nodeStyle(nodeStyleJson); + std::optional fallback_style; + NodeStyle const &nodeStyle = node_rendering::resolved_node_style(_graphModel, + _nodeId, + fallback_style); setOpacity(nodeStyle.Opacity); diff --git a/src/NodeRenderingUtils.cpp b/src/NodeRenderingUtils.cpp new file mode 100644 index 00000000..fcf2c836 --- /dev/null +++ b/src/NodeRenderingUtils.cpp @@ -0,0 +1,57 @@ +#include "NodeRenderingUtils.hpp" + +#include "DataFlowGraphModel.hpp" +#include "NodeDelegateModel.hpp" +#include "StyleCollection.hpp" + +#include +#include +#include + +#include +#include + +namespace QtNodes::node_rendering { + +NodeStyle const &resolved_node_style( + AbstractGraphModel &model, + NodeId node_id, + std::optional &fallback_storage) +{ + if (auto *df_model = dynamic_cast(&model)) { + if (auto *delegate = df_model->delegateModel(node_id)) { + return delegate->nodeStyle(); + } + } + + QVariant const style_data = model.nodeData(node_id, NodeRole::Style); + if (!style_data.isValid() || style_data.isNull()) { + return StyleCollection::nodeStyle(); + } + + QJsonObject const style_json = QJsonDocument::fromVariant(style_data).object(); + if (style_json.isEmpty()) { + return StyleCollection::nodeStyle(); + } + + fallback_storage.emplace(style_json); + return *fallback_storage; +} + +QImage render_icon_image(QIcon const &icon, QSize const &logical_size, qreal dpr) +{ + QSize const physical_size(std::max(1, static_cast(std::ceil(logical_size.width() * dpr))), + std::max(1, static_cast(std::ceil(logical_size.height() * dpr)))); + + QImage image(physical_size, QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(dpr); + image.fill(Qt::transparent); + + QPainter painter(&image); + icon.paint(&painter, QRect(QPoint(0, 0), logical_size)); + painter.end(); + + return image; +} + +} // namespace QtNodes::node_rendering diff --git a/test/src/TestBasicGraphicsScene.cpp b/test/src/TestBasicGraphicsScene.cpp index 99c548f8..b52f08cf 100644 --- a/test/src/TestBasicGraphicsScene.cpp +++ b/test/src/TestBasicGraphicsScene.cpp @@ -2,10 +2,15 @@ #include "TestGraphModel.hpp" #include +#include +#include +#include #include #include +#include +#include #include using QtNodes::BasicGraphicsScene; @@ -13,6 +18,24 @@ using QtNodes::ConnectionId; using QtNodes::NodeId; using QtNodes::NodeRole; +namespace { + +QVariantMap shadow_enabled_style() +{ + return QtNodes::StyleCollection::nodeStyle().toJson().toVariantMap(); +} + +QVariantMap shadow_disabled_style() +{ + QVariantMap style = shadow_enabled_style(); + QVariantMap nodeStyle = style["NodeStyle"].toMap(); + nodeStyle["ShadowEnabled"] = false; + style["NodeStyle"] = nodeStyle; + return style; +} + +} // namespace + TEST_CASE("BasicGraphicsScene functionality", "[graphics]") { auto app = applicationSetup(); @@ -90,6 +113,17 @@ TEST_CASE("BasicGraphicsScene functionality", "[graphics]") // Don't call view.show() to avoid potential graphics system issues } + + SECTION("Nodes without explicit style fall back to collection defaults") + { + NodeId const nodeId = model.addNode("TestNode"); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + CHECK(nodeGraphics->opacity() == Approx(QtNodes::StyleCollection::nodeStyle().Opacity)); + } } TEST_CASE("BasicGraphicsScene undo/redo support", "[graphics]") @@ -119,3 +153,48 @@ TEST_CASE("BasicGraphicsScene undo/redo support", "[graphics]") CHECK(undoStack.count() >= 0); } } + +TEST_CASE("Node shadow bounds follow visual margins", "[graphics]") +{ + auto app = applicationSetup(); + TestGraphModel model; + BasicGraphicsScene scene(model); + + SECTION("Shadow-enabled bounds include the full painter shadow") + { + NodeId const nodeId = model.addNode("TestNode"); + model.setNodeData(nodeId, NodeRole::Style, shadow_enabled_style()); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + QRectF const bounds = nodeGraphics->boundingRect(); + QSize const size = scene.nodeGeometry().size(nodeId); + QMarginsF const margins = QtNodes::node_rendering::node_visual_margins(true); + + CHECK(bounds.left() == Approx(-margins.left())); + CHECK(bounds.top() == Approx(-margins.top())); + CHECK(bounds.right() == Approx(size.width() + margins.right())); + CHECK(bounds.bottom() == Approx(size.height() + margins.bottom())); + } + + SECTION("Shadow-disabled bounds fall back to port margins only") + { + NodeId const nodeId = model.addNode("TestNode"); + model.setNodeData(nodeId, NodeRole::Style, shadow_disabled_style()); + QCoreApplication::processEvents(); + + auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); + REQUIRE(nodeGraphics != nullptr); + + QRectF const bounds = nodeGraphics->boundingRect(); + QSize const size = scene.nodeGeometry().size(nodeId); + QMarginsF const margins = QtNodes::node_rendering::node_visual_margins(false); + + CHECK(bounds.left() == Approx(-margins.left())); + CHECK(bounds.top() == Approx(-margins.top())); + CHECK(bounds.right() == Approx(size.width() + margins.right())); + CHECK(bounds.bottom() == Approx(size.height() + margins.bottom())); + } +} From 0045c0aa351ab11687e585acf522f18c742dc246 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 06:34:18 +0100 Subject: [PATCH 09/26] Make smooth zoom time-invariant --- include/QtNodes/internal/GraphicsView.hpp | 7 ++++ src/GraphicsView.cpp | 49 +++++++++++++++++++++-- test/src/TestZoomFeatures.cpp | 30 ++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/include/QtNodes/internal/GraphicsView.hpp b/include/QtNodes/internal/GraphicsView.hpp index 7263d2c5..4ed90f72 100644 --- a/include/QtNodes/internal/GraphicsView.hpp +++ b/include/QtNodes/internal/GraphicsView.hpp @@ -4,6 +4,8 @@ #include "Export.hpp" +#include + namespace QtNodes { class BasicGraphicsScene; @@ -68,6 +70,10 @@ class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView void stopZoomAnimation(); + static double zoomAnimationScaleFactor(double velocity, double elapsedTimerSteps); + + static double zoomAnimationVelocityAfter(double velocity, double elapsedTimerSteps); + public Q_SLOTS: void scaleUp(); @@ -134,6 +140,7 @@ public Q_SLOTS: double _zoomVelocity = 0.0; QPointF _zoomPivot; int _zoomTimerId = 0; + std::chrono::steady_clock::time_point _lastZoomStepTime; TextRenderingPolicy _textRenderingPolicy = TextRenderingPolicy::PathAlways; RasterizationPolicy _rasterizationPolicy = RasterizationPolicy::Consistent; }; diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 70e14420..2d6b52d1 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -37,6 +37,13 @@ constexpr double zoom_per_notch = 1.05; constexpr int zoom_timer_interval_ms = 16; constexpr double zoom_velocity_epsilon = 0.001; +double zoom_base_k() +{ + static double const value = std::pow(zoom_per_notch, + (1.0 - zoom_friction) / zoom_impulse_per_step); + return value; +} + QPointF map_to_scene_exact(QGraphicsView const &view, QPointF const &viewPoint) { bool invertible = false; @@ -353,6 +360,10 @@ void GraphicsView::wheelEvent(QWheelEvent *event) _zoomPivot = event->position(); if (_zoomTimerId == 0 && std::abs(_zoomVelocity) >= zoom_velocity_epsilon) { + // Seed the timestamp one reference interval in the past so the immediate + // applyZoomStep() call below sees dt ≈ 1.0 and applies a full first step. + _lastZoomStepTime = std::chrono::steady_clock::now() + - std::chrono::milliseconds(zoom_timer_interval_ms); _zoomTimerId = startTimer(zoom_timer_interval_ms); refresh_node_cache_mode(scene(), true); } @@ -371,6 +382,29 @@ void GraphicsView::timerEvent(QTimerEvent *event) } } +double GraphicsView::zoomAnimationScaleFactor(double velocity, double elapsedTimerSteps) +{ + if (elapsedTimerSteps <= 0.0 || std::abs(velocity) < zoom_velocity_epsilon) { + return 1.0; + } + + double const velocityDecay = std::pow(zoom_friction, elapsedTimerSteps); + double const integratedVelocity = std::abs(1.0 - zoom_friction) > 1e-12 + ? velocity * (1.0 - velocityDecay) / (1.0 - zoom_friction) + : velocity * elapsedTimerSteps; + + return std::pow(zoom_base_k(), integratedVelocity); +} + +double GraphicsView::zoomAnimationVelocityAfter(double velocity, double elapsedTimerSteps) +{ + if (elapsedTimerSteps <= 0.0 || std::abs(velocity) < zoom_velocity_epsilon) { + return velocity; + } + + return velocity * std::pow(zoom_friction, elapsedTimerSteps); +} + void GraphicsView::applyZoomStep() { if (std::abs(_zoomVelocity) < zoom_velocity_epsilon) { @@ -378,9 +412,16 @@ void GraphicsView::applyZoomStep() return; } - static double const base_k = std::pow(zoom_per_notch, - (1.0 - zoom_friction) / zoom_impulse_per_step); - double const factor = std::pow(base_k, _zoomVelocity); + auto now = std::chrono::steady_clock::now(); + double elapsed_ms = std::chrono::duration(now - _lastZoomStepTime).count(); + _lastZoomStepTime = now; + + if (elapsed_ms <= 0.0) { + elapsed_ms = zoom_timer_interval_ms; + } + + double const dt = elapsed_ms / zoom_timer_interval_ms; + double const factor = zoomAnimationScaleFactor(_zoomVelocity, dt); double const current_scale = transform().m11(); double const new_scale = current_scale * factor; @@ -396,7 +437,7 @@ void GraphicsView::applyZoomStep() } applyZoomFactor(factor); - _zoomVelocity *= zoom_friction; + _zoomVelocity = zoomAnimationVelocityAfter(_zoomVelocity, dt); } void GraphicsView::applyZoomFactor(double factor) diff --git a/test/src/TestZoomFeatures.cpp b/test/src/TestZoomFeatures.cpp index 9c6078eb..36b3ba67 100644 --- a/test/src/TestZoomFeatures.cpp +++ b/test/src/TestZoomFeatures.cpp @@ -12,6 +12,8 @@ #include #include +#include + using QtNodes::BasicGraphicsScene; using QtNodes::GraphicsView; using QtNodes::NodeId; @@ -115,6 +117,34 @@ TEST_CASE("GraphicsView scale range", "[zoom]") CHECK(view.transform().dx() == Approx(0.0).margin(0.01)); CHECK(view.transform().dy() == Approx(0.0).margin(0.01)); } + + SECTION("Smooth wheel zoom math is invariant to timer cadence") + { + struct Zoom_state + { + double scale = 1.0; + double velocity = 0.0; + }; + + auto advanceZoom = [](double initialVelocity, std::vector const &elapsedSteps) { + Zoom_state state{1.0, initialVelocity}; + for (double const elapsedStep : elapsedSteps) { + state.scale *= GraphicsView::zoomAnimationScaleFactor(state.velocity, elapsedStep); + state.velocity = GraphicsView::zoomAnimationVelocityAfter(state.velocity, elapsedStep); + } + return state; + }; + + Zoom_state const singleGap = advanceZoom(4.0, {10.0}); + Zoom_state const splitGap = advanceZoom(4.0, {4.0, 6.0}); + Zoom_state const fineSteps = advanceZoom(4.0, {1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0}); + + CHECK(singleGap.scale == Approx(splitGap.scale).epsilon(1e-10)); + CHECK(singleGap.velocity == Approx(splitGap.velocity).epsilon(1e-10)); + CHECK(singleGap.scale == Approx(fineSteps.scale).epsilon(1e-10)); + CHECK(singleGap.velocity == Approx(fineSteps.velocity).epsilon(1e-10)); + } } TEST_CASE("GraphicsView node cache policy", "[zoom]") From 844141b6bb6ef731405fe5ecf1695ef79b5901e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 05:16:37 +0000 Subject: [PATCH 10/26] Cleanup: remove dead code, fix bug, deduplicate style/model helpers - Fix bug in NodeGraphicsObject::hoverMoveEvent: `|` -> `&` for flag test (line 466 was always true due to bitwise OR with Resizable) - Replace triplicated JSON style read/write macros across NodeStyle, ConnectionStyle, and GraphicsViewStyle with shared helpers in Style.hpp - Extract portCountRole() helper to eliminate repeated ternary expressions across 7 files - Extract DataFlowGraphModel::connectDelegateModel() to deduplicate ~30 lines of identical signal connection setup in addNode/loadNode - Remove unused hash specializations (pair/tuple) from ConnectionIdHash.hpp - Remove commented-out code from NodeGraphicsObject, ConnectionGraphicsObject, ConnectionState, and Style.hpp - Remove unused NODE_EDITOR_DEMANGLED macro from Export.hpp - Remove unused PySide2/Shiboken2 cmake modules (never referenced) - Simplify oppositePort() switch with direct returns Net reduction: ~715 lines across 20 files. https://claude.ai/code/session_015aZAxLSpakeTpF7zaq6D2F --- cmake/FindPySide2.cmake | 160 --------------- cmake/FindShiboken2.cmake | 187 ------------------ cmake/PySide2ModuleBuild.cmake | 157 --------------- include/QtNodes/internal/ConnectionIdHash.hpp | 32 +-- .../QtNodes/internal/ConnectionIdUtils.hpp | 22 +-- .../QtNodes/internal/DataFlowGraphModel.hpp | 2 + include/QtNodes/internal/Export.hpp | 6 - include/QtNodes/internal/Style.hpp | 56 +++++- src/AbstractGraphModel.cpp | 8 +- src/AbstractNodeGeometry.cpp | 6 +- src/ConnectionGraphicsObject.cpp | 6 - src/ConnectionState.cpp | 5 +- src/ConnectionStyle.cpp | 106 ++-------- src/DataFlowGraphModel.cpp | 113 ++++------- src/DefaultHorizontalNodeGeometry.cpp | 7 +- src/DefaultNodePainter.cpp | 15 +- src/DefaultVerticalNodeGeometry.cpp | 7 +- src/GraphicsViewStyle.cpp | 53 +---- src/NodeGraphicsObject.cpp | 6 +- src/NodeStyle.cpp | 145 ++++---------- 20 files changed, 192 insertions(+), 907 deletions(-) delete mode 100644 cmake/FindPySide2.cmake delete mode 100644 cmake/FindShiboken2.cmake delete mode 100644 cmake/PySide2ModuleBuild.cmake diff --git a/cmake/FindPySide2.cmake b/cmake/FindPySide2.cmake deleted file mode 100644 index 7ebb33d0..00000000 --- a/cmake/FindPySide2.cmake +++ /dev/null @@ -1,160 +0,0 @@ -# -# SPDX-FileCopyrightText: 2020-2021 Klarälvdalens Datakonsult AB, a KDAB Group company -# Author: Renato Araujo Oliveira Filho -# -# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only -# -# Contact KDAB at for commercial licensing options. -# - -# PYSIDE_BASEDIR - Top of the PySide2 installation -# PYSIDE_INCLUDE_DIR - Directories to include to use PySide2 -# PYSIDE_LIBRARY - Files to link against to use PySide2 -# PYSIDE_TYPESYSTEMS - Type system files that should be used by other bindings extending PySide2 -# -# You can install PySide2 from Qt repository with -# pip3 install --index-url=https://download.qt.io/official_releases/QtForPython --trusted-host download.qt.io pyside2 - -find_package(PkgConfig) -if(PKG_CONFIG_FOUND) - pkg_check_modules(PYSIDE2_PRIV QUIET pyside2) -endif() - -set(PYSIDE2_FOUND FALSE) - -if(PYSIDE2_PRIV_FOUND) - set(PYSIDE2_FOUND TRUE) - message(STATUS "Using PySide2 found in the system!") - pkg_get_variable(SHIBOKEN_BINARY - pyside2 - generator_location - ) - pkg_get_variable(PYSIDE2_BASEDIR - pyside2 - typesystemdir - ) - pkg_get_variable(PYSIDE_INCLUDE_DIR - pyside2 - includedir - ) - set(PYSIDE_TYPESYSTEMS ${PYSIDE2_BASEDIR}) - set(PYSIDE2_SO_VERSION ${PYSIDE2_PRIV_VERSION}) - set(PYSIDE_LIBRARY ${PYSIDE2_PRIV_LINK_LIBRARIES}) - list(GET PYSIDE_LIBRARY 0 PYSIDE_LIBRARY) -else() - # extract python library basename - list(GET Python3_LIBRARIES 0 PYTHON_LIBRARY_FILENAME) - get_filename_component(PYTHON_LIBRARY_FILENAME ${PYTHON_LIBRARY_FILENAME} NAME) - - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os, sys - try: - import PySide2.QtCore as QtCore - print(os.path.dirname(QtCore.__file__)) - except Exception as error: - print(error, file=sys.stderr) - exit() - " - OUTPUT_VARIABLE PYSIDE2_BASEDIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - if(PYSIDE2_BASEDIR) - set(PYSIDE_BASEDIR ${PYSIDE2_BASEDIR} CACHE PATH "Top level install of PySide2" FORCE) - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - import PySide2.QtCore as QtCore - print(os.path.basename(QtCore.__file__).split('.', 1)[1]) - " - OUTPUT_VARIABLE PYSIDE2_SUFFIX - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - import PySide2.QtCore as QtCore - print(';'.join(map(str, QtCore.__version_info__))) - " - OUTPUT_VARIABLE PYSIDE2_SO_VERSION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - list(GET PYSIDE2_SO_VERSION 0 PYSIDE2_SO_MACRO_VERSION) - list(GET PYSIDE2_SO_VERSION 1 PYSIDE2_SO_MICRO_VERSION) - list(GET PYSIDE2_SO_VERSION 2 PYSIDE2_SO_MINOR_VERSION) - string(REPLACE ";" "." PYSIDE2_SO_VERSION "${PYSIDE2_SO_VERSION}") - - if(NOT APPLE) - set(PYSIDE2_SUFFIX "${PYSIDE2_SUFFIX}.${PYSIDE2_SO_MACRO_VERSION}.${PYSIDE2_SO_MICRO_VERSION}") - else() - string(REPLACE ".so" "" PYSIDE2_SUFFIX ${PYSIDE2_SUFFIX}) - set(PYSIDE2_SUFFIX "${PYSIDE2_SUFFIX}.${PYSIDE2_SO_MACRO_VERSION}.${PYSIDE2_SO_MICRO_VERSION}.dylib") - endif() - - set(PYSIDE2_FOUND TRUE) - message(STATUS "PySide2 base dir: ${PYSIDE2_BASEDIR}" ) - message(STATUS "PySide2 suffix: ${PYSIDE2_SUFFIX}") - endif() - - if (PYSIDE2_FOUND) - #PySide - #=============================================================================== - find_path(PYSIDE_INCLUDE_DIR - pyside.h - PATHS ${PYSIDE2_BASEDIR}/include ${PYSIDE2_CUSTOM_PREFIX}/include/PySide2 - NO_DEFAULT_PATH) - - # Platform specific library names - if(MSVC) - SET(PYSIDE_LIBRARY_BASENAMES "pyside2.abi3.lib") - elseif(CYGWIN) - SET(PYSIDE_LIBRARY_BASENAMES "") - elseif(WIN32) - SET(PYSIDE_LIBRARY_BASENAMES "libpyside2.${PYSIDE2_SUFFIX}") - else() - SET(PYSIDE_LIBRARY_BASENAMES "libpyside2.${PYSIDE2_SUFFIX}") - endif() - - find_file(PYSIDE_LIBRARY - ${PYSIDE_LIBRARY_BASENAMES} - PATHS ${PYSIDE2_BASEDIR} ${PYSIDE2_CUSTOM_PREFIX}/lib - NO_DEFAULT_PATH) - - find_path(PYSIDE_TYPESYSTEMS - typesystem_core.xml - PATHS ${PYSIDE2_BASEDIR}/typesystems ${PYSIDE2_CUSTOM_PREFIX}/share/PySide2/typesystems - NO_DEFAULT_PATH) - endif() -endif() - -if(PYSIDE2_FOUND) - message(STATUS "PySide include dir: ${PYSIDE_INCLUDE_DIR}") - message(STATUS "PySide library: ${PYSIDE_LIBRARY}") - message(STATUS "PySide typesystems: ${PYSIDE_TYPESYSTEMS}") - message(STATUS "PySide2 version: ${PYSIDE2_SO_VERSION}") - - # Create PySide2 target - add_library(PySide2::pyside2 SHARED IMPORTED GLOBAL) - if(MSVC) - set_property(TARGET PySide2::pyside2 PROPERTY - IMPORTED_IMPLIB ${PYSIDE_LIBRARY}) - endif() - set_property(TARGET PySide2::pyside2 PROPERTY - IMPORTED_LOCATION ${PYSIDE_LIBRARY}) - set_property(TARGET PySide2::pyside2 APPEND PROPERTY - INTERFACE_INCLUDE_DIRECTORIES - ${PYSIDE_INCLUDE_DIR} - ${PYSIDE_INCLUDE_DIR}/QtCore/ - ${PYSIDE_INCLUDE_DIR}/QtGui/ - ${PYSIDE_INCLUDE_DIR}/QtWidgets/ - ${Python3_INCLUDE_DIRS} - ) -endif() - - -find_package_handle_standard_args(PySide2 - REQUIRED_VARS PYSIDE2_BASEDIR PYSIDE_INCLUDE_DIR PYSIDE_LIBRARY PYSIDE_TYPESYSTEMS - VERSION_VAR PYSIDE2_SO_VERSION -) diff --git a/cmake/FindShiboken2.cmake b/cmake/FindShiboken2.cmake deleted file mode 100644 index b86e6ad1..00000000 --- a/cmake/FindShiboken2.cmake +++ /dev/null @@ -1,187 +0,0 @@ -# -# SPDX-FileCopyrightText: 2020-2021 Klarälvdalens Datakonsult AB, a KDAB Group company -# Author: Renato Araujo Oliveira Filho -# -# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only -# -# Contact KDAB at for commercial licensing options. -# - -# SHIBOKEN_INCLUDE_DIR - Directories to include to use SHIBOKEN -# SHIBOKEN_LIBRARY - Files to link against to use SHIBOKEN -# SHIBOKEN_BINARY - Executable name -# SHIBOKEN_BUILD_TYPE - Tells if Shiboken was compiled in Release or Debug mode. - -# You can install Shiboken from Qt repository with -# pip3 install --index-url=https://download.qt.io/official_releases/QtForPython --trusted-host download.qt.io shiboken2-generator - -find_package(PkgConfig) -if(PKG_CONFIG_FOUND) - pkg_check_modules(SHIBOKEN2_PRIV QUIET shiboken2) -endif() - -set(SHIBOKEN_FOUND FALSE) - -if(SHIBOKEN2_PRIV_FOUND) - set(SHIBOKEN_FOUND TRUE) - message(STATUS "Using shiboken found in the system!") - pkg_get_variable(SHIBOKEN_BINARY - shiboken2 - generator_location - ) - pkg_get_variable(SHIBOKEN_BASEDIR - shiboken2 - libdir - ) - pkg_get_variable(SHIBOKEN_INCLUDE_DIR - shiboken2 - includedir - ) - set(SHIBOKEN_VERSION ${SHIBOKEN2_PRIV_VERSION}) - set(SHIBOKEN_LIBRARY ${SHIBOKEN2_PRIV_LINK_LIBRARIES}) -else() - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - try: - import shiboken2_generator - print(shiboken2_generator.__path__[0]) - except: - exit() - " - OUTPUT_VARIABLE SHIBOKEN_GENERATOR_BASEDIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - try: - import shiboken2 - print(shiboken2.__path__[0]) - except: - exit() - " - OUTPUT_VARIABLE SHIBOKEN_BASEDIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - import os - import shiboken2 - print(';'.join(filter(None, map(str, shiboken2.__version_info__)))) - " - OUTPUT_VARIABLE SHIBOKEN_VERSION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - list(GET SHIBOKEN_VERSION 0 SHIBOKEN_MACRO_VERSION) - list(GET SHIBOKEN_VERSION 1 SHIBOKEN_MICRO_VERSION) - list(GET SHIBOKEN_VERSION 2 SHIBOKEN_MINOR_VERSION) - string(REPLACE ";" "." SHIBOKEN_VERSION "${SHIBOKEN_VERSION}") - - message(STATUS "ShibokenGenerator base dir: ${SHIBOKEN_GENERATOR_BASEDIR}") - message(STATUS "Shiboken base dir: ${SHIBOKEN_BASEDIR}") - message(STATUS "Shiboken custom path: ${SHIBOKEN_CUSTOM_PATH}") - - if(SHIBOKEN_BASEDIR) - find_path(SHIBOKEN_INCLUDE_DIR - shiboken.h - PATHS ${SHIBOKEN_CUSTOM_PATH} ${SHIBOKEN_GENERATOR_BASEDIR}/include - NO_DEFAULT_PATH) - if(MSVC) - SET(SHIBOKEN_LIBRARY_BASENAMES "shiboken2.abi3.lib") - elseif(CYGWIN) - SET(SHIBOKEN_LIBRARY_BASENAMES "") - elseif(WIN32) - SET(SHIBOKEN_LIBRARY_BASENAMES "libshiboken2.${PYSIDE2_SUFFIX}") - elseif(APPLE) - SET(SHIBOKEN_LIBRARY_BASENAMES - libshiboken2.abi3.dylib - libshiboken2.abi3.${SHIBOKEN_MACRO_VERSION}.dylib - libshiboken2.abi3.${SHIBOKEN_MACRO_VERSION}.${SHIBOKEN_MICRO_VERSION}.dylib - libshiboken2.abi3.${SHIBOKEN_VERSION}.dylib - ) - else() - SET(SHIBOKEN_LIBRARY_BASENAMES - libshiboken2.abi3.so - libshiboken2.abi3.so.${SHIBOKEN_MACRO_VERSION} - libshiboken2.abi3.so.${SHIBOKEN_MACRO_VERSION}.${SHIBOKEN_MICRO_VERSION} - libshiboken2.abi3.so.${SHIBOKEN_VERSION} - ) - endif() - - if (NOT SHIBOKEN_INCLUDE_DIR) - return() - endif() - set(SHIBOKEN_SEARCH_PATHS ${SHIBOKEN_CUSTOM_PATH}) - list(APPEND SHIBOKEN_SEARCH_PATHS ${SHIBOKEN_BASEDIR}) - list(APPEND SHIBOKEN_SEARCH_PATHS ${SHIBOKEN_GENERATOR_BASEDIR}) - find_file(SHIBOKEN_LIBRARY - ${SHIBOKEN_LIBRARY_BASENAMES} - PATHS ${SHIBOKEN_SEARCH_PATHS} - NO_DEFAULT_PATH) - - find_program(SHIBOKEN_BINARY - shiboken2 - PATHS ${SHIBOKEN_SEARCH_PATHS} - NO_DEFAULT_PATH - ) - endif() - if (SHIBOKEN_INCLUDE_DIR AND SHIBOKEN_LIBRARY AND SHIBOKEN_BINARY) - set(SHIBOKEN_FOUND TRUE) - endif() - - if(SHIBOKEN_FOUND) - endif() - - - if(MSVC) - # On Windows we must link to python3.dll that is a small library that links against python3x.dll - # that allow us to choose any python3x.dll at runtime - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "if True: - for lib in '${Python3_LIBRARIES}'.split(';'): - if '/' in lib: - prefix, py = lib.rsplit('/', 1) - if py.startswith('python3'): - print(prefix + '/python3.lib') - break - " - OUTPUT_VARIABLE PYTHON_LIMITED_LIBRARIES - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - else() - # On Linux and MacOs our modules should not link with any python library - # that must be handled by the main process - set(PYTHON_LIMITED_LIBRARIES "") - endif() -endif() -if (SHIBOKEN_FOUND) - message(STATUS "Shiboken include dir: ${SHIBOKEN_INCLUDE_DIR}") - message(STATUS "Shiboken library: ${SHIBOKEN_LIBRARY}") - message(STATUS "Shiboken binary: ${SHIBOKEN_BINARY}") - message(STATUS "Shiboken version: ${SHIBOKEN_VERSION}") - - # Create shiboke2 target - add_library(Shiboken2::libshiboken SHARED IMPORTED GLOBAL) - if(MSVC) - set_property(TARGET Shiboken2::libshiboken PROPERTY - IMPORTED_IMPLIB ${SHIBOKEN_LIBRARY}) - endif() - set_property(TARGET Shiboken2::libshiboken PROPERTY - IMPORTED_LOCATION ${SHIBOKEN_LIBRARY}) - set_property(TARGET Shiboken2::libshiboken APPEND PROPERTY - INTERFACE_INCLUDE_DIRECTORIES ${SHIBOKEN_INCLUDE_DIR} ${Python3_INCLUDE_DIRS}) - set_property(TARGET Shiboken2::libshiboken APPEND PROPERTY - INTERFACE_LINK_LIBRARIES ${PYTHON_LIMITED_LIBRARIES}) - - # Generator target - add_executable(Shiboken2::shiboken IMPORTED GLOBAL) - set_property(TARGET Shiboken2::shiboken PROPERTY - IMPORTED_LOCATION ${SHIBOKEN_BINARY}) -endif() - -find_package_handle_standard_args(Shiboken2 - REQUIRED_VARS SHIBOKEN_BASEDIR SHIBOKEN_INCLUDE_DIR SHIBOKEN_LIBRARY SHIBOKEN_BINARY - VERSION_VAR SHIBOKEN_VERSION -) - diff --git a/cmake/PySide2ModuleBuild.cmake b/cmake/PySide2ModuleBuild.cmake deleted file mode 100644 index 06f2b620..00000000 --- a/cmake/PySide2ModuleBuild.cmake +++ /dev/null @@ -1,157 +0,0 @@ -# -# SPDX-FileCopyrightText: 2020-2021 Klarälvdalens Datakonsult AB, a KDAB Group company -# Author: Renato Araujo Oliveira Filho -# -# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only -# -# Contact KDAB at for commercial licensing options. -# - -if (NOT ${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX) - SET(${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX} CACHE FILEPATH "Custom path to install python bindings.") -endif() - -message(STATUS "PYTHON INSTALL PREFIX ${${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX}") - -if (WIN32) - set(PATH_SEP "\;") -else() - set(PATH_SEP ":") -endif() -if (NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 17) -endif() - -# On macOS, check if Qt is a framework build. This affects how include paths should be handled. -get_target_property(QtCore_is_framework Qt5::Core FRAMEWORK) -if (QtCore_is_framework) - # Get the path to the framework dir. - list(GET Qt5Core_INCLUDE_DIRS 0 QT_INCLUDE_DIR) - get_filename_component(QT_FRAMEWORK_INCLUDE_DIR "${QT_INCLUDE_DIR}/../" ABSOLUTE) - - # QT_INCLUDE_DIR points to the QtCore.framework directory, so we need to adjust this to point - # to the actual include directory, which has include files for non-framework parts of Qt. - get_filename_component(QT_INCLUDE_DIR "${QT_INCLUDE_DIR}/../../include" ABSOLUTE) -endif() - -# Flags that we will pass to shiboken-generator -# --generator-set=shiboken: tells the generator that we want to use shiboken to generate code, -# a doc generator is also available -# --enable-parent-ctor-heuristic: Enable heuristics to detect parent relationship on constructors, -# this try to guess parent ownership based on the arguments of the constructors -# --enable-pyside-extensionsL: This will generate code for Qt based classes, adding extra attributes, -# like signal, slot; -# --enable-return-value-heuristic: Similar as --enable-parent-ctor-heuristic this use some logic to guess -# parent child relationship based on the returned argument -# --use-isnull-as-nb_nonzero: If a class have an isNull() const method, it will be used to compute -# the value of boolean casts. -# Example, QImage::isNull() will be used when on python side you do `if (myQImage)` -set(GENERATOR_EXTRA_FLAGS --generator-set=shiboken - --enable-parent-ctor-heuristic - --enable-pyside-extensions - --enable-return-value-heuristic - --use-isnull-as-nb_nonzero - -std=c++${CMAKE_CXX_STANDARD}) - -# 2017-04-24 The protected hack can unfortunately not be disabled, because -# Clang does produce linker errors when we disable the hack. -# But the ugly workaround in Python is replaced by a shiboken change. -if(WIN32 OR DEFINED AVOID_PROTECTED_HACK) - set(GENERATOR_EXTRA_FLAGS ${GENERATOR_EXTRA_FLAGS} --avoid-protected-hack) - add_definitions(-DAVOID_PROTECTED_HACK) -endif() - -macro(make_path varname) - # accepts any number of path variables - string(REPLACE ";" "${PATH_SEP}" ${varname} "${ARGN}") -endmacro() - -# Creates a PySide module target based on the arguments -# This will: -# 1 - Create a Cmake custom-target that call shiboken-generator passign the correct arguments -# 2 - Create a Cmake library target called "Py${LIBRARY_NAME}" the output name of this target -# will be changed to match PySide template -# Args: -# LIBRARY_NAME - The name of the output module -# TYPESYSTEM_PATHS - A list of paths where shiboken should look for typesystem files -# INCLUDE_PATHS - Include pahts necessary to parse your class. *This is not the same as build* -# OUTPUT_SOURCES - The files that will be generated by shiboken -# TARGET_INCLUDE_DIRS - This will be passed to target_include_directories -# TARGET_LINK_LIBRARIES - This will be passed to target_link_libraries -# GLOBAL_INCLUDE - A header-file that contains alls classes that will be generated -# TYPESYSTEM_XML - The target binding typesystem (that should be the full path) -# DEPENDS - This var will be passed to add_custom_command(DEPENDS) so a new generation will be -# trigger if one of these files changes -# MODULE_OUTPUT_DIR - Where the library file should be stored -macro(CREATE_PYTHON_BINDINGS - LIBRARY_NAME - TYPESYSTEM_PATHS - INCLUDE_PATHS - OUTPUT_SOURCES - TARGET_INCLUDE_DIRS - TARGET_LINK_LIBRARIES - GLOBAL_INCLUDE - TYPESYSTEM_XML - DEPENDS - MODULE_OUTPUT_DIR) - - # Transform the path separators into something shiboken understands. - make_path(shiboken_include_dirs ${INCLUDE_PATHS}) - make_path(shiboken_typesystem_dirs ${TYPESYSTEM_PATHS}) - get_property(raw_python_dir_include_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES) - make_path(python_dir_include_dirs ${raw_python_dir_include_dirs}) - set(shiboken_include_dirs "${shiboken_include_dirs}${PATH_SEP}${python_dir_include_dirs}") - - set(shiboken_framework_include_dirs_option "") - if(CMAKE_HOST_APPLE) - set(shiboken_framework_include_dirs "${QT_FRAMEWORK_INCLUDE_DIR}") - make_path(shiboken_framework_include_dirs ${shiboken_framework_include_dirs}) - set(shiboken_framework_include_dirs_option "--framework-include-paths=${shiboken_framework_include_dirs}") - endif() - set_property(SOURCE ${OUTPUT_SOURCES} PROPERTY SKIP_AUTOGEN ON) - add_custom_command(OUTPUT ${OUTPUT_SOURCES} - COMMAND $ ${GENERATOR_EXTRA_FLAGS} - ${GLOBAL_INCLUDE} - --include-paths=${shiboken_include_dirs} - --typesystem-paths=${shiboken_typesystem_dirs} - ${shiboken_framework_include_dirs_option} - --output-directory=${CMAKE_CURRENT_BINARY_DIR} - ${TYPESYSTEM_XML} - DEPENDS ${TYPESYSTEM_XML} ${DEPENDS} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Running generator for ${LIBRARY_NAME} binding...") - - set(TARGET_NAME "Py${LIBRARY_NAME}") - set(MODULE_NAME "${LIBRARY_NAME}") - add_library(${TARGET_NAME} MODULE ${OUTPUT_SOURCES}) - - set_target_properties(${TARGET_NAME} PROPERTIES - PREFIX "" - OUTPUT_NAME ${MODULE_NAME} - LIBRARY_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} - ) - - if(WIN32) - set_target_properties(${TARGET_NAME} PROPERTIES SUFFIX ".pyd") - endif() - - target_include_directories(${TARGET_NAME} PUBLIC - ${TARGET_INCLUDE_DIRS} - ${PYSIDE_EXTRA_INCLUDES} - ) - - target_link_libraries(${TARGET_NAME} - ${TARGET_LINK_LIBRARIES} - PySide2::pyside2 - Shiboken2::libshiboken - ) - target_compile_definitions(${TARGET_NAME} - PRIVATE Py_LIMITED_API=0x03050000 - ) - if(APPLE) - set_property(TARGET ${TARGET_NAME} APPEND PROPERTY - LINK_FLAGS "-undefined dynamic_lookup") - endif() - install(TARGETS ${TARGET_NAME} - LIBRARY DESTINATION ${${PROJECT_NAME}_PYTHON_BINDINGS_INSTALL_PREFIX}/${TARGET_NAME}) -endmacro() diff --git a/include/QtNodes/internal/ConnectionIdHash.hpp b/include/QtNodes/internal/ConnectionIdHash.hpp index 09eaced6..1ab3f01e 100644 --- a/include/QtNodes/internal/ConnectionIdHash.hpp +++ b/include/QtNodes/internal/ConnectionIdHash.hpp @@ -4,17 +4,14 @@ #include "Definitions.hpp" -inline void hash_combine(std::size_t &seed) -{ - Q_UNUSED(seed); -} - template inline void hash_combine(std::size_t &seed, const T &v, Rest... rest) { std::hash hasher; seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); - hash_combine(seed, rest...); + if constexpr (sizeof...(rest) > 0) { + hash_combine(seed, rest...); + } } namespace std { @@ -29,27 +26,4 @@ struct hash } }; -template<> -struct hash> -{ - inline std::size_t operator()(std::pair const &nodePort) const - { - std::size_t h = 0; - hash_combine(h, nodePort.first, nodePort.second); - return h; - } -}; - -template<> -struct hash> -{ - using Key = std::tuple; - - inline std::size_t operator()(Key const &key) const - { - std::size_t h = 0; - hash_combine(h, std::get<0>(key), std::get<1>(key), std::get<2>(key)); - return h; - } -}; } // namespace std diff --git a/include/QtNodes/internal/ConnectionIdUtils.hpp b/include/QtNodes/internal/ConnectionIdUtils.hpp index 3e29babf..55421883 100644 --- a/include/QtNodes/internal/ConnectionIdUtils.hpp +++ b/include/QtNodes/internal/ConnectionIdUtils.hpp @@ -37,25 +37,14 @@ inline PortIndex getPortIndex(PortType portType, ConnectionId connectionId) inline PortType oppositePort(PortType port) { - PortType result = PortType::None; - switch (port) { case PortType::In: - result = PortType::Out; - break; - + return PortType::Out; case PortType::Out: - result = PortType::In; - break; - - case PortType::None: - result = PortType::None; - break; - + return PortType::In; default: - break; + return PortType::None; } - return result; } inline bool isPortIndexValid(PortIndex index) @@ -156,4 +145,9 @@ inline ConnectionId fromJson(QJsonObject const &connJson) return connId; } +inline NodeRole portCountRole(PortType portType) +{ + return (portType == PortType::Out) ? NodeRole::OutPortCount : NodeRole::InPortCount; +} + } // namespace QtNodes diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index b1ef892e..3e10c3a7 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -107,6 +107,8 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel private: NodeId newNodeId() override { return _nextNodeId++; } + void connectDelegateModel(NodeDelegateModel *model, NodeId nodeId); + void sendConnectionCreation(ConnectionId const connectionId); void sendConnectionDeletion(ConnectionId const connectionId); diff --git a/include/QtNodes/internal/Export.hpp b/include/QtNodes/internal/Export.hpp index 41e6e3d0..78ac2edf 100644 --- a/include/QtNodes/internal/Export.hpp +++ b/include/QtNodes/internal/Export.hpp @@ -17,12 +17,6 @@ #define NODE_EDITOR_LOCAL #endif -#ifdef __cplusplus -#define NODE_EDITOR_DEMANGLED extern "C" -#else -#define NODE_EDITOR_DEMANGLED -#endif - #if defined(NODE_EDITOR_SHARED) && !defined(NODE_EDITOR_STATIC) #ifdef NODE_EDITOR_EXPORTS #define NODE_EDITOR_PUBLIC NODE_EDITOR_EXPORT diff --git a/include/QtNodes/internal/Style.hpp b/include/QtNodes/internal/Style.hpp index f878083e..262bdfad 100644 --- a/include/QtNodes/internal/Style.hpp +++ b/include/QtNodes/internal/Style.hpp @@ -3,16 +3,15 @@ #include #include #include +#include #include #include #include namespace QtNodes { -class Style // : public QObject +class Style { - //Q_OBJECT - public: virtual ~Style() = default; @@ -45,4 +44,55 @@ class Style // : public QObject } }; +namespace detail { + +inline bool readColor(QJsonObject const &obj, QString const &key, QColor &color) +{ + if (!obj.contains(key)) + return false; + + QJsonValue value = obj[key]; + if (value.isArray()) { + auto colorArray = value.toArray(); + int rgb[] = {colorArray[0].toInt(), colorArray[1].toInt(), colorArray[2].toInt()}; + color = QColor(rgb[0], rgb[1], rgb[2]); + } else { + color = QColor(value.toString()); + } + return true; +} + +inline void writeColor(QJsonObject &obj, QString const &key, QColor const &color) +{ + obj[key] = color.name(); +} + +inline bool readFloat(QJsonObject const &obj, QString const &key, double &val) +{ + if (!obj.contains(key)) + return false; + val = obj[key].toDouble(); + return true; +} + +inline void writeFloat(QJsonObject &obj, QString const &key, double val) +{ + obj[key] = val; +} + +inline bool readBool(QJsonObject const &obj, QString const &key, bool &val) +{ + if (!obj.contains(key)) + return false; + val = obj[key].toBool(); + return true; +} + +inline void writeBool(QJsonObject &obj, QString const &key, bool val) +{ + obj[key] = val; +} + +} // namespace detail + } // namespace QtNodes diff --git a/src/AbstractGraphModel.cpp b/src/AbstractGraphModel.cpp index 3ea47435..06a69617 100644 --- a/src/AbstractGraphModel.cpp +++ b/src/AbstractGraphModel.cpp @@ -11,9 +11,7 @@ void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, { _shiftedByDynamicPortsConnections.clear(); - auto portCountRole = portType == PortType::In ? NodeRole::InPortCount : NodeRole::OutPortCount; - - unsigned int portCount = nodeData(nodeId, portCountRole).toUInt(); + unsigned int portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); if (first > portCount - 1) return; @@ -67,9 +65,7 @@ void AbstractGraphModel::portsAboutToBeInserted(NodeId const nodeId, { _shiftedByDynamicPortsConnections.clear(); - auto portCountRole = portType == PortType::In ? NodeRole::InPortCount : NodeRole::OutPortCount; - - unsigned int portCount = nodeData(nodeId, portCountRole).toUInt(); + unsigned int portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); if (first > portCount) return; diff --git a/src/AbstractNodeGeometry.cpp b/src/AbstractNodeGeometry.cpp index f6c893df..8240ff6c 100644 --- a/src/AbstractNodeGeometry.cpp +++ b/src/AbstractNodeGeometry.cpp @@ -1,6 +1,7 @@ #include "AbstractNodeGeometry.hpp" #include "AbstractGraphModel.hpp" +#include "ConnectionIdUtils.hpp" #include "StyleCollection.hpp" #include @@ -38,10 +39,7 @@ PortIndex AbstractNodeGeometry::checkPortHit(NodeId const nodeId, double const tolerance = 2.0 * nodeStyle.ConnectionPointDiameter; - size_t const n = _graphModel.nodeData(nodeId, - (portType == PortType::Out) - ? NodeRole::OutPortCount - : NodeRole::InPortCount); + size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)); for (unsigned int portIndex = 0; portIndex < n; ++portIndex) { auto pp = portPosition(nodeId, portType, portIndex); diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp index a840e474..931f7631 100644 --- a/src/ConnectionGraphicsObject.cpp +++ b/src/ConnectionGraphicsObject.cpp @@ -42,8 +42,6 @@ ConnectionGraphicsObject::ConnectionGraphicsObject(BasicGraphicsScene &scene, setAcceptHoverEvents(true); - //addGraphicsEffect(); - setZValue(-1.0); initializePosition(); @@ -362,10 +360,6 @@ void ConnectionGraphicsObject::addGraphicsEffect() effect->setBlurRadius(5); setGraphicsEffect(effect); - //auto effect = new QGraphicsDropShadowEffect; - //auto effect = new ConnectionBlurEffect(this); - //effect->setOffset(4, 4); - //effect->setColor(QColor(Qt::gray).darker(800)); } std::pair ConnectionGraphicsObject::pointsC1C2Horizontal() const diff --git a/src/ConnectionState.cpp b/src/ConnectionState.cpp index eed24a2f..5b411a9b 100644 --- a/src/ConnectionState.cpp +++ b/src/ConnectionState.cpp @@ -10,10 +10,7 @@ namespace QtNodes { -ConnectionState::~ConnectionState() -{ - //resetLastHoveredNode(); -} +ConnectionState::~ConnectionState() = default; PortType ConnectionState::requiredPort() const { diff --git a/src/ConnectionStyle.cpp b/src/ConnectionStyle.cpp index 81265898..a784aa9d 100644 --- a/src/ConnectionStyle.cpp +++ b/src/ConnectionStyle.cpp @@ -2,15 +2,14 @@ #include "StyleCollection.hpp" -#include #include -#include #include #include using QtNodes::ConnectionStyle; +using namespace QtNodes::detail; inline void initResources() { @@ -40,103 +39,38 @@ void ConnectionStyle::setConnectionStyle(QString jsonText) StyleCollection::setConnectionStyle(style); } -#ifdef STYLE_DEBUG -#define CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(v, variable) \ - { \ - if (v.type() == QJsonValue::Undefined || v.type() == QJsonValue::Null) \ - qWarning() << "Undefined value for parameter:" << #variable; \ - } -#else -#define CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(v, variable) -#endif - -#define CONNECTION_VALUE_EXISTS(v) \ - (v.type() != QJsonValue::Undefined && v.type() != QJsonValue::Null) - -#define CONNECTION_STYLE_READ_COLOR(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (CONNECTION_VALUE_EXISTS(valueRef)) { \ - if (valueRef.isArray()) { \ - auto colorArray = valueRef.toArray(); \ - std::vector rgb; \ - rgb.reserve(3); \ - for (auto it = colorArray.begin(); it != colorArray.end(); ++it) { \ - rgb.push_back((*it).toInt()); \ - } \ - variable = QColor(rgb[0], rgb[1], rgb[2]); \ - } else { \ - variable = QColor(valueRef.toString()); \ - } \ - } \ - } - -#define CONNECTION_STYLE_WRITE_COLOR(values, variable) \ - { \ - values[#variable] = variable.name(); \ - } - -#define CONNECTION_STYLE_READ_FLOAT(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (CONNECTION_VALUE_EXISTS(valueRef)) \ - variable = valueRef.toDouble(); \ - } - -#define CONNECTION_STYLE_WRITE_FLOAT(values, variable) \ - { \ - values[#variable] = variable; \ - } - -#define CONNECTION_STYLE_READ_BOOL(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - CONNECTION_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (CONNECTION_VALUE_EXISTS(valueRef)) \ - variable = valueRef.toBool(); \ - } - -#define CONNECTION_STYLE_WRITE_BOOL(values, variable) \ - { \ - values[#variable] = variable; \ - } - void ConnectionStyle::loadJson(QJsonObject const &json) { - QJsonValue nodeStyleValues = json["ConnectionStyle"]; - - QJsonObject obj = nodeStyleValues.toObject(); + QJsonObject obj = json["ConnectionStyle"].toObject(); - CONNECTION_STYLE_READ_COLOR(obj, ConstructionColor); - CONNECTION_STYLE_READ_COLOR(obj, NormalColor); - CONNECTION_STYLE_READ_COLOR(obj, SelectedColor); - CONNECTION_STYLE_READ_COLOR(obj, SelectedHaloColor); - CONNECTION_STYLE_READ_COLOR(obj, HoveredColor); + readColor(obj, "ConstructionColor", ConstructionColor); + readColor(obj, "NormalColor", NormalColor); + readColor(obj, "SelectedColor", SelectedColor); + readColor(obj, "SelectedHaloColor", SelectedHaloColor); + readColor(obj, "HoveredColor", HoveredColor); - CONNECTION_STYLE_READ_FLOAT(obj, LineWidth); - CONNECTION_STYLE_READ_FLOAT(obj, ConstructionLineWidth); - CONNECTION_STYLE_READ_FLOAT(obj, PointDiameter); + readFloat(obj, "LineWidth", LineWidth); + readFloat(obj, "ConstructionLineWidth", ConstructionLineWidth); + readFloat(obj, "PointDiameter", PointDiameter); - CONNECTION_STYLE_READ_BOOL(obj, UseDataDefinedColors); + readBool(obj, "UseDataDefinedColors", UseDataDefinedColors); } QJsonObject ConnectionStyle::toJson() const { QJsonObject obj; - CONNECTION_STYLE_WRITE_COLOR(obj, ConstructionColor); - CONNECTION_STYLE_WRITE_COLOR(obj, NormalColor); - CONNECTION_STYLE_WRITE_COLOR(obj, SelectedColor); - CONNECTION_STYLE_WRITE_COLOR(obj, SelectedHaloColor); - CONNECTION_STYLE_WRITE_COLOR(obj, HoveredColor); + writeColor(obj, "ConstructionColor", ConstructionColor); + writeColor(obj, "NormalColor", NormalColor); + writeColor(obj, "SelectedColor", SelectedColor); + writeColor(obj, "SelectedHaloColor", SelectedHaloColor); + writeColor(obj, "HoveredColor", HoveredColor); - CONNECTION_STYLE_WRITE_FLOAT(obj, LineWidth); - CONNECTION_STYLE_WRITE_FLOAT(obj, ConstructionLineWidth); - CONNECTION_STYLE_WRITE_FLOAT(obj, PointDiameter); + writeFloat(obj, "LineWidth", LineWidth); + writeFloat(obj, "ConstructionLineWidth", ConstructionLineWidth); + writeFloat(obj, "PointDiameter", PointDiameter); - CONNECTION_STYLE_WRITE_BOOL(obj, UseDataDefinedColors); + writeBool(obj, "UseDataDefinedColors", UseDataDefinedColors); QJsonObject root; root["ConnectionStyle"] = obj; diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index d269dc9d..958ab0c8 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -1,6 +1,7 @@ #include "DataFlowGraphModel.hpp" #include "ConnectionIdHash.hpp" +#include "ConnectionIdUtils.hpp" #include "Definitions.hpp" #include @@ -66,39 +67,7 @@ NodeId DataFlowGraphModel::addNode(QString const nodeType) if (model) { NodeId newId = newNodeId(); - connect(model.get(), - &NodeDelegateModel::dataUpdated, - [newId, this](PortIndex const portIndex) { - onOutPortDataUpdated(newId, portIndex); - }); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeDeleted, - this, - [newId, this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeDeleted(newId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsDeleted, - this, - &DataFlowGraphModel::portsDeleted); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeInserted, - this, - [newId, this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeInserted(newId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsInserted, - this, - &DataFlowGraphModel::portsInserted); - - connect(model.get(), &NodeDelegateModel::requestNodeUpdate, this, [newId, this]() { - Q_EMIT nodeUpdated(newId); - }); + connectDelegateModel(model.get(), newId); _models[newId] = std::move(model); @@ -120,10 +89,7 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con // Check port bounds, i.e. that we do not connect non-existing port numbers auto checkPortBounds = [&](PortType const portType) { NodeId const nodeId = getNodeId(portType, connectionId); - auto portCountRole = (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount; - - std::size_t const portCount = nodeData(nodeId, portCountRole).toUInt(); + std::size_t const portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); return getPortIndex(portType, connectionId) < portCount; }; @@ -203,6 +169,43 @@ void DataFlowGraphModel::addConnection(ConnectionId const connectionId) PortRole::Data); } +void DataFlowGraphModel::connectDelegateModel(NodeDelegateModel *model, NodeId nodeId) +{ + connect(model, + &NodeDelegateModel::dataUpdated, + [nodeId, this](PortIndex const portIndex) { + onOutPortDataUpdated(nodeId, portIndex); + }); + + connect(model, + &NodeDelegateModel::portsAboutToBeDeleted, + this, + [nodeId, this](PortType const portType, PortIndex const first, PortIndex const last) { + portsAboutToBeDeleted(nodeId, portType, first, last); + }); + + connect(model, + &NodeDelegateModel::portsDeleted, + this, + &DataFlowGraphModel::portsDeleted); + + connect(model, + &NodeDelegateModel::portsAboutToBeInserted, + this, + [nodeId, this](PortType const portType, PortIndex const first, PortIndex const last) { + portsAboutToBeInserted(nodeId, portType, first, last); + }); + + connect(model, + &NodeDelegateModel::portsInserted, + this, + &DataFlowGraphModel::portsInserted); + + connect(model, &NodeDelegateModel::requestNodeUpdate, this, [nodeId, this]() { + Q_EMIT nodeUpdated(nodeId); + }); +} + void DataFlowGraphModel::sendConnectionCreation(ConnectionId const connectionId) { Q_EMIT connectionCreated(connectionId); @@ -558,41 +561,7 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) std::unique_ptr model = _registry->create(delegateModelName); if (model) { - connect(model.get(), - &NodeDelegateModel::dataUpdated, - [restoredNodeId, this](PortIndex const portIndex) { - onOutPortDataUpdated(restoredNodeId, portIndex); - }); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeDeleted, - this, - [restoredNodeId, - this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeDeleted(restoredNodeId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsDeleted, - this, - &DataFlowGraphModel::portsDeleted); - - connect(model.get(), - &NodeDelegateModel::portsAboutToBeInserted, - this, - [restoredNodeId, - this](PortType const portType, PortIndex const first, PortIndex const last) { - portsAboutToBeInserted(restoredNodeId, portType, first, last); - }); - - connect(model.get(), - &NodeDelegateModel::portsInserted, - this, - &DataFlowGraphModel::portsInserted); - - connect(model.get(), &NodeDelegateModel::requestNodeUpdate, this, [restoredNodeId, this]() { - Q_EMIT nodeUpdated(restoredNodeId); - }); + connectDelegateModel(model.get(), restoredNodeId); _models[restoredNodeId] = std::move(model); diff --git a/src/DefaultHorizontalNodeGeometry.cpp b/src/DefaultHorizontalNodeGeometry.cpp index f7f8c632..eebf1a69 100644 --- a/src/DefaultHorizontalNodeGeometry.cpp +++ b/src/DefaultHorizontalNodeGeometry.cpp @@ -1,6 +1,7 @@ #include "DefaultHorizontalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" +#include "ConnectionIdUtils.hpp" #include "NodeRenderingUtils.hpp" #include "NodeData.hpp" @@ -227,11 +228,7 @@ unsigned int DefaultHorizontalNodeGeometry::maxPortsTextAdvance(NodeId const nod { unsigned int width = 0; - size_t const n = _graphModel - .nodeData(nodeId, - (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount) - .toUInt(); + size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)).toUInt(); for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { QString name; diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 9830d1b1..6e344bef 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -477,9 +477,7 @@ void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObj auto reducedDiameter = diameter * 0.6; for (PortType portType : {PortType::Out, PortType::In}) { - auto portCountRole = (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount; - size_t const n = model.nodeData(nodeId, portCountRole).toUInt(); + size_t const n = model.nodeData(nodeId, portCountRole(portType)).toUInt(); for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { QPointF p = geometry.portPosition(nodeId, portType, portIndex); @@ -541,11 +539,7 @@ void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraph auto diameter = nodeStyle.ConnectionPointDiameter; for (PortType portType : {PortType::Out, PortType::In}) { - size_t const n = model - .nodeData(nodeId, - (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount) - .toUInt(); + size_t const n = model.nodeData(nodeId, portCountRole(portType)).toUInt(); for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { QPointF p = geometry.portPosition(nodeId, portType, portIndex); @@ -611,10 +605,7 @@ void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject & AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); for (PortType portType : {PortType::Out, PortType::In}) { - unsigned int n = model.nodeData(nodeId, - (portType == PortType::Out) - ? NodeRole::OutPortCount - : NodeRole::InPortCount); + unsigned int n = model.nodeData(nodeId, portCountRole(portType)); for (PortIndex portIndex = 0; portIndex < n; ++portIndex) { auto const &connected = model.connections(nodeId, portType, portIndex); diff --git a/src/DefaultVerticalNodeGeometry.cpp b/src/DefaultVerticalNodeGeometry.cpp index 08d89b19..43a9c8d0 100644 --- a/src/DefaultVerticalNodeGeometry.cpp +++ b/src/DefaultVerticalNodeGeometry.cpp @@ -1,6 +1,7 @@ #include "DefaultVerticalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" +#include "ConnectionIdUtils.hpp" #include "NodeRenderingUtils.hpp" #include "NodeData.hpp" @@ -247,11 +248,7 @@ unsigned int DefaultVerticalNodeGeometry::maxPortsTextAdvance(NodeId const nodeI { unsigned int width = 0; - size_t const n = _graphModel - .nodeData(nodeId, - (portType == PortType::Out) ? NodeRole::OutPortCount - : NodeRole::InPortCount) - .toUInt(); + size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)).toUInt(); for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { QString name; diff --git a/src/GraphicsViewStyle.cpp b/src/GraphicsViewStyle.cpp index 3138e8ee..c09645f3 100644 --- a/src/GraphicsViewStyle.cpp +++ b/src/GraphicsViewStyle.cpp @@ -2,13 +2,10 @@ #include "StyleCollection.hpp" -#include -#include #include -#include - using QtNodes::GraphicsViewStyle; +using namespace QtNodes::detail; inline void initResources() { @@ -37,56 +34,22 @@ void GraphicsViewStyle::setStyle(QString jsonText) StyleCollection::setGraphicsViewStyle(style); } -#ifdef STYLE_DEBUG -#define FLOW_VIEW_STYLE_CHECK_UNDEFINED_VALUE(v, variable) \ - { \ - if (v.type() == QJsonValue::Undefined || v.type() == QJsonValue::Null) \ - qWarning() << "Undefined value for parameter:" << #variable; \ - } -#else -#define FLOW_VIEW_STYLE_CHECK_UNDEFINED_VALUE(v, variable) -#endif - -#define FLOW_VIEW_STYLE_READ_COLOR(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - FLOW_VIEW_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (valueRef.isArray()) { \ - auto colorArray = valueRef.toArray(); \ - std::vector rgb; \ - rgb.reserve(3); \ - for (auto it = colorArray.begin(); it != colorArray.end(); ++it) { \ - rgb.push_back((*it).toInt()); \ - } \ - variable = QColor(rgb[0], rgb[1], rgb[2]); \ - } else { \ - variable = QColor(valueRef.toString()); \ - } \ - } - -#define FLOW_VIEW_STYLE_WRITE_COLOR(values, variable) \ - { \ - values[#variable] = variable.name(); \ - } - void GraphicsViewStyle::loadJson(QJsonObject const &json) { - QJsonValue nodeStyleValues = json["GraphicsViewStyle"]; - - QJsonObject obj = nodeStyleValues.toObject(); + QJsonObject obj = json["GraphicsViewStyle"].toObject(); - FLOW_VIEW_STYLE_READ_COLOR(obj, BackgroundColor); - FLOW_VIEW_STYLE_READ_COLOR(obj, FineGridColor); - FLOW_VIEW_STYLE_READ_COLOR(obj, CoarseGridColor); + readColor(obj, "BackgroundColor", BackgroundColor); + readColor(obj, "FineGridColor", FineGridColor); + readColor(obj, "CoarseGridColor", CoarseGridColor); } QJsonObject GraphicsViewStyle::toJson() const { QJsonObject obj; - FLOW_VIEW_STYLE_WRITE_COLOR(obj, BackgroundColor); - FLOW_VIEW_STYLE_WRITE_COLOR(obj, FineGridColor); - FLOW_VIEW_STYLE_WRITE_COLOR(obj, CoarseGridColor); + writeColor(obj, "BackgroundColor", BackgroundColor); + writeColor(obj, "FineGridColor", FineGridColor); + writeColor(obj, "CoarseGridColor", CoarseGridColor); QJsonObject root; root["GraphicsViewStyle"] = obj; diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 98719b04..1b2e8331 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -147,7 +147,7 @@ void NodeGraphicsObject::embedQWidget() updateQWidgetEmbedPos(); - //update(); + _proxyWidget->setOpacity(1.0); _proxyWidget->setFlag(QGraphicsItem::ItemIgnoresParentOpacity); @@ -169,7 +169,6 @@ QRectF NodeGraphicsObject::boundingRect() const { AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); return geometry.boundingRect(_nodeId); - //return NodeGeometry(_nodeId, _graphModel, nodeScene()).boundingRect(); } void NodeGraphicsObject::setGeometryChanged() @@ -460,10 +459,9 @@ void NodeGraphicsObject::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { auto pos = event->pos(); - //NodeGeometry geometry(_nodeId, _graphModel, nodeScene()); AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); - if ((_graphModel.nodeFlags(_nodeId) | NodeFlag::Resizable) + if ((_graphModel.nodeFlags(_nodeId) & NodeFlag::Resizable) && geometry.resizeHandleRect(_nodeId).contains(QPoint(pos.x(), pos.y()))) { setCursor(QCursor(Qt::SizeFDiagCursor)); } else { diff --git a/src/NodeStyle.cpp b/src/NodeStyle.cpp index 0cfacf51..9702373e 100644 --- a/src/NodeStyle.cpp +++ b/src/NodeStyle.cpp @@ -2,13 +2,12 @@ #include "StyleCollection.hpp" -#include #include -#include #include using QtNodes::NodeStyle; +using namespace QtNodes::detail; inline void initResources() { @@ -50,114 +49,56 @@ void NodeStyle::setNodeStyle(QString jsonText) StyleCollection::setNodeStyle(style); } -#ifdef STYLE_DEBUG -#define NODE_STYLE_CHECK_UNDEFINED_VALUE(v, variable) \ - { \ - if (v.type() == QJsonValue::Undefined || v.type() == QJsonValue::Null) \ - qWarning() << "Undefined value for parameter:" << #variable; \ - } -#else -#define NODE_STYLE_CHECK_UNDEFINED_VALUE(v, variable) -#endif - -#define NODE_STYLE_READ_COLOR(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - NODE_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - if (valueRef.isArray()) { \ - auto colorArray = valueRef.toArray(); \ - std::vector rgb; \ - rgb.reserve(3); \ - for (auto it = colorArray.begin(); it != colorArray.end(); ++it) { \ - rgb.push_back((*it).toInt()); \ - } \ - variable = QColor(rgb[0], rgb[1], rgb[2]); \ - } else { \ - variable = QColor(valueRef.toString()); \ - } \ - } - -#define NODE_STYLE_WRITE_COLOR(values, variable) \ - { \ - values[#variable] = variable.name(); \ - } - -#define NODE_STYLE_READ_FLOAT(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - NODE_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - variable = valueRef.toDouble(); \ - } - -#define NODE_STYLE_WRITE_FLOAT(values, variable) \ - { \ - values[#variable] = variable; \ - } - -#define NODE_STYLE_READ_BOOL(values, variable) \ - { \ - auto valueRef = values[#variable]; \ - NODE_STYLE_CHECK_UNDEFINED_VALUE(valueRef, variable) \ - variable = valueRef.toBool(); \ - } - -#define NODE_STYLE_WRITE_BOOL(values, variable) \ - { \ - values[#variable] = variable; \ - } - void NodeStyle::loadJson(QJsonObject const &json) { - QJsonValue nodeStyleValues = json["NodeStyle"]; - - QJsonObject obj = nodeStyleValues.toObject(); - - NODE_STYLE_READ_COLOR(obj, NormalBoundaryColor); - NODE_STYLE_READ_COLOR(obj, SelectedBoundaryColor); - NODE_STYLE_READ_COLOR(obj, GradientColor0); - NODE_STYLE_READ_COLOR(obj, GradientColor1); - NODE_STYLE_READ_COLOR(obj, GradientColor2); - NODE_STYLE_READ_COLOR(obj, GradientColor3); - NODE_STYLE_READ_COLOR(obj, ShadowColor); - NODE_STYLE_READ_BOOL(obj, ShadowEnabled); - NODE_STYLE_READ_COLOR(obj, FontColor); - NODE_STYLE_READ_COLOR(obj, FontColorFaded); - NODE_STYLE_READ_COLOR(obj, ConnectionPointColor); - NODE_STYLE_READ_COLOR(obj, FilledConnectionPointColor); - NODE_STYLE_READ_COLOR(obj, WarningColor); - NODE_STYLE_READ_COLOR(obj, ErrorColor); - - NODE_STYLE_READ_FLOAT(obj, PenWidth); - NODE_STYLE_READ_FLOAT(obj, HoveredPenWidth); - NODE_STYLE_READ_FLOAT(obj, ConnectionPointDiameter); - - NODE_STYLE_READ_FLOAT(obj, Opacity); + QJsonObject obj = json["NodeStyle"].toObject(); + + readColor(obj, "NormalBoundaryColor", NormalBoundaryColor); + readColor(obj, "SelectedBoundaryColor", SelectedBoundaryColor); + readColor(obj, "GradientColor0", GradientColor0); + readColor(obj, "GradientColor1", GradientColor1); + readColor(obj, "GradientColor2", GradientColor2); + readColor(obj, "GradientColor3", GradientColor3); + readColor(obj, "ShadowColor", ShadowColor); + readBool(obj, "ShadowEnabled", ShadowEnabled); + readColor(obj, "FontColor", FontColor); + readColor(obj, "FontColorFaded", FontColorFaded); + readColor(obj, "ConnectionPointColor", ConnectionPointColor); + readColor(obj, "FilledConnectionPointColor", FilledConnectionPointColor); + readColor(obj, "WarningColor", WarningColor); + readColor(obj, "ErrorColor", ErrorColor); + + readFloat(obj, "PenWidth", PenWidth); + readFloat(obj, "HoveredPenWidth", HoveredPenWidth); + readFloat(obj, "ConnectionPointDiameter", ConnectionPointDiameter); + + readFloat(obj, "Opacity", Opacity); } QJsonObject NodeStyle::toJson() const { QJsonObject obj; - NODE_STYLE_WRITE_COLOR(obj, NormalBoundaryColor); - NODE_STYLE_WRITE_COLOR(obj, SelectedBoundaryColor); - NODE_STYLE_WRITE_COLOR(obj, GradientColor0); - NODE_STYLE_WRITE_COLOR(obj, GradientColor1); - NODE_STYLE_WRITE_COLOR(obj, GradientColor2); - NODE_STYLE_WRITE_COLOR(obj, GradientColor3); - NODE_STYLE_WRITE_COLOR(obj, ShadowColor); - NODE_STYLE_WRITE_BOOL(obj, ShadowEnabled); - NODE_STYLE_WRITE_COLOR(obj, FontColor); - NODE_STYLE_WRITE_COLOR(obj, FontColorFaded); - NODE_STYLE_WRITE_COLOR(obj, ConnectionPointColor); - NODE_STYLE_WRITE_COLOR(obj, FilledConnectionPointColor); - NODE_STYLE_WRITE_COLOR(obj, WarningColor); - NODE_STYLE_WRITE_COLOR(obj, ErrorColor); - - NODE_STYLE_WRITE_FLOAT(obj, PenWidth); - NODE_STYLE_WRITE_FLOAT(obj, HoveredPenWidth); - NODE_STYLE_WRITE_FLOAT(obj, ConnectionPointDiameter); - - NODE_STYLE_WRITE_FLOAT(obj, Opacity); + writeColor(obj, "NormalBoundaryColor", NormalBoundaryColor); + writeColor(obj, "SelectedBoundaryColor", SelectedBoundaryColor); + writeColor(obj, "GradientColor0", GradientColor0); + writeColor(obj, "GradientColor1", GradientColor1); + writeColor(obj, "GradientColor2", GradientColor2); + writeColor(obj, "GradientColor3", GradientColor3); + writeColor(obj, "ShadowColor", ShadowColor); + writeBool(obj, "ShadowEnabled", ShadowEnabled); + writeColor(obj, "FontColor", FontColor); + writeColor(obj, "FontColorFaded", FontColorFaded); + writeColor(obj, "ConnectionPointColor", ConnectionPointColor); + writeColor(obj, "FilledConnectionPointColor", FilledConnectionPointColor); + writeColor(obj, "WarningColor", WarningColor); + writeColor(obj, "ErrorColor", ErrorColor); + + writeFloat(obj, "PenWidth", PenWidth); + writeFloat(obj, "HoveredPenWidth", HoveredPenWidth); + writeFloat(obj, "ConnectionPointDiameter", ConnectionPointDiameter); + + writeFloat(obj, "Opacity", Opacity); QJsonObject root; root["NodeStyle"] = obj; From 6ac178bca06d53c4a410cf2a80be6fa4f6e1bb94 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 05:21:15 +0000 Subject: [PATCH 11/26] Remove dead code, unused includes, and stale forward declarations - Remove phantom forward declarations for non-existent classes: NodeDataModel, ConnectionGeometry, GraphModel, NodeGeometry - Remove redundant forward declarations where the header is already included (QPainter, ConnectionId, DataFlowGraphModel, etc.) - Remove unused forward declarations (NodeStyle, NodeState, StyleCollection, NodeDelegateModel, NodeConnectionInteraction, etc.) - Remove dead methods: connectionRequiredPort(), nodePortScenePosition(), connectionEndScenePosition() in NodeConnectionInteraction; addGraphicsEffect() in ConnectionGraphicsObject - Remove #if 0 blocks in NodeDelegateModelRegistry.hpp (old registerModel overloads, type converter API) - Remove unused includes: QUuid (3 headers), QUuidStdHash, iostream, QFile, QMessageBox, QMargins, QPointF, QIcon, QJsonObject, NodeConnectionInteraction, NodeState, ConnectionGraphicsObject - Remove duplicate includes: NodeState.hpp (NodeGraphicsObject.hpp), QColor (NodeDelegateModel.hpp) - Remove unused using declarations and variables: DataFlowGraphModel, NodeConnectionInteraction, NodeDataType, QVariant result in setPortData https://claude.ai/code/session_015aZAxLSpakeTpF7zaq6D2F --- .../internal/AbstractConnectionPainter.hpp | 2 - .../QtNodes/internal/AbstractNodePainter.hpp | 3 -- .../QtNodes/internal/BasicGraphicsScene.hpp | 5 --- .../internal/ConnectionGraphicsObject.hpp | 2 - include/QtNodes/internal/ConnectionState.hpp | 4 -- .../internal/DataFlowGraphicsScene.hpp | 2 - .../internal/DefaultConnectionPainter.hpp | 2 - .../DefaultHorizontalNodeGeometry.hpp | 1 - .../QtNodes/internal/DefaultNodePainter.hpp | 4 -- .../internal/DefaultVerticalNodeGeometry.hpp | 1 - .../internal/NodeConnectionInteraction.hpp | 6 --- .../QtNodes/internal/NodeDelegateModel.hpp | 3 -- .../internal/NodeDelegateModelRegistry.hpp | 39 ------------------- .../QtNodes/internal/NodeGraphicsObject.hpp | 3 -- include/QtNodes/internal/NodeGroup.hpp | 8 ---- src/AbstractNodeGeometry.cpp | 2 - src/BasicGraphicsScene.cpp | 1 - src/ConnectionGraphicsObject.cpp | 9 ----- src/ConnectionState.cpp | 1 - src/DataFlowGraphModel.cpp | 2 - src/NodeConnectionInteraction.cpp | 22 ----------- src/NodeDelegateModelRegistry.cpp | 4 -- src/NodeGroup.cpp | 3 -- 23 files changed, 129 deletions(-) diff --git a/include/QtNodes/internal/AbstractConnectionPainter.hpp b/include/QtNodes/internal/AbstractConnectionPainter.hpp index f838a0b9..fd81a1d4 100644 --- a/include/QtNodes/internal/AbstractConnectionPainter.hpp +++ b/include/QtNodes/internal/AbstractConnectionPainter.hpp @@ -4,8 +4,6 @@ #include "Export.hpp" -class QPainter; - namespace QtNodes { class ConnectionGraphicsObject; diff --git a/include/QtNodes/internal/AbstractNodePainter.hpp b/include/QtNodes/internal/AbstractNodePainter.hpp index cd4c7d63..aac64897 100644 --- a/include/QtNodes/internal/AbstractNodePainter.hpp +++ b/include/QtNodes/internal/AbstractNodePainter.hpp @@ -4,12 +4,9 @@ #include "Export.hpp" -class QPainter; - namespace QtNodes { class NodeGraphicsObject; -class NodeDataModel; /// Class enables custom painting. class NODE_EDITOR_PUBLIC AbstractNodePainter diff --git a/include/QtNodes/internal/BasicGraphicsScene.hpp b/include/QtNodes/internal/BasicGraphicsScene.hpp index 64b11d9a..ff9e26d0 100644 --- a/include/QtNodes/internal/BasicGraphicsScene.hpp +++ b/include/QtNodes/internal/BasicGraphicsScene.hpp @@ -7,10 +7,7 @@ #include "Export.hpp" #include "GroupGraphicsObject.hpp" #include "NodeGroup.hpp" -#include "QUuidStdHash.hpp" #include "UndoCommands.hpp" -#include -#include #include #include @@ -28,12 +25,10 @@ class AbstractGraphModel; class AbstractNodePainter; class ConnectionGraphicsObject; class NodeGraphicsObject; -class NodeStyle; class DeleteCommand; class CopyCommand; class NodeGroup; class GroupGraphicsObject; -struct ConnectionId; /// An instance of QGraphicsScene , holds connections and nodes. class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene diff --git a/include/QtNodes/internal/ConnectionGraphicsObject.hpp b/include/QtNodes/internal/ConnectionGraphicsObject.hpp index 435972ef..3f883e07 100644 --- a/include/QtNodes/internal/ConnectionGraphicsObject.hpp +++ b/include/QtNodes/internal/ConnectionGraphicsObject.hpp @@ -90,8 +90,6 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject private: void initializePosition(); - void addGraphicsEffect(); - std::pair pointsC1C2Horizontal() const; std::pair pointsC1C2Vertical() const; diff --git a/include/QtNodes/internal/ConnectionState.hpp b/include/QtNodes/internal/ConnectionState.hpp index d1373826..6d1a5fcb 100644 --- a/include/QtNodes/internal/ConnectionState.hpp +++ b/include/QtNodes/internal/ConnectionState.hpp @@ -1,13 +1,9 @@ #pragma once -#include - #include "Export.hpp" #include "Definitions.hpp" -class QPointF; - namespace QtNodes { class ConnectionGraphicsObject; diff --git a/include/QtNodes/internal/DataFlowGraphicsScene.hpp b/include/QtNodes/internal/DataFlowGraphicsScene.hpp index 30bcc949..e928f830 100644 --- a/include/QtNodes/internal/DataFlowGraphicsScene.hpp +++ b/include/QtNodes/internal/DataFlowGraphicsScene.hpp @@ -1,10 +1,8 @@ #pragma once #include "BasicGraphicsScene.hpp" -#include "ConnectionGraphicsObject.hpp" #include "DataFlowGraphModel.hpp" #include "Export.hpp" -#include "NodeConnectionInteraction.hpp" namespace QtNodes { diff --git a/include/QtNodes/internal/DefaultConnectionPainter.hpp b/include/QtNodes/internal/DefaultConnectionPainter.hpp index 42e79f1b..f4c2913f 100644 --- a/include/QtNodes/internal/DefaultConnectionPainter.hpp +++ b/include/QtNodes/internal/DefaultConnectionPainter.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -10,7 +9,6 @@ namespace QtNodes { -class ConnectionGeometry; class ConnectionGraphicsObject; class DefaultConnectionPainter : public AbstractConnectionPainter diff --git a/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp b/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp index 33367e10..22c34b1e 100644 --- a/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp +++ b/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp @@ -7,7 +7,6 @@ namespace QtNodes { class AbstractGraphModel; -class BasicGraphicsScene; class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public AbstractNodeGeometry { diff --git a/include/QtNodes/internal/DefaultNodePainter.hpp b/include/QtNodes/internal/DefaultNodePainter.hpp index 3c763e5b..be4d1eb8 100644 --- a/include/QtNodes/internal/DefaultNodePainter.hpp +++ b/include/QtNodes/internal/DefaultNodePainter.hpp @@ -9,12 +9,8 @@ namespace QtNodes { -class BasicGraphicsScene; class GraphicsView; -class GraphModel; -class NodeGeometry; class NodeGraphicsObject; -class NodeState; /// @ Lightweight class incapsulating paint code. /// diff --git a/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp b/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp index ce4dd9f1..05cdb1a9 100644 --- a/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp +++ b/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp @@ -7,7 +7,6 @@ namespace QtNodes { class AbstractGraphModel; -class BasicGraphicsScene; class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public AbstractNodeGeometry { diff --git a/include/QtNodes/internal/NodeConnectionInteraction.hpp b/include/QtNodes/internal/NodeConnectionInteraction.hpp index f71ae97e..4fe589d5 100644 --- a/include/QtNodes/internal/NodeConnectionInteraction.hpp +++ b/include/QtNodes/internal/NodeConnectionInteraction.hpp @@ -59,12 +59,6 @@ class NodeConnectionInteraction NodeGraphicsObject &nodeGraphicsObject() { return _ngo; } private: - PortType connectionRequiredPort() const; - - QPointF connectionEndScenePosition(PortType) const; - - QPointF nodePortScenePosition(PortType portType, PortIndex portIndex) const; - PortIndex nodePortIndexUnderScenePoint(PortType portType, QPointF const &p) const; private: diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index de0cc073..5d1b776f 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -14,7 +14,6 @@ #include "NodeData.hpp" #include "NodeStyle.hpp" #include "Serializable.hpp" -#include namespace QtNodes { @@ -49,8 +48,6 @@ enum class NodeProcessingStatus : int { Partial = 6, ///< Computation finished incompletely; only partial results are available. }; -class StyleCollection; - /** * The class wraps Node-specific data operations and propagates it to * the nesting DataFlowGraphModel which is a subclass of diff --git a/include/QtNodes/internal/NodeDelegateModelRegistry.hpp b/include/QtNodes/internal/NodeDelegateModelRegistry.hpp index 3ab66062..86a462f5 100644 --- a/include/QtNodes/internal/NodeDelegateModelRegistry.hpp +++ b/include/QtNodes/internal/NodeDelegateModelRegistry.hpp @@ -27,8 +27,6 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry using RegisteredModelsCategoryMap = std::unordered_map; using CategoriesSet = std::set; - //using RegisteredTypeConvertersMap = std::map; - NodeDelegateModelRegistry() = default; ~NodeDelegateModelRegistry() = default; @@ -68,33 +66,6 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry } -#if 0 - template - void - registerModel(RegistryItemCreator creator, - QString const& category = "Nodes") - { - registerModel(std::move(creator), category); - } - - - template - void - registerModel(QString const& category, ModelCreator&& creator) - { - registerModel(std::forward(creator), category); - } - - - void - registerTypeConverter(TypeConverterId const& id, - TypeConverter typeConverter) - { - _registeredTypeConverters[id] = std::move(typeConverter); - } - -#endif - std::unique_ptr create(QString const &modelName); RegisteredModelCreatorsMap const ®isteredModelCreators() const; @@ -103,12 +74,6 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry CategoriesSet const &categories() const; -#if 0 - TypeConverter - getTypeConverter(NodeDataType const& d1, - NodeDataType const& d2) const; -#endif - private: RegisteredModelsCategoryMap _registeredModelsCategory; @@ -116,10 +81,6 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry RegisteredModelCreatorsMap _registeredItemCreators; -#if 0 - RegisteredTypeConvertersMap _registeredTypeConverters; -#endif - private: // If the registered ModelType class has the static member method // `static QString Name();`, use it. Otherwise use the non-static diff --git a/include/QtNodes/internal/NodeGraphicsObject.hpp b/include/QtNodes/internal/NodeGraphicsObject.hpp index e8cd640f..bd707291 100644 --- a/include/QtNodes/internal/NodeGraphicsObject.hpp +++ b/include/QtNodes/internal/NodeGraphicsObject.hpp @@ -5,11 +5,9 @@ #include "NodeState.hpp" #include #include -#include #include #include "Export.hpp" -#include "NodeState.hpp" class QGraphicsProxyWidget; @@ -19,7 +17,6 @@ class BasicGraphicsScene; class AbstractGraphModel; class GraphicsView; class NodeGroup; -class NodeDelegateModel; class GroupGraphicsObject; class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject diff --git a/include/QtNodes/internal/NodeGroup.hpp b/include/QtNodes/internal/NodeGroup.hpp index f9f6c5ab..de27f8d7 100644 --- a/include/QtNodes/internal/NodeGroup.hpp +++ b/include/QtNodes/internal/NodeGroup.hpp @@ -4,21 +4,13 @@ #include #include -#include "DataFlowGraphModel.hpp" #include "Definitions.hpp" #include "Export.hpp" #include "GroupGraphicsObject.hpp" -#include "NodeConnectionInteraction.hpp" -#include "NodeState.hpp" namespace QtNodes { -class DataFlowGraphModel; -class GroupGraphicsObject; -class NodeState; -class NodeConnectionInteraction; class NodeGraphicsObject; -struct ConnectionId; /** * @brief The NodeGroup class defines a controller for node groups. It is diff --git a/src/AbstractNodeGeometry.cpp b/src/AbstractNodeGeometry.cpp index 8240ff6c..0b20bccc 100644 --- a/src/AbstractNodeGeometry.cpp +++ b/src/AbstractNodeGeometry.cpp @@ -4,8 +4,6 @@ #include "ConnectionIdUtils.hpp" #include "StyleCollection.hpp" -#include - #include namespace QtNodes { diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index 724baa52..4b6b5b4c 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -33,7 +33,6 @@ #include #include -#include #include #include #include diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp index 931f7631..02f90c3b 100644 --- a/src/ConnectionGraphicsObject.cpp +++ b/src/ConnectionGraphicsObject.cpp @@ -353,15 +353,6 @@ std::pair ConnectionGraphicsObject::pointsC1C2() const throw std::logic_error("Unreachable code after switch statement"); } -void ConnectionGraphicsObject::addGraphicsEffect() -{ - auto effect = new QGraphicsBlurEffect; - - effect->setBlurRadius(5); - setGraphicsEffect(effect); - -} - std::pair ConnectionGraphicsObject::pointsC1C2Horizontal() const { double const defaultOffset = 200; diff --git a/src/ConnectionState.cpp b/src/ConnectionState.cpp index 5b411a9b..b279f77c 100644 --- a/src/ConnectionState.cpp +++ b/src/ConnectionState.cpp @@ -5,7 +5,6 @@ #include "NodeGraphicsObject.hpp" #include -#include namespace QtNodes { diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index 958ab0c8..4465558b 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -436,8 +436,6 @@ bool DataFlowGraphModel::setPortData( { Q_UNUSED(nodeId); - QVariant result; - auto it = _models.find(nodeId); if (it == _models.end()) return false; diff --git a/src/NodeConnectionInteraction.cpp b/src/NodeConnectionInteraction.cpp index c43fe36f..e44cad1c 100644 --- a/src/NodeConnectionInteraction.cpp +++ b/src/NodeConnectionInteraction.cpp @@ -104,28 +104,6 @@ bool NodeConnectionInteraction::disconnect(PortType portToDisconnect) const return true; } -// ------------------ util functions below - -PortType NodeConnectionInteraction::connectionRequiredPort() const -{ - auto const &state = _cgo.connectionState(); - - return state.requiredPort(); -} - -QPointF NodeConnectionInteraction::nodePortScenePosition(PortType portType, - PortIndex portIndex) const -{ - AbstractNodeGeometry &geometry = _scene.nodeGeometry(); - - QPointF p = geometry.portScenePosition(_ngo.nodeId(), - portType, - portIndex, - _ngo.sceneTransform()); - - return p; -} - PortIndex NodeConnectionInteraction::nodePortIndexUnderScenePoint(PortType portType, QPointF const &scenePoint) const { diff --git a/src/NodeDelegateModelRegistry.cpp b/src/NodeDelegateModelRegistry.cpp index 11a1f84a..1698c753 100644 --- a/src/NodeDelegateModelRegistry.cpp +++ b/src/NodeDelegateModelRegistry.cpp @@ -1,9 +1,5 @@ #include "NodeDelegateModelRegistry.hpp" -#include -#include - -using QtNodes::NodeDataType; using QtNodes::NodeDelegateModel; using QtNodes::NodeDelegateModelRegistry; diff --git a/src/NodeGroup.cpp b/src/NodeGroup.cpp index 1c214973..a414d935 100644 --- a/src/NodeGroup.cpp +++ b/src/NodeGroup.cpp @@ -1,14 +1,11 @@ #include "NodeGroup.hpp" #include "ConnectionIdUtils.hpp" -#include "NodeConnectionInteraction.hpp" #include #include #include -using QtNodes::DataFlowGraphModel; using QtNodes::GroupGraphicsObject; -using QtNodes::NodeConnectionInteraction; using QtNodes::NodeGraphicsObject; using QtNodes::NodeGroup; using QtNodes::NodeId; From f61ea0d7234ce6d8437f9cf1fae5214b4232fb88 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 05:22:53 +0000 Subject: [PATCH 12/26] Fix broken NodeGeometry forwarding header and remove unused includes - Fix include/QtNodes/NodeGeometry: was pointing to non-existent internal/NodeGeometry.hpp, now correctly points to internal/AbstractNodeGeometry.hpp - Remove unused QVariant includes from QUuidStdHash.hpp and QStringStdHash.hpp - Remove unused QObject include from Style.hpp https://claude.ai/code/session_015aZAxLSpakeTpF7zaq6D2F --- include/QtNodes/NodeGeometry | 3 +-- include/QtNodes/internal/QStringStdHash.hpp | 1 - include/QtNodes/internal/QUuidStdHash.hpp | 1 - include/QtNodes/internal/Style.hpp | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/include/QtNodes/NodeGeometry b/include/QtNodes/NodeGeometry index 46a70a6d..c4265e1d 100644 --- a/include/QtNodes/NodeGeometry +++ b/include/QtNodes/NodeGeometry @@ -1,2 +1 @@ -#include "internal/NodeGeometry.hpp" - +#include "internal/AbstractNodeGeometry.hpp" diff --git a/include/QtNodes/internal/QStringStdHash.hpp b/include/QtNodes/internal/QStringStdHash.hpp index 617d9b50..bbf7b845 100644 --- a/include/QtNodes/internal/QStringStdHash.hpp +++ b/include/QtNodes/internal/QStringStdHash.hpp @@ -9,7 +9,6 @@ #include #include -#include namespace std { template<> diff --git a/include/QtNodes/internal/QUuidStdHash.hpp b/include/QtNodes/internal/QUuidStdHash.hpp index 224bc9f4..63ff51a5 100644 --- a/include/QtNodes/internal/QUuidStdHash.hpp +++ b/include/QtNodes/internal/QUuidStdHash.hpp @@ -3,7 +3,6 @@ #include #include -#include namespace std { template<> diff --git a/include/QtNodes/internal/Style.hpp b/include/QtNodes/internal/Style.hpp index 262bdfad..08af6300 100644 --- a/include/QtNodes/internal/Style.hpp +++ b/include/QtNodes/internal/Style.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include namespace QtNodes { From 864769b95bd3bcad9fcad5342dd6a6dfa1abe650 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 05:27:56 +0000 Subject: [PATCH 13/26] Refactor: extract geometry base class, unify connection control points 1. Extract DefaultNodeGeometryBase from duplicated geometry classes DefaultHorizontalNodeGeometry and DefaultVerticalNodeGeometry shared ~120 lines of identical code (constructor, boundingRect, size, captionRect, portTextRect, maxPortsTextAdvance, maxPortsExtent, and all 4 member variables). Extract these into a new intermediate base class DefaultNodeGeometryBase, leaving only orientation-specific methods in the subclasses. 2. Unify pointsC1C2Horizontal/pointsC1C2Vertical These two methods had identical algorithmic structure with X/Y axes swapped. Replace with a single computeControlPoints(Qt::Orientation) method that parameterizes by axis. 3. Extract selectedItemsOfType template helper selectedNodes() and selectedGroups() shared the same iteration pattern over QGraphicsScene::selectedItems() with qgraphicsitem_cast. Extract into a local template function. https://claude.ai/code/session_015aZAxLSpakeTpF7zaq6D2F --- CMakeLists.txt | 1 + .../internal/ConnectionGraphicsObject.hpp | 4 +- .../DefaultHorizontalNodeGeometry.hpp | 28 +---- .../internal/DefaultNodeGeometryBase.hpp | 45 +++++++ .../internal/DefaultVerticalNodeGeometry.hpp | 27 +---- src/BasicGraphicsScene.cpp | 35 +++--- src/ConnectionGraphicsObject.cpp | 84 +++++-------- src/DefaultHorizontalNodeGeometry.cpp | 102 +--------------- src/DefaultNodeGeometryBase.cpp | 112 ++++++++++++++++++ src/DefaultVerticalNodeGeometry.cpp | 104 +--------------- 10 files changed, 214 insertions(+), 328 deletions(-) create mode 100644 include/QtNodes/internal/DefaultNodeGeometryBase.hpp create mode 100644 src/DefaultNodeGeometryBase.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d4f8a2d..5cc0e7c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ set(CPP_SOURCE_FILES src/DataFlowGraphModel.cpp src/DataFlowGraphicsScene.cpp src/DefaultConnectionPainter.cpp + src/DefaultNodeGeometryBase.cpp src/DefaultHorizontalNodeGeometry.cpp src/DefaultNodePainter.cpp src/DefaultVerticalNodeGeometry.cpp diff --git a/include/QtNodes/internal/ConnectionGraphicsObject.hpp b/include/QtNodes/internal/ConnectionGraphicsObject.hpp index 3f883e07..7371391a 100644 --- a/include/QtNodes/internal/ConnectionGraphicsObject.hpp +++ b/include/QtNodes/internal/ConnectionGraphicsObject.hpp @@ -90,9 +90,7 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject private: void initializePosition(); - std::pair pointsC1C2Horizontal() const; - - std::pair pointsC1C2Vertical() const; + std::pair computeControlPoints(Qt::Orientation orientation) const; void rebuildCachedGeometry() const; static constexpr int k_path_sample_count = 61; diff --git a/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp b/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp index 22c34b1e..66abdec0 100644 --- a/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp +++ b/include/QtNodes/internal/DefaultHorizontalNodeGeometry.hpp @@ -1,23 +1,17 @@ #pragma once -#include "AbstractNodeGeometry.hpp" - -#include +#include "DefaultNodeGeometryBase.hpp" namespace QtNodes { class AbstractGraphModel; -class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public AbstractNodeGeometry +class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public DefaultNodeGeometryBase { public: DefaultHorizontalNodeGeometry(AbstractGraphModel &graphModel); public: - QRectF boundingRect(NodeId const nodeId) const override; - - QSize size(NodeId const nodeId) const override; - void recomputeSize(NodeId const nodeId) const override; QPointF portPosition(NodeId const nodeId, @@ -29,31 +23,13 @@ class NODE_EDITOR_PUBLIC DefaultHorizontalNodeGeometry : public AbstractNodeGeom PortIndex const PortIndex) const override; QPointF captionPosition(NodeId const nodeId) const override; - QRectF captionRect(NodeId const nodeId) const override; - QPointF widgetPosition(NodeId const nodeId) const override; QRect resizeHandleRect(NodeId const nodeId) const override; private: - QRectF portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const; - /// Finds max number of ports and multiplies by (a port height + interval) unsigned int maxVerticalPortsExtent(NodeId const nodeId) const; - - unsigned int maxPortsTextAdvance(NodeId const nodeId, PortType const portType) const; - -private: - // Some variables are mutable because we need to change drawing - // metrics corresponding to fontMetrics but this doesn't change - // constness of the Node. - - mutable unsigned int _portSize; - unsigned int _portSpasing; - mutable QFontMetrics _fontMetrics; - mutable QFontMetrics _boldFontMetrics; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultNodeGeometryBase.hpp b/include/QtNodes/internal/DefaultNodeGeometryBase.hpp new file mode 100644 index 00000000..704285ca --- /dev/null +++ b/include/QtNodes/internal/DefaultNodeGeometryBase.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "AbstractNodeGeometry.hpp" + +#include + +namespace QtNodes { + +class AbstractGraphModel; + +/** + * Shared base for DefaultHorizontalNodeGeometry and DefaultVerticalNodeGeometry. + * + * Contains common member variables and methods that are identical in both + * orientations: boundingRect, size, captionRect, portTextRect, + * maxPortsTextAdvance, and maxPortsExtent. + */ +class NODE_EDITOR_PUBLIC DefaultNodeGeometryBase : public AbstractNodeGeometry +{ +public: + DefaultNodeGeometryBase(AbstractGraphModel &graphModel); + + QRectF boundingRect(NodeId const nodeId) const override; + + QSize size(NodeId const nodeId) const override; + + QRectF captionRect(NodeId const nodeId) const override; + +protected: + QRectF portTextRect(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const; + + unsigned int maxPortsExtent(NodeId const nodeId) const; + + unsigned int maxPortsTextAdvance(NodeId const nodeId, PortType const portType) const; + +protected: + mutable unsigned int _portSize; + unsigned int _portSpasing; + mutable QFontMetrics _fontMetrics; + mutable QFontMetrics _boldFontMetrics; +}; + +} // namespace QtNodes diff --git a/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp b/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp index 05cdb1a9..2fda46fd 100644 --- a/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp +++ b/include/QtNodes/internal/DefaultVerticalNodeGeometry.hpp @@ -1,23 +1,17 @@ #pragma once -#include "AbstractNodeGeometry.hpp" - -#include +#include "DefaultNodeGeometryBase.hpp" namespace QtNodes { class AbstractGraphModel; -class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public AbstractNodeGeometry +class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public DefaultNodeGeometryBase { public: DefaultVerticalNodeGeometry(AbstractGraphModel &graphModel); public: - QRectF boundingRect(NodeId const nodeId) const override; - - QSize size(NodeId const nodeId) const override; - void recomputeSize(NodeId const nodeId) const override; QPointF portPosition(NodeId const nodeId, @@ -30,32 +24,15 @@ class NODE_EDITOR_PUBLIC DefaultVerticalNodeGeometry : public AbstractNodeGeomet QPointF captionPosition(NodeId const nodeId) const override; - QRectF captionRect(NodeId const nodeId) const override; - QPointF widgetPosition(NodeId const nodeId) const override; QRect resizeHandleRect(NodeId const nodeId) const override; private: - QRectF portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const; /// Finds unsigned int maxHorizontalPortsExtent(NodeId const nodeId) const; - unsigned int maxPortsTextAdvance(NodeId const nodeId, PortType const portType) const; - unsigned int portCaptionsHeight(NodeId const nodeId, PortType const portType) const; - -private: - // Some variables are mutable because we need to change drawing - // metrics corresponding to fontMetrics but this doesn't change - // constness of the Node. - - mutable unsigned int _portSize; - unsigned int _portSpasing; - mutable QFontMetrics _fontMetrics; - mutable QFontMetrics _boldFontMetrics; }; } // namespace QtNodes diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index 4b6b5b4c..850b27fc 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -512,43 +512,36 @@ std::weak_ptr BasicGraphicsScene::createGroup(std::vector BasicGraphicsScene::selectedNodes() const +namespace { +template +std::vector selectedItemsOfType(QGraphicsScene const *scene) { - QList graphicsItems = selectedItems(); + QList graphicsItems = scene->selectedItems(); - std::vector result; + std::vector result; result.reserve(graphicsItems.size()); for (QGraphicsItem *item : graphicsItems) { - auto ngo = qgraphicsitem_cast(item); - - if (ngo) { - result.push_back(ngo); + if (auto typed = qgraphicsitem_cast(item)) { + result.push_back(typed); } } return result; } +} // namespace + +std::vector BasicGraphicsScene::selectedNodes() const +{ + return selectedItemsOfType(this); +} std::vector BasicGraphicsScene::selectedGroups() const { if (!_groupingEnabled) return {}; - QList graphicsItems = selectedItems(); - - std::vector result; - result.reserve(graphicsItems.size()); - - for (QGraphicsItem *item : graphicsItems) { - auto ngo = qgraphicsitem_cast(item); - - if (ngo) { - result.push_back(ngo); - } - } - - return result; + return selectedItemsOfType(this); } void BasicGraphicsScene::addNodeToGroup(NodeId nodeId, GroupId groupId) diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp index 02f90c3b..9abf21fb 100644 --- a/src/ConnectionGraphicsObject.cpp +++ b/src/ConnectionGraphicsObject.cpp @@ -340,77 +340,53 @@ void ConnectionGraphicsObject::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) std::pair ConnectionGraphicsObject::pointsC1C2() const { - switch (nodeScene()->orientation()) { - case Qt::Horizontal: - return pointsC1C2Horizontal(); - break; - - case Qt::Vertical: - return pointsC1C2Vertical(); - break; - } - - throw std::logic_error("Unreachable code after switch statement"); + return computeControlPoints(nodeScene()->orientation()); } -std::pair ConnectionGraphicsObject::pointsC1C2Horizontal() const +std::pair ConnectionGraphicsObject::computeControlPoints( + Qt::Orientation orientation) const { double const defaultOffset = 200; - double xDistance = _in.x() - _out.x(); - - double horizontalOffset = qMin(defaultOffset, std::abs(xDistance)); - - double verticalOffset = 0; - - double ratioX = 0.5; - - if (xDistance <= 0) { - double yDistance = _in.y() - _out.y() + 20; - - double vector = yDistance < 0 ? -1.0 : 1.0; - - verticalOffset = qMin(defaultOffset, std::abs(yDistance)) * vector; - - ratioX = 1.0; - } - - horizontalOffset *= ratioX; - - QPointF c1(_out.x() + horizontalOffset, _out.y() + verticalOffset); - - QPointF c2(_in.x() - horizontalOffset, _in.y() - verticalOffset); - - return std::make_pair(c1, c2); -} - -std::pair ConnectionGraphicsObject::pointsC1C2Vertical() const -{ - double const defaultOffset = 200; + // In horizontal mode the primary axis is X; in vertical mode it is Y. + auto primary = [&](QPointF const &p) { + return orientation == Qt::Horizontal ? p.x() : p.y(); + }; + auto secondary = [&](QPointF const &p) { + return orientation == Qt::Horizontal ? p.y() : p.x(); + }; - double yDistance = _in.y() - _out.y(); + double primaryDistance = primary(_in) - primary(_out); - double verticalOffset = qMin(defaultOffset, std::abs(yDistance)); + double primaryOffset = qMin(defaultOffset, std::abs(primaryDistance)); - double horizontalOffset = 0; + double secondaryOffset = 0; - double ratioY = 0.5; + double ratio = 0.5; - if (yDistance <= 0) { - double xDistance = _in.x() - _out.x() + 20; + if (primaryDistance <= 0) { + double secDistance = secondary(_in) - secondary(_out) + 20; - double vector = xDistance < 0 ? -1.0 : 1.0; + double vector = secDistance < 0 ? -1.0 : 1.0; - horizontalOffset = qMin(defaultOffset, std::abs(xDistance)) * vector; + secondaryOffset = qMin(defaultOffset, std::abs(secDistance)) * vector; - ratioY = 1.0; + ratio = 1.0; } - verticalOffset *= ratioY; + primaryOffset *= ratio; - QPointF c1(_out.x() + horizontalOffset, _out.y() + verticalOffset); + double hOff, vOff; + if (orientation == Qt::Horizontal) { + hOff = primaryOffset; + vOff = secondaryOffset; + } else { + hOff = secondaryOffset; + vOff = primaryOffset; + } - QPointF c2(_in.x() - horizontalOffset, _in.y() - verticalOffset); + QPointF c1(_out.x() + hOff, _out.y() + vOff); + QPointF c2(_in.x() - hOff, _in.y() - vOff); return std::make_pair(c1, c2); } diff --git a/src/DefaultHorizontalNodeGeometry.cpp b/src/DefaultHorizontalNodeGeometry.cpp index eebf1a69..1e63a384 100644 --- a/src/DefaultHorizontalNodeGeometry.cpp +++ b/src/DefaultHorizontalNodeGeometry.cpp @@ -1,48 +1,16 @@ #include "DefaultHorizontalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" -#include "ConnectionIdUtils.hpp" -#include "NodeRenderingUtils.hpp" -#include "NodeData.hpp" #include #include #include -#include - namespace QtNodes { DefaultHorizontalNodeGeometry::DefaultHorizontalNodeGeometry(AbstractGraphModel &graphModel) - : AbstractNodeGeometry(graphModel) - , _portSize(20) - , _portSpasing(10) - , _fontMetrics(QFont()) - , _boldFontMetrics(QFont()) -{ - QFont f; - f.setBold(true); - _boldFontMetrics = QFontMetrics(f); - - _portSize = _fontMetrics.height(); -} - -QRectF DefaultHorizontalNodeGeometry::boundingRect(NodeId const nodeId) const -{ - QSize s = size(nodeId); - std::optional fallback_style; - NodeStyle const &style = node_rendering::resolved_node_style(_graphModel, nodeId, fallback_style); - QMarginsF const margins = node_rendering::node_visual_margins(style.ShadowEnabled); - - QRectF r(QPointF(0, 0), s); - - return r.marginsAdded(margins); -} - -QSize DefaultHorizontalNodeGeometry::size(NodeId const nodeId) const -{ - return _graphModel.nodeData(nodeId, NodeRole::Size); -} + : DefaultNodeGeometryBase(graphModel) +{} void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const { @@ -149,16 +117,6 @@ QPointF DefaultHorizontalNodeGeometry::portTextPosition(NodeId const nodeId, return p; } -QRectF DefaultHorizontalNodeGeometry::captionRect(NodeId const nodeId) const -{ - if (!_graphModel.nodeData(nodeId, NodeRole::CaptionVisible)) - return QRect(); - - QString name = _graphModel.nodeData(nodeId, NodeRole::Caption); - - return _boldFontMetrics.boundingRect(name); -} - QPointF DefaultHorizontalNodeGeometry::captionPosition(NodeId const nodeId) const { QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); @@ -195,63 +153,9 @@ QRect DefaultHorizontalNodeGeometry::resizeHandleRect(NodeId const nodeId) const return QRect(size.width() - _portSpasing, size.height() - _portSpasing, rectSize, rectSize); } -QRectF DefaultHorizontalNodeGeometry::portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const -{ - QString s; - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - s = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType); - - s = portData.value().name; - } - - return _fontMetrics.boundingRect(s); -} - unsigned int DefaultHorizontalNodeGeometry::maxVerticalPortsExtent(NodeId const nodeId) const { - PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); - - PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); - - unsigned int maxNumOfEntries = std::max(nInPorts, nOutPorts); - unsigned int step = _portSize + _portSpasing; - - return step * maxNumOfEntries; -} - -unsigned int DefaultHorizontalNodeGeometry::maxPortsTextAdvance(NodeId const nodeId, - PortType const portType) const -{ - unsigned int width = 0; - - size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)).toUInt(); - - for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { - QString name; - - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - name = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - NodeDataType portData = _graphModel.portData(nodeId, - portType, - portIndex, - PortRole::DataType); - - name = portData.name; - } - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - width = std::max(unsigned(_fontMetrics.horizontalAdvance(name)), width); -#else - width = std::max(unsigned(_fontMetrics.width(name)), width); -#endif - } - - return width; + return maxPortsExtent(nodeId); } } // namespace QtNodes diff --git a/src/DefaultNodeGeometryBase.cpp b/src/DefaultNodeGeometryBase.cpp new file mode 100644 index 00000000..e032e7c3 --- /dev/null +++ b/src/DefaultNodeGeometryBase.cpp @@ -0,0 +1,112 @@ +#include "DefaultNodeGeometryBase.hpp" + +#include "AbstractGraphModel.hpp" +#include "ConnectionIdUtils.hpp" +#include "NodeRenderingUtils.hpp" +#include "NodeData.hpp" + +#include + +namespace QtNodes { + +DefaultNodeGeometryBase::DefaultNodeGeometryBase(AbstractGraphModel &graphModel) + : AbstractNodeGeometry(graphModel) + , _portSize(20) + , _portSpasing(10) + , _fontMetrics(QFont()) + , _boldFontMetrics(QFont()) +{ + QFont f; + f.setBold(true); + _boldFontMetrics = QFontMetrics(f); + + _portSize = _fontMetrics.height(); +} + +QRectF DefaultNodeGeometryBase::boundingRect(NodeId const nodeId) const +{ + QSize s = size(nodeId); + std::optional fallback_style; + NodeStyle const &style = node_rendering::resolved_node_style(_graphModel, nodeId, fallback_style); + QMarginsF const margins = node_rendering::node_visual_margins(style.ShadowEnabled); + + QRectF r(QPointF(0, 0), s); + + return r.marginsAdded(margins); +} + +QSize DefaultNodeGeometryBase::size(NodeId const nodeId) const +{ + return _graphModel.nodeData(nodeId, NodeRole::Size); +} + +QRectF DefaultNodeGeometryBase::captionRect(NodeId const nodeId) const +{ + if (!_graphModel.nodeData(nodeId, NodeRole::CaptionVisible)) + return QRect(); + + QString name = _graphModel.nodeData(nodeId, NodeRole::Caption); + + return _boldFontMetrics.boundingRect(name); +} + +QRectF DefaultNodeGeometryBase::portTextRect(NodeId const nodeId, + PortType const portType, + PortIndex const portIndex) const +{ + QString s; + if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { + s = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); + } else { + auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType); + + s = portData.value().name; + } + + return _fontMetrics.boundingRect(s); +} + +unsigned int DefaultNodeGeometryBase::maxPortsExtent(NodeId const nodeId) const +{ + PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); + + PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); + + unsigned int maxNumOfEntries = std::max(nInPorts, nOutPorts); + unsigned int step = _portSize + _portSpasing; + + return step * maxNumOfEntries; +} + +unsigned int DefaultNodeGeometryBase::maxPortsTextAdvance(NodeId const nodeId, + PortType const portType) const +{ + unsigned int width = 0; + + size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)).toUInt(); + + for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { + QString name; + + if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { + name = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); + } else { + NodeDataType portData = _graphModel.portData(nodeId, + portType, + portIndex, + PortRole::DataType); + + name = portData.name; + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + width = std::max(unsigned(_fontMetrics.horizontalAdvance(name)), width); +#else + width = std::max(unsigned(_fontMetrics.width(name)), width); +#endif + } + + return width; +} + +} // namespace QtNodes diff --git a/src/DefaultVerticalNodeGeometry.cpp b/src/DefaultVerticalNodeGeometry.cpp index 43a9c8d0..cb2e4cbd 100644 --- a/src/DefaultVerticalNodeGeometry.cpp +++ b/src/DefaultVerticalNodeGeometry.cpp @@ -1,52 +1,20 @@ #include "DefaultVerticalNodeGeometry.hpp" #include "AbstractGraphModel.hpp" -#include "ConnectionIdUtils.hpp" -#include "NodeRenderingUtils.hpp" -#include "NodeData.hpp" #include #include #include -#include - namespace QtNodes { DefaultVerticalNodeGeometry::DefaultVerticalNodeGeometry(AbstractGraphModel &graphModel) - : AbstractNodeGeometry(graphModel) - , _portSize(20) - , _portSpasing(10) - , _fontMetrics(QFont()) - , _boldFontMetrics(QFont()) -{ - QFont f; - f.setBold(true); - _boldFontMetrics = QFontMetrics(f); - - _portSize = _fontMetrics.height(); -} - -QRectF DefaultVerticalNodeGeometry::boundingRect(NodeId const nodeId) const -{ - QSize s = size(nodeId); - std::optional fallback_style; - NodeStyle const &style = node_rendering::resolved_node_style(_graphModel, nodeId, fallback_style); - QMarginsF const margins = node_rendering::node_visual_margins(style.ShadowEnabled); - - QRectF r(QPointF(0, 0), s); - - return r.marginsAdded(margins); -} - -QSize DefaultVerticalNodeGeometry::size(NodeId const nodeId) const -{ - return _graphModel.nodeData(nodeId, NodeRole::Size); -} + : DefaultNodeGeometryBase(graphModel) +{} void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const { - unsigned int height = _portSpasing; // maxHorizontalPortsExtent(nodeId); + unsigned int height = _portSpasing; if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { height = std::max(height, static_cast(w->height())); @@ -165,16 +133,6 @@ QPointF DefaultVerticalNodeGeometry::portTextPosition(NodeId const nodeId, return p; } -QRectF DefaultVerticalNodeGeometry::captionRect(NodeId const nodeId) const -{ - if (!_graphModel.nodeData(nodeId, NodeRole::CaptionVisible)) - return QRect(); - - QString name = _graphModel.nodeData(nodeId, NodeRole::Caption); - - return _boldFontMetrics.boundingRect(name); -} - QPointF DefaultVerticalNodeGeometry::captionPosition(NodeId const nodeId) const { QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); @@ -215,63 +173,9 @@ QRect DefaultVerticalNodeGeometry::resizeHandleRect(NodeId const nodeId) const return QRect(size.width() - rectSize, size.height() - rectSize, rectSize, rectSize); } -QRectF DefaultVerticalNodeGeometry::portTextRect(NodeId const nodeId, - PortType const portType, - PortIndex const portIndex) const -{ - QString s; - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - s = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - auto portData = _graphModel.portData(nodeId, portType, portIndex, PortRole::DataType); - - s = portData.value().name; - } - - return _fontMetrics.boundingRect(s); -} - unsigned int DefaultVerticalNodeGeometry::maxHorizontalPortsExtent(NodeId const nodeId) const { - PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); - - PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); - - unsigned int maxNumOfEntries = std::max(nInPorts, nOutPorts); - unsigned int step = _portSize + _portSpasing; - - return step * maxNumOfEntries; -} - -unsigned int DefaultVerticalNodeGeometry::maxPortsTextAdvance(NodeId const nodeId, - PortType const portType) const -{ - unsigned int width = 0; - - size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)).toUInt(); - - for (PortIndex portIndex = 0ul; portIndex < n; ++portIndex) { - QString name; - - if (_graphModel.portData(nodeId, portType, portIndex, PortRole::CaptionVisible)) { - name = _graphModel.portData(nodeId, portType, portIndex, PortRole::Caption); - } else { - NodeDataType portData = _graphModel.portData(nodeId, - portType, - portIndex, - PortRole::DataType); - - name = portData.name; - } - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - width = std::max(unsigned(_fontMetrics.horizontalAdvance(name)), width); -#else - width = std::max(unsigned(_fontMetrics.width(name)), width); -#endif - } - - return width; + return maxPortsExtent(nodeId); } unsigned int DefaultVerticalNodeGeometry::portCaptionsHeight(NodeId const nodeId, From 67a8cd15f73c12444f9e8f8d6b1cf41db3629480 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 06:42:26 +0100 Subject: [PATCH 14/26] Initialize NodeStyle primitive defaults --- include/QtNodes/internal/NodeStyle.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/QtNodes/internal/NodeStyle.hpp b/include/QtNodes/internal/NodeStyle.hpp index 01ecd2ea..aa26ef83 100644 --- a/include/QtNodes/internal/NodeStyle.hpp +++ b/include/QtNodes/internal/NodeStyle.hpp @@ -60,7 +60,7 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style QColor GradientColor2; QColor GradientColor3; QColor ShadowColor; - bool ShadowEnabled; + bool ShadowEnabled{false}; QColor FontColor; QColor FontColorFaded; @@ -71,12 +71,12 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style QColor ErrorColor; QColor ToolTipIconColor; - float PenWidth; - float HoveredPenWidth; + float PenWidth{0.0f}; + float HoveredPenWidth{0.0f}; - float ConnectionPointDiameter; + float ConnectionPointDiameter{0.0f}; - float Opacity; + float Opacity{0.0f}; // Status icons - initialized in constructor after Q_INIT_RESOURCE QIcon statusUpdated; From 816fbca9f69a505722453f8df22a3d6aaa9ca755 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 09:45:03 +0100 Subject: [PATCH 15/26] Fix restore and interaction correctness --- .../QtNodes/internal/ConnectionIdUtils.hpp | 33 ++++---- .../QtNodes/internal/DataFlowGraphModel.hpp | 10 ++- .../QtNodes/internal/GroupGraphicsObject.hpp | 3 + .../QtNodes/internal/NodeDelegateModel.hpp | 6 +- include/QtNodes/internal/UndoCommands.hpp | 2 +- src/AbstractGraphModel.cpp | 2 +- src/BasicGraphicsScene.cpp | 14 ++++ src/ConnectionState.cpp | 7 +- src/DataFlowGraphModel.cpp | 82 ++++++++++++++++--- src/DefaultNodePainter.cpp | 3 +- src/GroupGraphicsObject.cpp | 26 ++++++ src/NodeConnectionInteraction.cpp | 8 +- src/NodeGraphicsObject.cpp | 28 +++++-- src/UndoCommands.cpp | 56 ++++++++----- 14 files changed, 217 insertions(+), 63 deletions(-) diff --git a/include/QtNodes/internal/ConnectionIdUtils.hpp b/include/QtNodes/internal/ConnectionIdUtils.hpp index 55421883..82dc6487 100644 --- a/include/QtNodes/internal/ConnectionIdUtils.hpp +++ b/include/QtNodes/internal/ConnectionIdUtils.hpp @@ -9,7 +9,7 @@ namespace QtNodes { -inline PortIndex getNodeId(PortType portType, ConnectionId connectionId) +inline NodeId getNodeId(PortType portType, ConnectionId const &connectionId) { NodeId id = InvalidNodeId; @@ -22,7 +22,7 @@ inline PortIndex getNodeId(PortType portType, ConnectionId connectionId) return id; } -inline PortIndex getPortIndex(PortType portType, ConnectionId connectionId) +inline PortIndex getPortIndex(PortType portType, ConnectionId const &connectionId) { PortIndex index = InvalidPortIndex; @@ -129,18 +129,23 @@ inline QJsonObject toJson(ConnectionId const &connId) inline ConnectionId fromJson(QJsonObject const &connJson) { - // Support both "inNodeId" (correct) and "intNodeId" (legacy typo) for backward compatibility - NodeId inNodeId = InvalidNodeId; - if (connJson.contains("inNodeId")) { - inNodeId = static_cast(connJson["inNodeId"].toInt(InvalidNodeId)); - } else if (connJson.contains("intNodeId")) { - inNodeId = static_cast(connJson["intNodeId"].toInt(InvalidNodeId)); - } - - ConnectionId connId{static_cast(connJson["outNodeId"].toInt(InvalidNodeId)), - static_cast(connJson["outPortIndex"].toInt(InvalidPortIndex)), - inNodeId, - static_cast(connJson["inPortIndex"].toInt(InvalidPortIndex))}; + auto parse_unsigned = [](QJsonValue const &value, quint64 maxValue, quint64 invalidValue) { + bool ok = false; + quint64 const parsed = value.toVariant().toULongLong(&ok); + if (!ok || parsed > maxValue) { + return invalidValue; + } + + return parsed; + }; + + ConnectionId connId{ + static_cast(parse_unsigned(connJson["outNodeId"], InvalidNodeId - 1ull, InvalidNodeId)), + static_cast( + parse_unsigned(connJson["outPortIndex"], InvalidPortIndex - 1ull, InvalidPortIndex)), + static_cast(parse_unsigned(connJson["inNodeId"], InvalidNodeId - 1ull, InvalidNodeId)), + static_cast( + parse_unsigned(connJson["inPortIndex"], InvalidPortIndex - 1ull, InvalidPortIndex))}; return connId; } diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index 3e10c3a7..ab3afc89 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -11,6 +11,7 @@ #include #include +#include namespace QtNodes { @@ -105,7 +106,14 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel void inPortDataWasSet(NodeId const, PortType const, PortIndex const); private: - NodeId newNodeId() override { return _nextNodeId++; } + NodeId newNodeId() override + { + if (_nextNodeId == InvalidNodeId) { + throw std::overflow_error("No available node identifiers"); + } + + return _nextNodeId++; + } void connectDelegateModel(NodeDelegateModel *model, NodeId nodeId); diff --git a/include/QtNodes/internal/GroupGraphicsObject.hpp b/include/QtNodes/internal/GroupGraphicsObject.hpp index 82990891..2d21e62b 100644 --- a/include/QtNodes/internal/GroupGraphicsObject.hpp +++ b/include/QtNodes/internal/GroupGraphicsObject.hpp @@ -182,6 +182,9 @@ class GroupGraphicsObject /** @copydoc QGraphicsItem::mouseMoveEvent() */ void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + /** @copydoc QGraphicsItem::mouseReleaseEvent() */ + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + /** @copydoc QGraphicsItem::mouseDoubleClickEvent() */ void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index 5d1b776f..3b39e0d8 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -27,9 +27,9 @@ struct NodeValidationState Warning = 1, ///< Some inputs are missing or questionable, processing may be unreliable. Error = 2, ///< Inputs or settings are invalid, preventing successful computation. }; - bool isValid() { return _state == State::Valid; }; - QString const message() { return _stateMessage; } - State state() { return _state; } + bool isValid() const { return _state == State::Valid; }; + QString message() const { return _stateMessage; } + State state() const { return _state; } State _state{State::Valid}; QString _stateMessage{""}; diff --git a/include/QtNodes/internal/UndoCommands.hpp b/include/QtNodes/internal/UndoCommands.hpp index 7aed4d60..b2f61c95 100644 --- a/include/QtNodes/internal/UndoCommands.hpp +++ b/include/QtNodes/internal/UndoCommands.hpp @@ -64,7 +64,7 @@ class NODE_EDITOR_PUBLIC PasteCommand : public QUndoCommand private: BasicGraphicsScene *_scene; - QPointF const &_mouseScenePos; + QPointF _mouseScenePos; QJsonObject _newSceneJson; }; diff --git a/src/AbstractGraphModel.cpp b/src/AbstractGraphModel.cpp index 06a69617..fbf9a488 100644 --- a/src/AbstractGraphModel.cpp +++ b/src/AbstractGraphModel.cpp @@ -13,7 +13,7 @@ void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, unsigned int portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); - if (first > portCount - 1) + if (portCount == 0 || first >= portCount) return; if (last < first) diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index 850b27fc..04adf763 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -804,8 +804,22 @@ std::weak_ptr BasicGraphicsScene::loadGroupFile() if (!file.open(QIODevice::ReadOnly)) { qDebug() << "Error loading group file!"; + return std::weak_ptr(); } + struct Current_dir_guard + { + QString path; + + ~Current_dir_guard() + { + if (!path.isEmpty()) { + QDir::setCurrent(path); + } + } + }; + + Current_dir_guard currentDirGuard{QDir::currentPath()}; QDir d = QFileInfo(fileName).absoluteDir(); QString absolute = d.absolutePath(); QDir::setCurrent(absolute); diff --git a/src/ConnectionState.cpp b/src/ConnectionState.cpp index b279f77c..b2a9d90b 100644 --- a/src/ConnectionState.cpp +++ b/src/ConnectionState.cpp @@ -53,8 +53,11 @@ NodeId ConnectionState::lastHoveredNode() const void ConnectionState::resetLastHoveredNode() { if (_lastHoveredNode != InvalidNodeId) { - auto ngo = _cgo.nodeScene()->nodeGraphicsObject(_lastHoveredNode); - ngo->update(); + if (auto *scene = _cgo.nodeScene()) { + if (auto *ngo = scene->nodeGraphicsObject(_lastHoveredNode)) { + ngo->update(); + } + } } _lastHoveredNode = InvalidNodeId; diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index 4465558b..098a7d5c 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -5,12 +5,29 @@ #include "Definitions.hpp" #include +#include #include #include +#include namespace QtNodes { +namespace { + +NodeId json_value_to_node_id(QJsonValue const &value) +{ + bool ok = false; + quint64 const parsed = value.toVariant().toULongLong(&ok); + if (!ok || parsed >= InvalidNodeId) { + return InvalidNodeId; + } + + return static_cast(parsed); +} + +} // namespace + DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr registry) : _registry(std::move(registry)) , _nextNodeId{0} @@ -113,9 +130,13 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con return connected.empty() || (policy == ConnectionPolicy::Many); }; + bool const portsValid = checkPortBounds(PortType::Out) && checkPortBounds(PortType::In); + if (!portsValid) { + return false; + } + bool const basicChecks = getDataType(PortType::Out).id == getDataType(PortType::In).id - && portVacant(PortType::Out) && portVacant(PortType::In) - && checkPortBounds(PortType::Out) && checkPortBounds(PortType::In); + && portVacant(PortType::Out) && portVacant(PortType::In); // In data-flow mode (this class) it's important to forbid graph loops. // We perform depth-first graph traversal starting from the "Input" port of @@ -123,12 +144,17 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con auto hasLoops = [this, &connectionId]() -> bool { std::stack filo; + std::unordered_set visited; filo.push(connectionId.inNodeId); while (!filo.empty()) { auto id = filo.top(); filo.pop(); + if (!visited.insert(id).second) { + continue; + } + if (id == connectionId.outNodeId) { // LOOP! return true; } @@ -153,6 +179,10 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con void DataFlowGraphModel::addConnection(ConnectionId const connectionId) { + if (connectionExists(connectionId) || !connectionPossible(connectionId)) { + return; + } + _connectivity.insert(connectionId); sendConnectionCreation(connectionId); @@ -255,11 +285,21 @@ QVariant DataFlowGraphModel::nodeData(NodeId nodeId, NodeRole role) const break; case NodeRole::Position: - result = _nodeGeometryData[nodeId].pos; + if (auto geometryIt = _nodeGeometryData.find(nodeId); geometryIt != _nodeGeometryData.end()) { + result = geometryIt->second.pos; + } + else { + result = QPointF{}; + } break; case NodeRole::Size: - result = _nodeGeometryData[nodeId].size; + if (auto geometryIt = _nodeGeometryData.find(nodeId); geometryIt != _nodeGeometryData.end()) { + result = geometryIt->second.size; + } + else { + result = QSize{}; + } break; case NodeRole::CaptionVisible: @@ -323,9 +363,9 @@ NodeFlags DataFlowGraphModel::nodeFlags(NodeId nodeId) const bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant value) { - Q_UNUSED(nodeId); - Q_UNUSED(role); - Q_UNUSED(value); + if (!nodeExists(nodeId)) { + return false; + } bool result = false; @@ -371,6 +411,7 @@ bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant valu auto state = value.value(); if (auto node = delegateModel(nodeId); node != nullptr) { node->setValidationState(state); + result = true; } } Q_EMIT nodeUpdated(nodeId); @@ -381,6 +422,7 @@ bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant valu auto status = value.value(); if (auto node = delegateModel(nodeId); node != nullptr) { node->setNodeProcessingStatus(status); + result = true; } } Q_EMIT nodeUpdated(nodeId); @@ -401,6 +443,15 @@ QVariant DataFlowGraphModel::portData(NodeId nodeId, if (it == _models.end()) return result; + if (portType == PortType::None) { + return result; + } + + PortCount const portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); + if (portIndex >= portCount) { + return result; + } + auto &model = it->second; switch (role) { @@ -434,12 +485,19 @@ QVariant DataFlowGraphModel::portData(NodeId nodeId, bool DataFlowGraphModel::setPortData( NodeId nodeId, PortType portType, PortIndex portIndex, QVariant const &value, PortRole role) { - Q_UNUSED(nodeId); - auto it = _models.find(nodeId); if (it == _models.end()) return false; + if (portType == PortType::None) { + return false; + } + + PortCount const portCount = nodeData(nodeId, portCountRole(portType)).toUInt(); + if (portIndex >= portCount) { + return false; + } + auto &model = it->second; switch (role) { @@ -452,6 +510,7 @@ bool DataFlowGraphModel::setPortData( // Triggers repainting on the scene. Q_EMIT inPortDataWasSet(nodeId, portType, portIndex); + return true; } break; @@ -548,7 +607,10 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) // loading. // 2. When undoing the deletion command. Conflict is not possible // because all the new ids were created past the removed nodes. - NodeId restoredNodeId = nodeJson["id"].toInt(); + NodeId restoredNodeId = json_value_to_node_id(nodeJson["id"]); + if (restoredNodeId == InvalidNodeId) { + throw std::logic_error("Invalid node id in serialized node"); + } _nextNodeId = std::max(_nextNodeId, restoredNodeId + 1); diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 6e344bef..0cb8ce9b 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -424,11 +424,10 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo case NodeValidationState::State::Warning: { invalid = true; color = nodeStyle.WarningColor; - break; + } break; default: break; } - } } QRectF boundary(0, 0, size.width(), size.height()); diff --git a/src/GroupGraphicsObject.cpp b/src/GroupGraphicsObject.cpp index a1d6a5d2..19e59504 100644 --- a/src/GroupGraphicsObject.cpp +++ b/src/GroupGraphicsObject.cpp @@ -14,6 +14,20 @@ using QtNodes::GroupGraphicsObject; using QtNodes::NodeConnectionInteraction; using QtNodes::NodeGraphicsObject; using QtNodes::NodeGroup; +using QtNodes::NodeRole; + +namespace { + +void update_model_position_if_needed(NodeGraphicsObject &node) +{ + QPointF const scenePos = node.pos(); + QPointF const modelPos = node.graphModel().nodeData(node.nodeId(), NodeRole::Position); + if (scenePos != modelPos) { + node.graphModel().setNodeData(node.nodeId(), NodeRole::Position, scenePos); + } +} + +} // namespace IconGraphicsItem::IconGraphicsItem(QGraphicsItem *parent) : QGraphicsPixmapItem(parent) @@ -202,6 +216,18 @@ void GroupGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent *event) } } +void GroupGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mouseReleaseEvent(event); + + for (auto &node : group().childNodes()) { + update_model_position_if_needed(*node); + } + + moveConnections(); + updateGroupGeometry(); +} + void GroupGraphicsObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { QGraphicsItem::mouseDoubleClickEvent(event); diff --git a/src/NodeConnectionInteraction.cpp b/src/NodeConnectionInteraction.cpp index e44cad1c..61474f90 100644 --- a/src/NodeConnectionInteraction.cpp +++ b/src/NodeConnectionInteraction.cpp @@ -96,10 +96,14 @@ bool NodeConnectionInteraction::disconnect(PortType portToDisconnect) const //Repaint connection points. NodeId connectedNodeId = getNodeId(oppositePort(portToDisconnect), connectionId); - _scene.nodeGraphicsObject(connectedNodeId)->update(); + if (auto *connectedNode = _scene.nodeGraphicsObject(connectedNodeId)) { + connectedNode->update(); + } NodeId disconnectedNodeId = getNodeId(portToDisconnect, connectionId); - _scene.nodeGraphicsObject(disconnectedNodeId)->update(); + if (auto *disconnectedNode = _scene.nodeGraphicsObject(disconnectedNodeId)) { + disconnectedNode->update(); + } return true; } diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 1b2e8331..3df7477c 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -58,6 +58,20 @@ QGraphicsItem::CacheMode initial_cache_mode(BasicGraphicsScene &scene) return QGraphicsItem::DeviceCoordinateCache; } +void update_model_position_if_needed(NodeGraphicsObject &node) +{ + auto *scene = node.nodeScene(); + if (!scene) { + return; + } + + QPointF const scenePos = node.pos(); + QPointF const modelPos = node.graphModel().nodeData(node.nodeId(), NodeRole::Position); + if (scenePos != modelPos) { + node.graphModel().setNodeData(node.nodeId(), NodeRole::Position, scenePos); + } +} + } // namespace NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) @@ -253,15 +267,12 @@ void NodeGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent *event) // Start dragging existing connection. if (!connected.empty() && portToCheck == PortType::In) { auto const &cnId = *connected.begin(); + if (auto *connection = nodeScene()->connectionGraphicsObject(cnId)) { + NodeConnectionInteraction interaction(*this, *connection, *nodeScene()); - // Need ConnectionGraphicsObject - - NodeConnectionInteraction interaction(*this, - *nodeScene()->connectionGraphicsObject(cnId), - *nodeScene()); - - if (_graphModel.detachPossible(cnId)) - interaction.disconnect(portToCheck); + if (_graphModel.detachPossible(cnId)) + interaction.disconnect(portToCheck); + } } else // initialize new Connection { if (portToCheck == PortType::Out) { @@ -401,6 +412,7 @@ void NodeGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) // position connections precisely after fast node move moveConnections(); + update_model_position_if_needed(*this); if (nodeScene()->groupingEnabled() && _draggingIntoGroup && _possibleGroup && _nodeGroup.expired()) { diff --git a/src/UndoCommands.cpp b/src/UndoCommands.cpp index 265d7e17..200690ef 100644 --- a/src/UndoCommands.cpp +++ b/src/UndoCommands.cpp @@ -24,6 +24,7 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) auto &graphModel = scene->graphModel(); std::unordered_set selectedNodes; + std::unordered_set selectedConnections; QJsonArray nodesJsonArray; QJsonArray groupsJsonArray; @@ -39,14 +40,21 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) } }; + auto appendConnection = [&](ConnectionId const &connectionId) { + if (selectedNodes.count(connectionId.outNodeId) == 0 + || selectedNodes.count(connectionId.inNodeId) == 0) { + return; + } + + if (selectedConnections.insert(connectionId).second) { + connJsonArray.append(toJson(connectionId)); + } + }; + for (QGraphicsItem *item : scene->selectedItems()) { if (auto group = qgraphicsitem_cast(item)) { for (auto *node : group->group().childNodes()) { appendNode(node); - - for (auto const &connectionId : graphModel.allConnectionIds(node->nodeId())) { - connJsonArray.append(toJson(connectionId)); - } } } } @@ -54,10 +62,12 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) for (QGraphicsItem *item : scene->selectedItems()) { if (auto ngo = qgraphicsitem_cast(item)) { appendNode(ngo); + } + } - for (auto const &connectionId : graphModel.allConnectionIds(ngo->nodeId())) { - connJsonArray.append(toJson(connectionId)); - } + for (NodeId const nodeId : selectedNodes) { + for (auto const &connectionId : graphModel.allConnectionIds(nodeId)) { + appendConnection(connectionId); } } @@ -81,11 +91,7 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) for (QGraphicsItem *item : scene->selectedItems()) { if (auto c = qgraphicsitem_cast(item)) { - auto const &cid = c->connectionId(); - - if (selectedNodes.count(cid.outNodeId) > 0 && selectedNodes.count(cid.inNodeId) > 0) { - connJsonArray.append(toJson(cid)); - } + appendConnection(c->connectionId()); } } @@ -108,8 +114,10 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s graphModel.loadNode(obj); auto id = obj["id"].toInt(); - scene->nodeGraphicsObject(id)->setZValue(1.0); - scene->nodeGraphicsObject(id)->setSelected(true); + if (auto *nodeObject = scene->nodeGraphicsObject(id)) { + nodeObject->setZValue(1.0); + nodeObject->setSelected(true); + } } QJsonArray const &connJsonArray = json["connections"].toArray(); @@ -122,7 +130,9 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s // Restore the connection graphModel.addConnection(connId); - scene->connectionGraphicsObject(connId)->setSelected(true); + if (auto *connectionObject = scene->connectionGraphicsObject(connId)) { + connectionObject->setSelected(true); + } } if (json.contains("groups")) { @@ -460,9 +470,15 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) ConnectionId connId = fromJson(connJson); - ConnectionId newConnId{mapNodeIds[connId.outNodeId], + auto const outIt = mapNodeIds.find(connId.outNodeId); + auto const inIt = mapNodeIds.find(connId.inNodeId); + if (outIt == mapNodeIds.end() || inIt == mapNodeIds.end()) { + continue; + } + + ConnectionId newConnId{outIt->second, connId.outPortIndex, - mapNodeIds[connId.inNodeId], + inIt->second, connId.inPortIndex}; newConnJsonArray.append(toJson(newConnId)); @@ -484,8 +500,10 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) QJsonArray newNodeIdsJson; for (const QJsonValue &idVal : nodeIdsJson) { NodeId oldId = static_cast(idVal.toInt()); - NodeId newId = mapNodeIds[oldId]; - newNodeIdsJson.append(static_cast(newId)); + auto const nodeIt = mapNodeIds.find(oldId); + if (nodeIt != mapNodeIds.end()) { + newNodeIdsJson.append(static_cast(nodeIt->second)); + } } groupJson["nodes"] = newNodeIdsJson; From 30be6948acf96a8618d2b32e99ff6c0cbb7ae20a Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 09:47:16 +0100 Subject: [PATCH 16/26] Index dataflow connection lookups --- .../QtNodes/internal/DataFlowGraphModel.hpp | 15 +++ src/DataFlowGraphModel.cpp | 92 +++++++++++++++---- 2 files changed, 89 insertions(+), 18 deletions(-) diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index ab3afc89..d0867866 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -12,6 +12,8 @@ #include #include +#include +#include namespace QtNodes { @@ -106,6 +108,9 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel void inPortDataWasSet(NodeId const, PortType const, PortIndex const); private: + using Connection_set = std::unordered_set; + using Connections_by_port = std::unordered_map; + NodeId newNodeId() override { if (_nextNodeId == InvalidNodeId) { @@ -117,6 +122,10 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel void connectDelegateModel(NodeDelegateModel *model, NodeId nodeId); + void indexConnection(ConnectionId const connectionId); + + void unindexConnection(ConnectionId const connectionId); + void sendConnectionCreation(ConnectionId const connectionId); void sendConnectionDeletion(ConnectionId const connectionId); @@ -146,6 +155,12 @@ private Q_SLOTS: std::unordered_set _connectivity; + std::unordered_map _nodeConnections; + + std::unordered_map _inConnectionsByPort; + + std::unordered_map _outConnectionsByPort; + mutable std::unordered_map _nodeGeometryData; }; diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index 098a7d5c..4127c4b2 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -43,33 +43,35 @@ std::unordered_set DataFlowGraphModel::allNodeIds() const std::unordered_set DataFlowGraphModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); + auto it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return {}; + } - return result; + return it->second; } std::unordered_set DataFlowGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; + if (portType == PortType::None) { + return {}; + } - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return {}; + } - return result; + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return {}; + } + + return portIt->second; } bool DataFlowGraphModel::connectionExists(ConnectionId const connectionId) const @@ -184,6 +186,7 @@ void DataFlowGraphModel::addConnection(ConnectionId const connectionId) } _connectivity.insert(connectionId); + indexConnection(connectionId); sendConnectionCreation(connectionId); @@ -236,6 +239,58 @@ void DataFlowGraphModel::connectDelegateModel(NodeDelegateModel *model, NodeId n }); } +void DataFlowGraphModel::indexConnection(ConnectionId const connectionId) +{ + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); +} + +void DataFlowGraphModel::unindexConnection(ConnectionId const connectionId) +{ + auto erase_from_node = [&](NodeId nodeId) { + auto nodeIt = _nodeConnections.find(nodeId); + if (nodeIt == _nodeConnections.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + _nodeConnections.erase(nodeIt); + } + }; + + auto erase_from_port_map = + [&](std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex) { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + }; + + erase_from_node(connectionId.outNodeId); + erase_from_node(connectionId.inNodeId); + erase_from_port_map(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); + erase_from_port_map(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); +} + void DataFlowGraphModel::sendConnectionCreation(ConnectionId const connectionId) { Q_EMIT connectionCreated(connectionId); @@ -534,6 +589,7 @@ bool DataFlowGraphModel::deleteConnection(ConnectionId const connectionId) } if (disconnected) { + unindexConnection(connectionId); sendConnectionDeletion(connectionId); propagateEmptyDataTo(getNodeId(PortType::In, connectionId), From 2e2a5ed895d9ea2149c968492345744579c98baf Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 09:54:10 +0100 Subject: [PATCH 17/26] Fix MSVC header and style build errors --- include/QtNodes/internal/NodeGroup.hpp | 5 ++++- include/QtNodes/internal/Style.hpp | 11 +++++++++++ src/NodeGroup.cpp | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/include/QtNodes/internal/NodeGroup.hpp b/include/QtNodes/internal/NodeGroup.hpp index de27f8d7..94c3af7c 100644 --- a/include/QtNodes/internal/NodeGroup.hpp +++ b/include/QtNodes/internal/NodeGroup.hpp @@ -1,15 +1,16 @@ #pragma once +#include #include #include #include #include "Definitions.hpp" #include "Export.hpp" -#include "GroupGraphicsObject.hpp" namespace QtNodes { +class GroupGraphicsObject; class NodeGraphicsObject; /** @@ -34,6 +35,8 @@ class NODE_EDITOR_PUBLIC NodeGroup : public QObject QString name = QString(), QObject *parent = nullptr); + ~NodeGroup() override; + public: /** * @brief Prepares a byte array containing this group's data to be saved in a diff --git a/include/QtNodes/internal/Style.hpp b/include/QtNodes/internal/Style.hpp index 08af6300..f0adeed7 100644 --- a/include/QtNodes/internal/Style.hpp +++ b/include/QtNodes/internal/Style.hpp @@ -74,6 +74,17 @@ inline bool readFloat(QJsonObject const &obj, QString const &key, double &val) return true; } +inline bool readFloat(QJsonObject const &obj, QString const &key, float &val) +{ + double tmp{}; + + if (!readFloat(obj, key, tmp)) + return false; + + val = static_cast(tmp); + return true; +} + inline void writeFloat(QJsonObject &obj, QString const &key, double val) { obj[key] = val; diff --git a/src/NodeGroup.cpp b/src/NodeGroup.cpp index a414d935..6b70d86c 100644 --- a/src/NodeGroup.cpp +++ b/src/NodeGroup.cpp @@ -1,5 +1,7 @@ #include "NodeGroup.hpp" #include "ConnectionIdUtils.hpp" +#include "GroupGraphicsObject.hpp" +#include "NodeGraphicsObject.hpp" #include #include @@ -25,6 +27,8 @@ NodeGroup::NodeGroup(std::vector nodes, _groupCount++; } +NodeGroup::~NodeGroup() = default; + QByteArray NodeGroup::saveToFile() const { QJsonObject groupJson; From 221c64425d972a08847cc7f4233e03cf9c7afb70 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 10:12:03 +0100 Subject: [PATCH 18/26] Validate serialized graph and group loads --- CMakeLists.txt | 1 + .../QtNodes/internal/ConnectionIdUtils.hpp | 42 +++-- .../internal/SerializationValidation.hpp | 157 ++++++++++++++++++ src/BasicGraphicsScene.cpp | 148 ++++++++++++----- src/DataFlowGraphModel.cpp | 117 ++++++++++--- src/DataFlowGraphicsScene.cpp | 123 +++++++++++--- 6 files changed, 479 insertions(+), 109 deletions(-) create mode 100644 include/QtNodes/internal/SerializationValidation.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cc0e7c4..cc043857 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,7 @@ set(HPP_HEADER_FILES include/QtNodes/internal/QStringStdHash.hpp include/QtNodes/internal/QUuidStdHash.hpp include/QtNodes/internal/Serializable.hpp + include/QtNodes/internal/SerializationValidation.hpp include/QtNodes/internal/Style.hpp include/QtNodes/internal/StyleCollection.hpp include/QtNodes/internal/DefaultConnectionPainter.hpp diff --git a/include/QtNodes/internal/ConnectionIdUtils.hpp b/include/QtNodes/internal/ConnectionIdUtils.hpp index 82dc6487..e8286da3 100644 --- a/include/QtNodes/internal/ConnectionIdUtils.hpp +++ b/include/QtNodes/internal/ConnectionIdUtils.hpp @@ -1,6 +1,7 @@ #pragma once #include "Definitions.hpp" +#include "SerializationValidation.hpp" #include @@ -129,27 +130,34 @@ inline QJsonObject toJson(ConnectionId const &connId) inline ConnectionId fromJson(QJsonObject const &connJson) { - auto parse_unsigned = [](QJsonValue const &value, quint64 maxValue, quint64 invalidValue) { - bool ok = false; - quint64 const parsed = value.toVariant().toULongLong(&ok); - if (!ok || parsed > maxValue) { - return invalidValue; - } - - return parsed; - }; - - ConnectionId connId{ - static_cast(parse_unsigned(connJson["outNodeId"], InvalidNodeId - 1ull, InvalidNodeId)), - static_cast( - parse_unsigned(connJson["outPortIndex"], InvalidPortIndex - 1ull, InvalidPortIndex)), - static_cast(parse_unsigned(connJson["inNodeId"], InvalidNodeId - 1ull, InvalidNodeId)), - static_cast( - parse_unsigned(connJson["inPortIndex"], InvalidPortIndex - 1ull, InvalidPortIndex))}; + ConnectionId connId{InvalidNodeId, InvalidPortIndex, InvalidNodeId, InvalidPortIndex}; + + detail::read_node_id(connJson["outNodeId"], connId.outNodeId); + detail::read_port_index(connJson["outPortIndex"], connId.outPortIndex); + detail::read_node_id(connJson["inNodeId"], connId.inNodeId); + detail::read_port_index(connJson["inPortIndex"], connId.inPortIndex); return connId; } +inline bool tryFromJson(QJsonObject const &connJson, ConnectionId &connId) +{ + NodeId outNodeId = InvalidNodeId; + NodeId inNodeId = InvalidNodeId; + PortIndex outPortIndex = InvalidPortIndex; + PortIndex inPortIndex = InvalidPortIndex; + + if (!detail::read_node_id(connJson["outNodeId"], outNodeId) + || !detail::read_port_index(connJson["outPortIndex"], outPortIndex) + || !detail::read_node_id(connJson["inNodeId"], inNodeId) + || !detail::read_port_index(connJson["inPortIndex"], inPortIndex)) { + return false; + } + + connId = ConnectionId{outNodeId, outPortIndex, inNodeId, inPortIndex}; + return true; +} + inline NodeRole portCountRole(PortType portType) { return (portType == PortType::Out) ? NodeRole::OutPortCount : NodeRole::InPortCount; diff --git a/include/QtNodes/internal/SerializationValidation.hpp b/include/QtNodes/internal/SerializationValidation.hpp new file mode 100644 index 00000000..8f785061 --- /dev/null +++ b/include/QtNodes/internal/SerializationValidation.hpp @@ -0,0 +1,157 @@ +#pragma once + +#include "Definitions.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace QtNodes::detail { + +inline bool read_unsigned_number(QJsonValue const &value, quint64 maxValue, quint64 &result) +{ + if (!value.isDouble()) { + return false; + } + + double const parsed = value.toDouble(); + if (!std::isfinite(parsed) || parsed < 0.0 || parsed > static_cast(maxValue)) { + return false; + } + + quint64 const integral = static_cast(parsed); + if (parsed != static_cast(integral)) { + return false; + } + + result = integral; + return true; +} + +inline bool read_node_id(QJsonValue const &value, NodeId &nodeId) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidNodeId - 1ull, parsed)) { + return false; + } + + nodeId = static_cast(parsed); + return true; +} + +inline bool read_group_id(QJsonValue const &value, GroupId &groupId) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidGroupId - 1ull, parsed)) { + return false; + } + + groupId = static_cast(parsed); + return true; +} + +inline bool read_port_index(QJsonValue const &value, PortIndex &portIndex) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidPortIndex - 1ull, parsed)) { + return false; + } + + portIndex = static_cast(parsed); + return true; +} + +inline bool read_finite_number(QJsonValue const &value, double &result) +{ + if (!value.isDouble()) { + return false; + } + + double const parsed = value.toDouble(); + if (!std::isfinite(parsed)) { + return false; + } + + result = parsed; + return true; +} + +inline bool read_required_object(QJsonObject const &obj, QString const &key, QJsonObject &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isObject()) { + return false; + } + + result = it->toObject(); + return true; +} + +inline bool read_required_array(QJsonObject const &obj, QString const &key, QJsonArray &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isArray()) { + return false; + } + + result = it->toArray(); + return true; +} + +inline bool read_required_string(QJsonObject const &obj, QString const &key, QString &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isString()) { + return false; + } + + result = it->toString(); + return true; +} + +inline bool read_optional_bool(QJsonObject const &obj, QString const &key, bool &result) +{ + auto const it = obj.find(key); + + if (it == obj.end()) { + return true; + } + + if (!it->isBool()) { + return false; + } + + result = it->toBool(); + return true; +} + +inline bool read_required_point(QJsonObject const &obj, QString const &key, QPointF &result) +{ + QJsonObject pointObject; + + if (!read_required_object(obj, key, pointObject)) { + return false; + } + + double x = 0.0; + double y = 0.0; + + if (!read_finite_number(pointObject["x"], x) || !read_finite_number(pointObject["y"], y)) { + return false; + } + + result = QPointF(x, y); + return true; +} + +} // namespace QtNodes::detail diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index 04adf763..086b070e 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -11,6 +11,7 @@ #include "GraphicsView.hpp" #include "NodeDelegateModel.hpp" #include "NodeGraphicsObject.hpp" +#include "SerializationValidation.hpp" #include @@ -23,23 +24,24 @@ #include #include -#include #include #include #include #include +#include #include #include #include #include #include +#include #include #include -#include namespace { +using QtNodes::ConnectionId; using QtNodes::GroupId; using QtNodes::InvalidGroupId; using QtNodes::InvalidNodeId; @@ -47,32 +49,49 @@ using QtNodes::NodeId; NodeId jsonValueToNodeId(QJsonValue const &value) { - if (value.isDouble()) { - return static_cast(value.toInt()); + NodeId nodeId = InvalidNodeId; + + if (!QtNodes::detail::read_node_id(value, nodeId)) { + return InvalidNodeId; } - if (value.isString()) { - auto const textValue = value.toString(); + return nodeId; +} - bool ok = false; - auto const numericValue = textValue.toULongLong(&ok, 10); - if (ok) { - return static_cast(numericValue); - } +void validate_group_json(QJsonObject const &groupJson) +{ + QString groupName; + if (!QtNodes::detail::read_required_string(groupJson, "name", groupName)) { + throw std::logic_error("Serialized group contains invalid name"); + } + Q_UNUSED(groupName); - QUuid uuidValue(textValue); - if (!uuidValue.isNull()) { - auto const bytes = uuidValue.toRfc4122(); - if (bytes.size() >= static_cast(sizeof(quint32))) { - QDataStream stream(bytes); - quint32 value32 = 0U; - stream >> value32; - return static_cast(value32); - } + QJsonArray nodesJson; + if (!QtNodes::detail::read_required_array(groupJson, "nodes", nodesJson)) { + throw std::logic_error("Serialized group contains invalid nodes array"); + } + + QJsonArray connectionsJson; + if (!QtNodes::detail::read_required_array(groupJson, "connections", connectionsJson)) { + throw std::logic_error("Serialized group contains invalid connections array"); + } + + for (QJsonValue const &nodeValue : nodesJson) { + if (!nodeValue.isObject()) { + throw std::logic_error("Serialized group contains invalid node entry"); } } - return InvalidNodeId; + for (QJsonValue const &connectionValue : connectionsJson) { + if (!connectionValue.isObject()) { + throw std::logic_error("Serialized group contains invalid connection entry"); + } + + ConnectionId connId; + if (!QtNodes::tryFromJson(connectionValue.toObject(), connId)) { + throw std::logic_error("Serialized group contains invalid connection id"); + } + } } } // namespace @@ -595,9 +614,10 @@ NodeGraphicsObject &BasicGraphicsScene::loadNodeToMap(QJsonObject nodeJson, bool if (keepOriginalId) { newNodeId = jsonValueToNodeId(nodeJson["id"]); - } - - if (newNodeId == InvalidNodeId) { + if (newNodeId == InvalidNodeId) { + throw std::logic_error("Invalid node id in serialized node"); + } + } else { newNodeId = _graphModel.newNodeId(); nodeJson["id"] = static_cast(newNodeId); } @@ -617,13 +637,16 @@ NodeGraphicsObject &BasicGraphicsScene::loadNodeToMap(QJsonObject nodeJson, bool void BasicGraphicsScene::loadConnectionToMap(QJsonObject const &connectionJson, std::unordered_map const &nodeIdMap) { - ConnectionId connId = fromJson(connectionJson); + ConnectionId connId; + if (!tryFromJson(connectionJson, connId)) { + throw std::logic_error("Invalid serialized connection"); + } auto const outIt = nodeIdMap.find(connId.outNodeId); auto const inIt = nodeIdMap.find(connId.inNodeId); if (outIt == nodeIdMap.end() || inIt == nodeIdMap.end()) { - return; + throw std::logic_error("Serialized connection references unknown node id"); } ConnectionId remapped{outIt->second, connId.outPortIndex, inIt->second, connId.inPortIndex}; @@ -632,9 +655,11 @@ void BasicGraphicsScene::loadConnectionToMap(QJsonObject const &connectionJson, return; } - if (_graphModel.connectionPossible(remapped)) { - _graphModel.addConnection(remapped); + if (!_graphModel.connectionPossible(remapped)) { + throw std::logic_error("Serialized connection is not valid for restored nodes"); } + + _graphModel.addConnection(remapped); } std::pair, std::unordered_map> @@ -643,6 +668,8 @@ BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) if (!_groupingEnabled) return {std::weak_ptr(), {}}; + validate_group_json(groupJson); + // since the new nodes will have the same IDs as in the file and the connections // need these old IDs to be restored, we must create new IDs and map them to the // old ones so the connections are properly restored @@ -650,29 +677,42 @@ BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) std::unordered_map nodeIdMap{}; std::vector group_children{}; + std::vector createdNodeIds{}; + + try { + QJsonArray nodesJson = groupJson["nodes"].toArray(); + for (const QJsonValueRef nodeJson : nodesJson) { + QJsonObject nodeObject = nodeJson.toObject(); + NodeId const oldNodeId = jsonValueToNodeId(nodeObject["id"]); + + NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, false); + NodeId const newNodeId = nodeRef.nodeId(); - QJsonArray nodesJson = groupJson["nodes"].toArray(); - for (const QJsonValueRef nodeJson : nodesJson) { - QJsonObject nodeObject = nodeJson.toObject(); - NodeId const oldNodeId = jsonValueToNodeId(nodeObject["id"]); + createdNodeIds.push_back(newNodeId); - NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, false); - NodeId const newNodeId = nodeRef.nodeId(); + if (oldNodeId != InvalidNodeId) { + nodeIdMap.emplace(oldNodeId, newNodeId); + IDsMap.emplace(static_cast(oldNodeId), static_cast(newNodeId)); + } - if (oldNodeId != InvalidNodeId) { - nodeIdMap.emplace(oldNodeId, newNodeId); - IDsMap.emplace(static_cast(oldNodeId), static_cast(newNodeId)); + group_children.push_back(&nodeRef); } - group_children.push_back(&nodeRef); - } + QJsonArray connectionJsonArray = groupJson["connections"].toArray(); + for (auto connection : connectionJsonArray) { + loadConnectionToMap(connection.toObject(), nodeIdMap); + } - QJsonArray connectionJsonArray = groupJson["connections"].toArray(); - for (auto connection : connectionJsonArray) { - loadConnectionToMap(connection.toObject(), nodeIdMap); - } + return std::make_pair(createGroup(group_children, groupJson["name"].toString()), IDsMap); + } catch (...) { + for (NodeId const nodeId : createdNodeIds) { + if (_graphModel.nodeExists(nodeId)) { + _graphModel.deleteNode(nodeId); + } + } - return std::make_pair(createGroup(group_children, groupJson["name"].toString()), IDsMap); + throw; + } } std::unordered_map> const &BasicGraphicsScene::groups() const @@ -826,9 +866,25 @@ std::weak_ptr BasicGraphicsScene::loadGroupFile() QByteArray wholeFile = file.readAll(); - const QJsonObject fileJson = QJsonDocument::fromJson(wholeFile).object(); + QJsonParseError parseError{}; + QJsonDocument const groupDocument = QJsonDocument::fromJson(wholeFile, &parseError); + if (parseError.error != QJsonParseError::NoError || !groupDocument.isObject()) { + return std::weak_ptr(); + } + + const QJsonObject fileJson = groupDocument.object(); - return restoreGroup(fileJson).first; + try { + validate_group_json(fileJson); + } catch (...) { + return std::weak_ptr(); + } + + try { + return restoreGroup(fileJson).first; + } catch (...) { + return std::weak_ptr(); + } } GroupId BasicGraphicsScene::nextGroupId() diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index 4127c4b2..306396d8 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace QtNodes { @@ -17,13 +18,24 @@ namespace { NodeId json_value_to_node_id(QJsonValue const &value) { - bool ok = false; - quint64 const parsed = value.toVariant().toULongLong(&ok); - if (!ok || parsed >= InvalidNodeId) { + NodeId nodeId = InvalidNodeId; + + if (!detail::read_node_id(value, nodeId)) { return InvalidNodeId; } - return static_cast(parsed); + return nodeId; +} + +QPointF json_object_to_point(QJsonObject const &obj, QString const &key) +{ + QPointF point; + + if (!detail::read_required_point(obj, key, point)) { + throw std::logic_error("Invalid node position in serialized node"); + } + + return point; } } // namespace @@ -668,11 +680,25 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) throw std::logic_error("Invalid node id in serialized node"); } - _nextNodeId = std::max(_nextNodeId, restoredNodeId + 1); + if (_models.find(restoredNodeId) != _models.end()) { + throw std::logic_error("Node identifier collision in serialized node"); + } - QJsonObject const internalDataJson = nodeJson["internal-data"].toObject(); + quint64 const nextNodeIdCandidate = static_cast(restoredNodeId) + 1ull; + _nextNodeId = std::max(_nextNodeId, static_cast(nextNodeIdCandidate)); - QString delegateModelName = internalDataJson["model-name"].toString(); + QJsonObject internalDataJson; + if (!detail::read_required_object(nodeJson, "internal-data", internalDataJson)) { + throw std::logic_error("Missing internal-data object in serialized node"); + } + + QString delegateModelName; + if (!detail::read_required_string(internalDataJson, "model-name", delegateModelName) + || delegateModelName.isEmpty()) { + throw std::logic_error("Missing model-name in serialized node"); + } + + QPointF const pos = json_object_to_point(nodeJson, "position"); std::unique_ptr model = _registry->create(delegateModelName); @@ -683,12 +709,14 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) Q_EMIT nodeCreated(restoredNodeId); - QJsonObject posJson = nodeJson["position"].toObject(); - QPointF const pos(posJson["x"].toDouble(), posJson["y"].toDouble()); - setNodeData(restoredNodeId, NodeRole::Position, pos); - _models[restoredNodeId]->load(internalDataJson); + try { + _models[restoredNodeId]->load(internalDataJson); + } catch (...) { + deleteNode(restoredNodeId); + throw; + } } else { throw std::logic_error(std::string("No registered model with name ") + delegateModelName.toLocal8Bit().data()); @@ -697,21 +725,70 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) void DataFlowGraphModel::load(QJsonObject const &jsonDocument) { - QJsonArray nodesJsonArray = jsonDocument["nodes"].toArray(); + QJsonArray nodesJsonArray; + if (!detail::read_required_array(jsonDocument, "nodes", nodesJsonArray)) { + throw std::logic_error("Serialized graph is missing nodes array"); + } + + QJsonArray connectionJsonArray; + if (!detail::read_required_array(jsonDocument, "connections", connectionJsonArray)) { + throw std::logic_error("Serialized graph is missing connections array"); + } - for (QJsonValueRef nodeJson : nodesJsonArray) { - loadNode(nodeJson.toObject()); + for (QJsonValue const &nodeJson : nodesJsonArray) { + if (!nodeJson.isObject()) { + throw std::logic_error("Serialized graph contains invalid node entry"); + } } - QJsonArray connectionJsonArray = jsonDocument["connections"].toArray(); + std::vector parsedConnections; + parsedConnections.reserve(connectionJsonArray.size()); - for (QJsonValueRef connection : connectionJsonArray) { - QJsonObject connJson = connection.toObject(); + for (QJsonValue const &connection : connectionJsonArray) { + if (!connection.isObject()) { + throw std::logic_error("Serialized graph contains invalid connection entry"); + } + + ConnectionId connId; + if (!tryFromJson(connection.toObject(), connId)) { + throw std::logic_error("Serialized graph contains invalid connection id"); + } - ConnectionId connId = fromJson(connJson); + parsedConnections.push_back(connId); + } + + std::vector loadedNodeIds; + loadedNodeIds.reserve(nodesJsonArray.size()); + std::vector loadedConnections; + loadedConnections.reserve(parsedConnections.size()); + + try { + for (QJsonValueRef nodeJson : nodesJsonArray) { + NodeId const nodeId = json_value_to_node_id(nodeJson.toObject()["id"]); + loadNode(nodeJson.toObject()); + loadedNodeIds.push_back(nodeId); + } + + for (ConnectionId const connId : parsedConnections) { + if (!connectionPossible(connId)) { + throw std::logic_error("Serialized graph contains invalid connection"); + } + + addConnection(connId); + loadedConnections.push_back(connId); + } + } catch (...) { + for (auto it = loadedConnections.rbegin(); it != loadedConnections.rend(); ++it) { + deleteConnection(*it); + } + + for (auto it = loadedNodeIds.rbegin(); it != loadedNodeIds.rend(); ++it) { + if (nodeExists(*it)) { + deleteNode(*it); + } + } - // Restore the connection - addConnection(connId); + throw; } } diff --git a/src/DataFlowGraphicsScene.cpp b/src/DataFlowGraphicsScene.cpp index 5a0e390c..a19add6f 100644 --- a/src/DataFlowGraphicsScene.cpp +++ b/src/DataFlowGraphicsScene.cpp @@ -4,6 +4,7 @@ #include "GraphicsView.hpp" #include "NodeDelegateModelRegistry.hpp" #include "NodeGraphicsObject.hpp" +#include "SerializationValidation.hpp" #include "UndoCommands.hpp" #include @@ -15,7 +16,6 @@ #include #include -#include #include #include #include @@ -23,10 +23,10 @@ #include #include #include -#include #include #include +#include #include #include @@ -34,35 +34,91 @@ namespace { using QtNodes::GroupId; using QtNodes::InvalidGroupId; +using QtNodes::InvalidNodeId; +using QtNodes::NodeId; -GroupId jsonValueToGroupId(QJsonValue const &value) +std::unordered_set collect_node_ids(QJsonArray const &nodesJsonArray) { - if (value.isDouble()) { - return static_cast(value.toInt()); + std::unordered_set nodeIds; + nodeIds.reserve(nodesJsonArray.size()); + + for (QJsonValue const &nodeValue : nodesJsonArray) { + if (!nodeValue.isObject()) { + throw std::logic_error("Serialized scene contains invalid node entry"); + } + + NodeId nodeId = InvalidNodeId; + if (!QtNodes::detail::read_node_id(nodeValue.toObject()["id"], nodeId)) { + throw std::logic_error("Serialized scene contains invalid node id"); + } + + if (!nodeIds.insert(nodeId).second) { + throw std::logic_error("Serialized scene contains duplicate node ids"); + } } - if (value.isString()) { - auto const textValue = value.toString(); + return nodeIds; +} + +void validate_groups_json(QJsonObject const &sceneJson) +{ + if (!sceneJson.contains("groups")) { + return; + } - bool ok = false; - auto const numericValue = textValue.toULongLong(&ok, 10); - if (ok) { - return static_cast(numericValue); + QJsonArray groupsJsonArray; + if (!QtNodes::detail::read_required_array(sceneJson, "groups", groupsJsonArray)) { + throw std::logic_error("Serialized scene contains invalid groups array"); + } + + QJsonArray nodesJsonArray; + if (!QtNodes::detail::read_required_array(sceneJson, "nodes", nodesJsonArray)) { + throw std::logic_error("Serialized scene is missing nodes array"); + } + + std::unordered_set const nodeIds = collect_node_ids(nodesJsonArray); + std::unordered_set seenGroupIds; + + for (QJsonValue const &groupValue : groupsJsonArray) { + if (!groupValue.isObject()) { + throw std::logic_error("Serialized scene contains invalid group entry"); } - QUuid uuidValue(textValue); - if (!uuidValue.isNull()) { - auto const bytes = uuidValue.toRfc4122(); - if (bytes.size() >= static_cast(sizeof(quint32))) { - QDataStream stream(bytes); - quint32 value32 = 0U; - stream >> value32; - return static_cast(value32); + QJsonObject const groupObject = groupValue.toObject(); + + GroupId groupId = InvalidGroupId; + if (!QtNodes::detail::read_group_id(groupObject["id"], groupId)) { + throw std::logic_error("Serialized scene contains invalid group id"); + } + + if (!seenGroupIds.insert(groupId).second) { + throw std::logic_error("Serialized scene contains duplicate group ids"); + } + + QString groupName; + if (!QtNodes::detail::read_required_string(groupObject, "name", groupName)) { + throw std::logic_error("Serialized scene contains invalid group name"); + } + Q_UNUSED(groupName); + + QJsonArray nodeIdsJson; + if (!QtNodes::detail::read_required_array(groupObject, "nodes", nodeIdsJson)) { + throw std::logic_error("Serialized scene contains invalid group nodes"); + } + + bool locked = true; + if (!QtNodes::detail::read_optional_bool(groupObject, "locked", locked)) { + throw std::logic_error("Serialized scene contains invalid group lock state"); + } + Q_UNUSED(locked); + + for (QJsonValue const &idValue : nodeIdsJson) { + NodeId nodeId = InvalidNodeId; + if (!QtNodes::detail::read_node_id(idValue, nodeId) || nodeIds.count(nodeId) == 0) { + throw std::logic_error("Serialized scene group references unknown node id"); } } } - - return InvalidGroupId; } } // namespace @@ -245,8 +301,6 @@ bool DataFlowGraphicsScene::load() if (!file.open(QIODevice::ReadOnly)) return false; - clearScene(); - QByteArray const wholeFile = file.readAll(); QJsonParseError parseError{}; @@ -256,7 +310,22 @@ bool DataFlowGraphicsScene::load() QJsonObject const sceneJson = sceneDocument.object(); - _graphModel.load(sceneJson); + try { + validate_groups_json(sceneJson); + + DataFlowGraphModel stagingModel(_graphModel.dataModelRegistry()); + stagingModel.load(sceneJson); + } catch (...) { + return false; + } + + clearScene(); + + try { + _graphModel.load(sceneJson); + } catch (...) { + return false; + } if (sceneJson.contains("groups")) { QJsonArray const groupsJsonArray = sceneJson["groups"].toArray(); @@ -269,7 +338,8 @@ bool DataFlowGraphicsScene::load() groupNodes.reserve(nodeIdsJson.size()); for (QJsonValue idValue : nodeIdsJson) { - NodeId const nodeId = static_cast(idValue.toInt()); + NodeId nodeId = InvalidNodeId; + detail::read_node_id(idValue, nodeId); if (auto *nodeObject = nodeGraphicsObject(nodeId)) { groupNodes.push_back(nodeObject); } @@ -279,7 +349,8 @@ bool DataFlowGraphicsScene::load() continue; QString const groupName = groupObject["name"].toString(); - GroupId const groupId = jsonValueToGroupId(groupObject["id"]); + GroupId groupId = InvalidGroupId; + detail::read_group_id(groupObject["id"], groupId); auto const groupWeak = createGroup(groupNodes, groupName, groupId); if (auto group = groupWeak.lock()) { From 270094ecedd9092ecb174c816b1c1652cc55b456 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 10:31:52 +0100 Subject: [PATCH 19/26] Tighten undo payload parsing and scene invariants --- .../QtNodes/internal/ConnectionIdUtils.hpp | 23 +++--- src/BasicGraphicsScene.cpp | 11 ++- src/ConnectionGraphicsObject.cpp | 4 +- src/NodeGraphicsObject.cpp | 4 +- src/UndoCommands.cpp | 74 +++++++++++++++---- 5 files changed, 83 insertions(+), 33 deletions(-) diff --git a/include/QtNodes/internal/ConnectionIdUtils.hpp b/include/QtNodes/internal/ConnectionIdUtils.hpp index e8286da3..a9e14cba 100644 --- a/include/QtNodes/internal/ConnectionIdUtils.hpp +++ b/include/QtNodes/internal/ConnectionIdUtils.hpp @@ -4,6 +4,7 @@ #include "SerializationValidation.hpp" #include +#include #include #include @@ -128,18 +129,6 @@ inline QJsonObject toJson(ConnectionId const &connId) return connJson; } -inline ConnectionId fromJson(QJsonObject const &connJson) -{ - ConnectionId connId{InvalidNodeId, InvalidPortIndex, InvalidNodeId, InvalidPortIndex}; - - detail::read_node_id(connJson["outNodeId"], connId.outNodeId); - detail::read_port_index(connJson["outPortIndex"], connId.outPortIndex); - detail::read_node_id(connJson["inNodeId"], connId.inNodeId); - detail::read_port_index(connJson["inPortIndex"], connId.inPortIndex); - - return connId; -} - inline bool tryFromJson(QJsonObject const &connJson, ConnectionId &connId) { NodeId outNodeId = InvalidNodeId; @@ -158,6 +147,16 @@ inline bool tryFromJson(QJsonObject const &connJson, ConnectionId &connId) return true; } +inline ConnectionId fromJson(QJsonObject const &connJson) +{ + ConnectionId connId{InvalidNodeId, InvalidPortIndex, InvalidNodeId, InvalidPortIndex}; + + bool const ok = tryFromJson(connJson, connId); + Q_ASSERT(ok); + + return connId; +} + inline NodeRole portCountRole(PortType portType) { return (portType == PortType::Out) ? NodeRole::OutPortCount : NodeRole::InPortCount; diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index 086b070e..84328a8e 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -874,15 +875,13 @@ std::weak_ptr BasicGraphicsScene::loadGroupFile() const QJsonObject fileJson = groupDocument.object(); - try { - validate_group_json(fileJson); - } catch (...) { - return std::weak_ptr(); - } - try { return restoreGroup(fileJson).first; + } catch (std::exception const &ex) { + qWarning() << "Failed to load group file:" << ex.what(); + return std::weak_ptr(); } catch (...) { + qWarning() << "Failed to load group file due to an unknown error"; return std::weak_ptr(); } } diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp index 9abf21fb..fc6b8008 100644 --- a/src/ConnectionGraphicsObject.cpp +++ b/src/ConnectionGraphicsObject.cpp @@ -88,7 +88,9 @@ AbstractGraphModel &ConnectionGraphicsObject::graphModel() const BasicGraphicsScene *ConnectionGraphicsObject::nodeScene() const { - return dynamic_cast(scene()); + auto *sceneObject = dynamic_cast(scene()); + Q_ASSERT(sceneObject != nullptr); + return sceneObject; } ConnectionId const &ConnectionGraphicsObject::connectionId() const diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 3df7477c..6f5698e3 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -125,7 +125,9 @@ AbstractGraphModel &NodeGraphicsObject::graphModel() const BasicGraphicsScene *NodeGraphicsObject::nodeScene() const { - return dynamic_cast(scene()); + auto *sceneObject = dynamic_cast(scene()); + Q_ASSERT(sceneObject != nullptr); + return sceneObject; } void NodeGraphicsObject::updateQWidgetEmbedPos() diff --git a/src/UndoCommands.cpp b/src/UndoCommands.cpp index 200690ef..150199a2 100644 --- a/src/UndoCommands.cpp +++ b/src/UndoCommands.cpp @@ -6,7 +6,9 @@ #include "Definitions.hpp" #include "GroupGraphicsObject.hpp" #include "NodeGraphicsObject.hpp" +#include "SerializationValidation.hpp" +#include #include #include #include @@ -17,6 +19,43 @@ namespace QtNodes { +namespace { + +NodeId json_value_to_node_id(QJsonValue const &value) +{ + NodeId nodeId = InvalidNodeId; + + if (!detail::read_node_id(value, nodeId)) { + throw std::logic_error("Invalid node id in serialized command payload"); + } + + return nodeId; +} + +ConnectionId connection_id_from_json(QJsonObject const &connJson) +{ + ConnectionId connId; + + if (!tryFromJson(connJson, connId)) { + throw std::logic_error("Invalid connection id in serialized command payload"); + } + + return connId; +} + +QPointF point_from_json(QJsonObject const &json, QString const &key) +{ + QPointF point; + + if (!detail::read_required_point(json, key, point)) { + throw std::logic_error("Invalid node position in serialized command payload"); + } + + return point; +} + +} // namespace + static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) { QJsonObject serializedScene; @@ -78,6 +117,7 @@ static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene) QJsonObject groupJson; groupJson["id"] = static_cast(group.id()); groupJson["name"] = group.name(); + groupJson["locked"] = groupGo->locked(); QJsonArray nodeIdsJson; for (NodeGraphicsObject *node : group.childNodes()) { @@ -113,7 +153,7 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s graphModel.loadNode(obj); - auto id = obj["id"].toInt(); + auto id = json_value_to_node_id(obj["id"]); if (auto *nodeObject = scene->nodeGraphicsObject(id)) { nodeObject->setZValue(1.0); nodeObject->setSelected(true); @@ -125,7 +165,7 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s for (QJsonValue connection : connJsonArray) { QJsonObject connJson = connection.toObject(); - ConnectionId connId = fromJson(connJson); + ConnectionId connId = connection_id_from_json(connJson); // Restore the connection graphModel.addConnection(connId); @@ -141,19 +181,25 @@ static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *s for (const QJsonValue &groupValue : groupsJsonArray) { QJsonObject groupJson = groupValue.toObject(); - QString name = QString("Group %1").arg(NodeGroup::groupCount()); + QString name = groupJson["name"].toString(); + if (name.isEmpty()) { + name = QString("Group %1").arg(NodeGroup::groupCount()); + } QJsonArray nodeIdsJson = groupJson["nodes"].toArray(); std::vector groupNodes; for (const QJsonValue &idVal : nodeIdsJson) { - NodeId nodeId = static_cast(idVal.toInt()); + NodeId nodeId = json_value_to_node_id(idVal); if (auto *ngo = scene->nodeGraphicsObject(nodeId)) { groupNodes.push_back(ngo); } } - scene->createGroup(groupNodes, name); + auto const groupWeak = scene->createGroup(groupNodes, name); + if (auto group = groupWeak.lock()) { + group->groupGraphicsObject().lock(groupJson["locked"].toBool(true)); + } } } } @@ -165,7 +211,7 @@ static void deleteSerializedItems(QJsonObject &sceneJson, AbstractGraphModel &gr for (QJsonValueRef connection : connectionJsonArray) { QJsonObject connJson = connection.toObject(); - ConnectionId connId = fromJson(connJson); + ConnectionId connId = connection_id_from_json(connJson); graphModel.deleteConnection(connId); } @@ -174,7 +220,7 @@ static void deleteSerializedItems(QJsonObject &sceneJson, AbstractGraphModel &gr for (QJsonValueRef node : nodesJsonArray) { QJsonObject nodeJson = node.toObject(); - graphModel.deleteNode(nodeJson["id"].toInt()); + graphModel.deleteNode(json_value_to_node_id(nodeJson["id"])); } } @@ -183,12 +229,13 @@ static QPointF computeAverageNodePosition(QJsonObject const &sceneJson) QPointF averagePos(0, 0); QJsonArray nodesJsonArray = sceneJson["nodes"].toArray(); + if (nodesJsonArray.isEmpty()) { + return averagePos; + } for (QJsonValueRef node : nodesJsonArray) { QJsonObject nodeJson = node.toObject(); - - averagePos += QPointF(nodeJson["position"].toObject()["x"].toDouble(), - nodeJson["position"].toObject()["y"].toDouble()); + averagePos += point_from_json(nodeJson, "position"); } averagePos /= static_cast(nodesJsonArray.size()); @@ -284,6 +331,7 @@ DeleteCommand::DeleteCommand(BasicGraphicsScene *scene) QJsonObject groupJson; groupJson["id"] = static_cast(groupData.id()); groupJson["name"] = groupData.name(); + groupJson["locked"] = groupGo->locked(); groupJson["nodes"] = groupNodeIdsJsonArray; groupsJsonArray.append(groupJson); } @@ -450,7 +498,7 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) for (QJsonValueRef node : nodesJsonArray) { QJsonObject nodeJson = node.toObject(); - NodeId oldNodeId = nodeJson["id"].toInt(); + NodeId oldNodeId = json_value_to_node_id(nodeJson["id"]); NodeId newNodeId = graphModel.newNodeId(); @@ -468,7 +516,7 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) for (QJsonValueRef connection : connectionJsonArray) { QJsonObject connJson = connection.toObject(); - ConnectionId connId = fromJson(connJson); + ConnectionId connId = connection_id_from_json(connJson); auto const outIt = mapNodeIds.find(connId.outNodeId); auto const inIt = mapNodeIds.find(connId.inNodeId); @@ -499,7 +547,7 @@ QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson) QJsonArray newNodeIdsJson; for (const QJsonValue &idVal : nodeIdsJson) { - NodeId oldId = static_cast(idVal.toInt()); + NodeId oldId = json_value_to_node_id(idVal); auto const nodeIt = mapNodeIds.find(oldId); if (nodeIt != mapNodeIds.end()) { newNodeIdsJson.append(static_cast(nodeIt->second)); From a38f450ad8f43f05c29a6cbbc2a930efb27b36b2 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 10:46:37 +0100 Subject: [PATCH 20/26] Clean up package metadata and geometry naming --- CMakeLists.txt | 18 ++++++++---- .../internal/DefaultNodeGeometryBase.hpp | 2 +- include/QtNodes/internal/Definitions.hpp | 6 ++-- src/DefaultHorizontalNodeGeometry.cpp | 26 ++++++++--------- src/DefaultNodeGeometryBase.cpp | 4 +-- src/DefaultVerticalNodeGeometry.cpp | 28 +++++++++---------- 6 files changed, 45 insertions(+), 39 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cc043857..498ad3b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_policy(SET CMP0072 NEW) # new in 3.11. The NEW behavior for this policy is cmake_policy(SET CMP0068 NEW) # new in 3.9. The NEW behavior of this policy is to ignore the RPATH settings for install_name on macOS. -project(QtNodesLibrary CXX) +project(QtNodesLibrary VERSION 3.0.16 LANGUAGES CXX) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) @@ -191,14 +191,14 @@ if(NOT "${CMAKE_CXX_SIMULATE_ID}" STREQUAL "MSVC") ) endif() -if(QT_NODES_DEVELOPER_DEFAULTS) - target_compile_features(QtNodes PUBLIC cxx_std_14) - set_target_properties(QtNodes PROPERTIES CXX_EXTENSIONS OFF) -endif() +target_compile_features(QtNodes PUBLIC cxx_std_14) +set_target_properties(QtNodes PROPERTIES CXX_EXTENSIONS OFF) set_target_properties(QtNodes PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin @@ -236,7 +236,6 @@ set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/QtNodes) install(TARGETS QtNodes EXPORT QtNodesTargets - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} @@ -258,7 +257,14 @@ configure_package_config_file(${CMAKE_CURRENT_LIST_DIR}/cmake/QtNodesConfig.cmak INSTALL_DESTINATION ${INSTALL_CONFIGDIR} ) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/QtNodesConfigVersion.cmake DESTINATION ${INSTALL_CONFIGDIR} ) diff --git a/include/QtNodes/internal/DefaultNodeGeometryBase.hpp b/include/QtNodes/internal/DefaultNodeGeometryBase.hpp index 704285ca..a5072121 100644 --- a/include/QtNodes/internal/DefaultNodeGeometryBase.hpp +++ b/include/QtNodes/internal/DefaultNodeGeometryBase.hpp @@ -37,7 +37,7 @@ class NODE_EDITOR_PUBLIC DefaultNodeGeometryBase : public AbstractNodeGeometry protected: mutable unsigned int _portSize; - unsigned int _portSpasing; + unsigned int _portSpacing; mutable QFontMetrics _fontMetrics; mutable QFontMetrics _boldFontMetrics; }; diff --git a/include/QtNodes/internal/Definitions.hpp b/include/QtNodes/internal/Definitions.hpp index fc4ffc26..be35a7fd 100644 --- a/include/QtNodes/internal/Definitions.hpp +++ b/include/QtNodes/internal/Definitions.hpp @@ -23,12 +23,12 @@ Q_NAMESPACE_EXPORT(NODE_EDITOR_PUBLIC) */ enum class NodeRole { Type = 0, ///< Type of the current node, usually a string. - Position = 1, ///< `QPointF` positon of the node on the scene. + Position = 1, ///< `QPointF` position of the node on the scene. Size = 2, ///< `QSize` for resizable nodes. CaptionVisible = 3, ///< `bool` for caption visibility. Caption = 4, ///< `QString` for node caption. Style = 5, ///< Custom NodeStyle as QJsonDocument - InternalData = 6, ///< Node-stecific user data as QJsonObject + InternalData = 6, ///< Node-specific user data as QJsonObject InPortCount = 7, ///< `unsigned int` OutPortCount = 9, ///< `unsigned int` Widget = 10, ///< Optional `QWidget*` or `nullptr` @@ -38,7 +38,7 @@ Q_NAMESPACE_EXPORT(NODE_EDITOR_PUBLIC) Q_ENUM_NS(NodeRole) /** - * Specific flags regulating node features and appeaarence. + * Specific flags regulating node features and appearance. */ enum NodeFlag { NoFlags = 0x0, ///< Default NodeFlag diff --git a/src/DefaultHorizontalNodeGeometry.cpp b/src/DefaultHorizontalNodeGeometry.cpp index 1e63a384..d9424614 100644 --- a/src/DefaultHorizontalNodeGeometry.cpp +++ b/src/DefaultHorizontalNodeGeometry.cpp @@ -24,8 +24,8 @@ void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const height += capRect.height(); - height += _portSpasing; // space above caption - height += _portSpasing; // space below caption + height += _portSpacing; // space above caption + height += _portSpacing; // space below caption QVariant var = _graphModel.nodeData(nodeId, NodeRole::ProcessingStatus); auto processingStatusValue = var.value(); @@ -36,13 +36,13 @@ void DefaultHorizontalNodeGeometry::recomputeSize(NodeId const nodeId) const unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In); unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out); - unsigned int width = inPortWidth + outPortWidth + 4 * _portSpasing; + unsigned int width = inPortWidth + outPortWidth + 4 * _portSpacing; if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { width += w->width(); } - width = std::max(width, static_cast(capRect.width()) + 2 * _portSpasing); + width = std::max(width, static_cast(capRect.width()) + 2 * _portSpacing); QSize size(width, height); @@ -53,14 +53,14 @@ QPointF DefaultHorizontalNodeGeometry::portPosition(NodeId const nodeId, PortType const portType, PortIndex const portIndex) const { - unsigned int const step = _portSize + _portSpasing; + unsigned int const step = _portSize + _portSpacing; QPointF result; double totalHeight = 0.0; totalHeight += captionRect(nodeId).height(); - totalHeight += _portSpasing; + totalHeight += _portSpacing; totalHeight += step * portIndex; totalHeight += step / 2.0; @@ -103,11 +103,11 @@ QPointF DefaultHorizontalNodeGeometry::portTextPosition(NodeId const nodeId, switch (portType) { case PortType::In: - p.setX(_portSpasing); + p.setX(_portSpacing); break; case PortType::Out: - p.setX(size.width() - _portSpasing - rect.width()); + p.setX(size.width() - _portSpacing - rect.width()); break; default: @@ -121,7 +121,7 @@ QPointF DefaultHorizontalNodeGeometry::captionPosition(NodeId const nodeId) cons { QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); return QPointF(0.5 * (size.width() - captionRect(nodeId).width()), - 0.5 * _portSpasing + captionRect(nodeId).height()); + 0.5 * _portSpacing + captionRect(nodeId).height()); } QPointF DefaultHorizontalNodeGeometry::widgetPosition(NodeId const nodeId) const @@ -134,10 +134,10 @@ QPointF DefaultHorizontalNodeGeometry::widgetPosition(NodeId const nodeId) const // If the widget wants to use as much vertical space as possible, // place it immediately after the caption. if (w->sizePolicy().verticalPolicy() & QSizePolicy::ExpandFlag) { - return QPointF(2.0 * _portSpasing + maxPortsTextAdvance(nodeId, PortType::In), - _portSpasing + captionHeight); + return QPointF(2.0 * _portSpacing + maxPortsTextAdvance(nodeId, PortType::In), + _portSpacing + captionHeight); } else { - return QPointF(2.0 * _portSpasing + maxPortsTextAdvance(nodeId, PortType::In), + return QPointF(2.0 * _portSpacing + maxPortsTextAdvance(nodeId, PortType::In), (captionHeight + size.height() - w->height()) / 2.0); } } @@ -150,7 +150,7 @@ QRect DefaultHorizontalNodeGeometry::resizeHandleRect(NodeId const nodeId) const unsigned int rectSize = 7; - return QRect(size.width() - _portSpasing, size.height() - _portSpasing, rectSize, rectSize); + return QRect(size.width() - _portSpacing, size.height() - _portSpacing, rectSize, rectSize); } unsigned int DefaultHorizontalNodeGeometry::maxVerticalPortsExtent(NodeId const nodeId) const diff --git a/src/DefaultNodeGeometryBase.cpp b/src/DefaultNodeGeometryBase.cpp index e032e7c3..747c5adc 100644 --- a/src/DefaultNodeGeometryBase.cpp +++ b/src/DefaultNodeGeometryBase.cpp @@ -12,7 +12,7 @@ namespace QtNodes { DefaultNodeGeometryBase::DefaultNodeGeometryBase(AbstractGraphModel &graphModel) : AbstractNodeGeometry(graphModel) , _portSize(20) - , _portSpasing(10) + , _portSpacing(10) , _fontMetrics(QFont()) , _boldFontMetrics(QFont()) { @@ -73,7 +73,7 @@ unsigned int DefaultNodeGeometryBase::maxPortsExtent(NodeId const nodeId) const PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); unsigned int maxNumOfEntries = std::max(nInPorts, nOutPorts); - unsigned int step = _portSize + _portSpasing; + unsigned int step = _portSize + _portSpacing; return step * maxNumOfEntries; } diff --git a/src/DefaultVerticalNodeGeometry.cpp b/src/DefaultVerticalNodeGeometry.cpp index cb2e4cbd..cbeed285 100644 --- a/src/DefaultVerticalNodeGeometry.cpp +++ b/src/DefaultVerticalNodeGeometry.cpp @@ -14,7 +14,7 @@ DefaultVerticalNodeGeometry::DefaultVerticalNodeGeometry(AbstractGraphModel &gra void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const { - unsigned int height = _portSpasing; + unsigned int height = _portSpacing; if (auto w = _graphModel.nodeData(nodeId, NodeRole::Widget)) { height = std::max(height, static_cast(w->height())); @@ -24,8 +24,8 @@ void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const height += capRect.height(); - height += _portSpasing; - height += _portSpasing; + height += _portSpacing; + height += _portSpacing; PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); @@ -39,11 +39,11 @@ void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out); unsigned int totalInPortsWidth = nInPorts > 0 - ? inPortWidth * nInPorts + _portSpasing * (nInPorts - 1) + ? inPortWidth * nInPorts + _portSpacing * (nInPorts - 1) : 0; unsigned int totalOutPortsWidth = nOutPorts > 0 ? outPortWidth * nOutPorts - + _portSpasing * (nOutPorts - 1) + + _portSpacing * (nOutPorts - 1) : 0; unsigned int width = std::max(totalInPortsWidth, totalOutPortsWidth); @@ -54,8 +54,8 @@ void DefaultVerticalNodeGeometry::recomputeSize(NodeId const nodeId) const width = std::max(width, static_cast(capRect.width())); - width += _portSpasing; - width += _portSpasing; + width += _portSpacing; + width += _portSpacing; QSize size(width, height); @@ -72,7 +72,7 @@ QPointF DefaultVerticalNodeGeometry::portPosition(NodeId const nodeId, switch (portType) { case PortType::In: { - unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In) + _portSpasing; + unsigned int inPortWidth = maxPortsTextAdvance(nodeId, PortType::In) + _portSpacing; PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); @@ -86,7 +86,7 @@ QPointF DefaultVerticalNodeGeometry::portPosition(NodeId const nodeId, } case PortType::Out: { - unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out) + _portSpasing; + unsigned int outPortWidth = maxPortsTextAdvance(nodeId, PortType::Out) + _portSpacing; PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); double x = (size.width() - (nOutPorts - 1) * outPortWidth) / 2.0 + portIndex * outPortWidth; @@ -138,7 +138,7 @@ QPointF DefaultVerticalNodeGeometry::captionPosition(NodeId const nodeId) const QSize size = _graphModel.nodeData(nodeId, NodeRole::Size); unsigned int step = portCaptionsHeight(nodeId, PortType::In); - step += _portSpasing; + step += _portSpacing; auto rect = captionRect(nodeId); @@ -155,9 +155,9 @@ QPointF DefaultVerticalNodeGeometry::widgetPosition(NodeId const nodeId) const // If the widget wants to use as much vertical space as possible, // place it immediately after the caption. if (w->sizePolicy().verticalPolicy() & QSizePolicy::ExpandFlag) { - return QPointF(_portSpasing + maxPortsTextAdvance(nodeId, PortType::In), captionHeight); + return QPointF(_portSpacing + maxPortsTextAdvance(nodeId, PortType::In), captionHeight); } else { - return QPointF(_portSpasing + maxPortsTextAdvance(nodeId, PortType::In), + return QPointF(_portSpacing + maxPortsTextAdvance(nodeId, PortType::In), (captionHeight + size.height() - w->height()) / 2.0); } } @@ -188,7 +188,7 @@ unsigned int DefaultVerticalNodeGeometry::portCaptionsHeight(NodeId const nodeId PortCount nInPorts = _graphModel.nodeData(nodeId, NodeRole::InPortCount); for (PortIndex i = 0; i < nInPorts; ++i) { if (_graphModel.portData(nodeId, PortType::In, i, PortRole::CaptionVisible)) { - h += _portSpasing; + h += _portSpacing; break; } } @@ -199,7 +199,7 @@ unsigned int DefaultVerticalNodeGeometry::portCaptionsHeight(NodeId const nodeId PortCount nOutPorts = _graphModel.nodeData(nodeId, NodeRole::OutPortCount); for (PortIndex i = 0; i < nOutPorts; ++i) { if (_graphModel.portData(nodeId, PortType::Out, i, PortRole::CaptionVisible)) { - h += _portSpasing; + h += _portSpacing; break; } } From 55bbed12c35bbe4c7ba2886e00a4120bc7e4b93f Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 12:10:54 +0100 Subject: [PATCH 21/26] Refine graph model lookup APIs and scene loading --- docs/getting-started/quickstart.rst | 4 +- docs/guide/graph-models.rst | 6 +- examples/custom_painter/SimpleGraphModel.cpp | 119 +++++++++++--- examples/custom_painter/SimpleGraphModel.hpp | 26 +++- examples/dynamic_ports/DynamicPortsModel.cpp | 120 +++++++++++---- examples/dynamic_ports/DynamicPortsModel.hpp | 26 +++- .../simple_graph_model/SimpleGraphModel.cpp | 120 +++++++++++---- .../simple_graph_model/SimpleGraphModel.hpp | 26 +++- examples/vertical_layout/SimpleGraphModel.cpp | 121 +++++++++++---- examples/vertical_layout/SimpleGraphModel.hpp | 26 +++- .../QtNodes/internal/AbstractGraphModel.hpp | 32 ++-- .../QtNodes/internal/BasicGraphicsScene.hpp | 6 +- .../QtNodes/internal/DataFlowGraphModel.hpp | 22 +-- .../QtNodes/internal/GroupGraphicsObject.hpp | 8 +- .../internal/NodeDelegateModelRegistry.hpp | 2 +- src/AbstractGraphModel.cpp | 29 +++- src/BasicGraphicsScene.cpp | 22 ++- src/DataFlowGraphModel.cpp | 40 +++-- src/DataFlowGraphicsScene.cpp | 145 +++++++++++------- src/GraphicsView.cpp | 13 ++ src/GroupGraphicsObject.cpp | 2 +- src/NodeGroup.cpp | 4 +- test/include/TestGraphModel.hpp | 128 ++++++++++++---- 23 files changed, 763 insertions(+), 284 deletions(-) diff --git a/docs/getting-started/quickstart.rst b/docs/getting-started/quickstart.rst index fee0b883..dd9be189 100644 --- a/docs/getting-started/quickstart.rst +++ b/docs/getting-started/quickstart.rst @@ -39,10 +39,10 @@ First, we need a class to store our graph data. Create ``SimpleGraphModel.hpp``: QtNodes::NodeId newNodeId() override { return _nextId++; } // Required: Return all node IDs - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; // Required: Return all connections for a node - std::unordered_set allConnectionIds(QtNodes::NodeId) const override; + ConnectionIdSet const &allConnectionIds(QtNodes::NodeId) const override; // ... (see full implementation in examples/simple_graph_model) diff --git a/docs/guide/graph-models.rst b/docs/guide/graph-models.rst index 3ce60bbc..e6dec43d 100644 --- a/docs/guide/graph-models.rst +++ b/docs/guide/graph-models.rst @@ -24,14 +24,14 @@ Your model must implement these pure virtual methods: NodeId newNodeId() override; // Node queries - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; bool nodeExists(NodeId) const override; QVariant nodeData(NodeId, NodeRole) const override; bool setNodeData(NodeId, NodeRole, QVariant) override; // Connection queries - std::unordered_set allConnectionIds(NodeId) const override; - std::unordered_set connections(NodeId, PortType, PortIndex) const override; + ConnectionIdSet const &allConnectionIds(NodeId) const override; + ConnectionIdSet const &connections(NodeId, PortType, PortIndex) const override; bool connectionExists(ConnectionId) const override; bool connectionPossible(ConnectionId) const override; diff --git a/examples/custom_painter/SimpleGraphModel.cpp b/examples/custom_painter/SimpleGraphModel.cpp index 760cea69..d13fdd96 100644 --- a/examples/custom_painter/SimpleGraphModel.cpp +++ b/examples/custom_painter/SimpleGraphModel.cpp @@ -1,45 +1,49 @@ #include "SimpleGraphModel.hpp" +#include + SimpleGraphModel::SimpleGraphModel() : _nextNodeId{0} {} SimpleGraphModel::~SimpleGraphModel() {} -std::unordered_set SimpleGraphModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() const { return _nodeIds; } -std::unordered_set SimpleGraphModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); + auto const it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return emptyConnections(); + } - return result; + return it->second; } -std::unordered_set SimpleGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; + if (portType == PortType::None) { + return emptyConnections(); + } - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto const nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return emptyConnections(); + } - return result; + auto const portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return emptyConnections(); + } + + return portIt->second; } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const @@ -65,6 +69,7 @@ bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const void SimpleGraphModel::addConnection(ConnectionId const connectionId) { _connectivity.insert(connectionId); + indexConnection(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -195,6 +200,7 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) if (it != _connectivity.end()) { disconnected = true; _connectivity.erase(it); + unindexConnection(connectionId); } if (disconnected) @@ -205,9 +211,14 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) bool SimpleGraphModel::deleteNode(NodeId const nodeId) { - auto connectionIds = allConnectionIds(nodeId); + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } - for (auto &cId : connectionIds) { + for (auto const &cId : connectionIds) { deleteConnection(cId); } @@ -254,3 +265,61 @@ void SimpleGraphModel::loadNode(QJsonObject const &nodeJson) setNodeData(restoredNodeId, NodeRole::Position, pos); } } + +QtNodes::AbstractGraphModel::ConnectionIdSet const &SimpleGraphModel::emptyConnections() +{ + static ConnectionIdSet const empty{}; + return empty; +} + +void SimpleGraphModel::indexConnection(ConnectionId const connectionId) +{ + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); +} + +void SimpleGraphModel::unindexConnection(ConnectionId const connectionId) +{ + auto eraseFromNode = [&](NodeId nodeId) { + auto nodeIt = _nodeConnections.find(nodeId); + if (nodeIt == _nodeConnections.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + _nodeConnections.erase(nodeIt); + } + }; + + auto eraseFromPortMap = + [&](std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex) { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + }; + + eraseFromNode(connectionId.inNodeId); + eraseFromNode(connectionId.outNodeId); + eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); + eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); +} diff --git a/examples/custom_painter/SimpleGraphModel.hpp b/examples/custom_painter/SimpleGraphModel.hpp index fccf99cb..3f6cd841 100644 --- a/examples/custom_painter/SimpleGraphModel.hpp +++ b/examples/custom_painter/SimpleGraphModel.hpp @@ -8,6 +8,8 @@ #include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -35,13 +37,13 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel ~SimpleGraphModel() override; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -79,8 +81,18 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; - std::unordered_set _connectivity; + using ConnectionsByPort = std::unordered_map; + + static ConnectionIdSet const &emptyConnections(); + + void indexConnection(ConnectionId const connectionId); + void unindexConnection(ConnectionId const connectionId); + + NodeIdSet _nodeIds; + ConnectionIdSet _connectivity; + std::unordered_map _nodeConnections; + std::unordered_map _inConnectionsByPort; + std::unordered_map _outConnectionsByPort; mutable std::unordered_map _nodeGeometryData; NodeId _nextNodeId; }; diff --git a/examples/dynamic_ports/DynamicPortsModel.cpp b/examples/dynamic_ports/DynamicPortsModel.cpp index 74288e7a..e1b28f2f 100644 --- a/examples/dynamic_ports/DynamicPortsModel.cpp +++ b/examples/dynamic_ports/DynamicPortsModel.cpp @@ -7,46 +7,49 @@ #include #include +#include DynamicPortsModel::DynamicPortsModel() : _nextNodeId{0} {} -std::unordered_set DynamicPortsModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &DynamicPortsModel::allNodeIds() const { return _nodeIds; } -std::unordered_set DynamicPortsModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +DynamicPortsModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); + auto const it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return emptyConnections(); + } - return result; + return it->second; } -std::unordered_set DynamicPortsModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +DynamicPortsModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; + if (portType == PortType::None) { + return emptyConnections(); + } - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto const nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return emptyConnections(); + } - return result; + auto const portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return emptyConnections(); + } + + return portIt->second; } bool DynamicPortsModel::connectionExists(ConnectionId const connectionId) const @@ -74,6 +77,7 @@ bool DynamicPortsModel::connectionPossible(ConnectionId const connectionId) cons void DynamicPortsModel::addConnection(ConnectionId const connectionId) { _connectivity.insert(connectionId); + indexConnection(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -253,6 +257,7 @@ bool DynamicPortsModel::deleteConnection(ConnectionId const connectionId) disconnected = true; _connectivity.erase(it); + unindexConnection(connectionId); }; if (disconnected) @@ -263,9 +268,14 @@ bool DynamicPortsModel::deleteConnection(ConnectionId const connectionId) bool DynamicPortsModel::deleteNode(NodeId const nodeId) { - // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); - for (auto &cId : connectionIds) { + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } + + for (auto const &cId : connectionIds) { deleteConnection(cId); } @@ -404,3 +414,61 @@ void DynamicPortsModel::removePort(NodeId nodeId, PortType portType, PortIndex p Q_EMIT nodeUpdated(nodeId); } + +QtNodes::AbstractGraphModel::ConnectionIdSet const &DynamicPortsModel::emptyConnections() +{ + static ConnectionIdSet const empty{}; + return empty; +} + +void DynamicPortsModel::indexConnection(ConnectionId const connectionId) +{ + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); +} + +void DynamicPortsModel::unindexConnection(ConnectionId const connectionId) +{ + auto eraseFromNode = [&](NodeId nodeId) { + auto nodeIt = _nodeConnections.find(nodeId); + if (nodeIt == _nodeConnections.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + _nodeConnections.erase(nodeIt); + } + }; + + auto eraseFromPortMap = + [&](std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex) { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + }; + + eraseFromNode(connectionId.inNodeId); + eraseFromNode(connectionId.outNodeId); + eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); + eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); +} diff --git a/examples/dynamic_ports/DynamicPortsModel.hpp b/examples/dynamic_ports/DynamicPortsModel.hpp index ba2886eb..c88841a9 100644 --- a/examples/dynamic_ports/DynamicPortsModel.hpp +++ b/examples/dynamic_ports/DynamicPortsModel.hpp @@ -7,6 +7,8 @@ #include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -39,13 +41,13 @@ class DynamicPortsModel : public QtNodes::AbstractGraphModel ~DynamicPortsModel() override = default; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -100,13 +102,23 @@ class DynamicPortsModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; + using ConnectionsByPort = std::unordered_map; + + static ConnectionIdSet const &emptyConnections(); + + void indexConnection(ConnectionId const connectionId); + void unindexConnection(ConnectionId const connectionId); + + NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. /// In your case it could be anything else representing a graph, for example, a /// table. Or a collection of structs with pointers to each other. Or an /// abstract syntax tree, you name it. - std::unordered_set _connectivity; + ConnectionIdSet _connectivity; + std::unordered_map _nodeConnections; + std::unordered_map _inConnectionsByPort; + std::unordered_map _outConnectionsByPort; mutable std::unordered_map _nodeGeometryData; diff --git a/examples/simple_graph_model/SimpleGraphModel.cpp b/examples/simple_graph_model/SimpleGraphModel.cpp index 7c04440f..567b142e 100644 --- a/examples/simple_graph_model/SimpleGraphModel.cpp +++ b/examples/simple_graph_model/SimpleGraphModel.cpp @@ -1,5 +1,7 @@ #include "SimpleGraphModel.hpp" +#include + SimpleGraphModel::SimpleGraphModel() : _nextNodeId{0} {} @@ -9,40 +11,42 @@ SimpleGraphModel::~SimpleGraphModel() // } -std::unordered_set SimpleGraphModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() const { return _nodeIds; } -std::unordered_set SimpleGraphModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); + auto const it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return emptyConnections(); + } - return result; + return it->second; } -std::unordered_set SimpleGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; + if (portType == PortType::None) { + return emptyConnections(); + } - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto const nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return emptyConnections(); + } - return result; + auto const portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return emptyConnections(); + } + + return portIt->second; } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const @@ -69,6 +73,7 @@ bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const void SimpleGraphModel::addConnection(ConnectionId const connectionId) { _connectivity.insert(connectionId); + indexConnection(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -230,6 +235,7 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) disconnected = true; _connectivity.erase(it); + unindexConnection(connectionId); } if (disconnected) @@ -240,10 +246,14 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) bool SimpleGraphModel::deleteNode(NodeId const nodeId) { - // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } - for (auto &cId : connectionIds) { + for (auto const &cId : connectionIds) { deleteConnection(cId); } @@ -292,3 +302,61 @@ void SimpleGraphModel::loadNode(QJsonObject const &nodeJson) setNodeData(restoredNodeId, NodeRole::Position, pos); } } + +QtNodes::AbstractGraphModel::ConnectionIdSet const &SimpleGraphModel::emptyConnections() +{ + static ConnectionIdSet const empty{}; + return empty; +} + +void SimpleGraphModel::indexConnection(ConnectionId const connectionId) +{ + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); +} + +void SimpleGraphModel::unindexConnection(ConnectionId const connectionId) +{ + auto eraseFromNode = [&](NodeId nodeId) { + auto nodeIt = _nodeConnections.find(nodeId); + if (nodeIt == _nodeConnections.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + _nodeConnections.erase(nodeIt); + } + }; + + auto eraseFromPortMap = + [&](std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex) { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + }; + + eraseFromNode(connectionId.inNodeId); + eraseFromNode(connectionId.outNodeId); + eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); + eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); +} diff --git a/examples/simple_graph_model/SimpleGraphModel.hpp b/examples/simple_graph_model/SimpleGraphModel.hpp index e3d07213..92a09eb2 100644 --- a/examples/simple_graph_model/SimpleGraphModel.hpp +++ b/examples/simple_graph_model/SimpleGraphModel.hpp @@ -8,6 +8,8 @@ #include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -38,13 +40,13 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel ~SimpleGraphModel() override; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -91,7 +93,14 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; + using ConnectionsByPort = std::unordered_map; + + static ConnectionIdSet const &emptyConnections(); + + void indexConnection(ConnectionId const connectionId); + void unindexConnection(ConnectionId const connectionId); + + NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. /// In your case it could be anything else representing a graph, for example, a @@ -100,7 +109,10 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel /// /// This data structure contains the graph connectivity information in both /// directions, i.e. from Node1 to Node2 and from Node2 to Node1. - std::unordered_set _connectivity; + ConnectionIdSet _connectivity; + std::unordered_map _nodeConnections; + std::unordered_map _inConnectionsByPort; + std::unordered_map _outConnectionsByPort; mutable std::unordered_map _nodeGeometryData; diff --git a/examples/vertical_layout/SimpleGraphModel.cpp b/examples/vertical_layout/SimpleGraphModel.cpp index 5b5e3b8b..49bc7ae2 100644 --- a/examples/vertical_layout/SimpleGraphModel.cpp +++ b/examples/vertical_layout/SimpleGraphModel.cpp @@ -1,5 +1,7 @@ #include "SimpleGraphModel.hpp" +#include + SimpleGraphModel::SimpleGraphModel() : _nextNodeId{0} {} @@ -9,40 +11,42 @@ SimpleGraphModel::~SimpleGraphModel() // } -std::unordered_set SimpleGraphModel::allNodeIds() const +QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() const { return _nodeIds; } -std::unordered_set SimpleGraphModel::allConnectionIds(NodeId const nodeId) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - std::unordered_set result; - - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&nodeId](ConnectionId const &cid) { - return cid.inNodeId == nodeId || cid.outNodeId == nodeId; - }); + auto const it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return emptyConnections(); + } - return result; + return it->second; } -std::unordered_set SimpleGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +QtNodes::AbstractGraphModel::ConnectionIdSet const & +SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; + if (portType == PortType::None) { + return emptyConnections(); + } - std::copy_if(_connectivity.begin(), - _connectivity.end(), - std::inserter(result, std::end(result)), - [&portType, &portIndex, &nodeId](ConnectionId const &cid) { - return (getNodeId(portType, cid) == nodeId - && getPortIndex(portType, cid) == portIndex); - }); + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto const nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return emptyConnections(); + } - return result; + auto const portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return emptyConnections(); + } + + return portIt->second; } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const @@ -69,6 +73,7 @@ bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const void SimpleGraphModel::addConnection(ConnectionId const connectionId) { _connectivity.insert(connectionId); + indexConnection(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -230,6 +235,7 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) disconnected = true; _connectivity.erase(it); + unindexConnection(connectionId); } if (disconnected) @@ -240,9 +246,14 @@ bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) bool SimpleGraphModel::deleteNode(NodeId const nodeId) { - // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); - for (auto &cId : connectionIds) { + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + connectionIds.push_back(connectionId); + } + + for (auto const &cId : connectionIds) { deleteConnection(cId); } @@ -291,3 +302,61 @@ void SimpleGraphModel::loadNode(QJsonObject const &nodeJson) setNodeData(restoredNodeId, NodeRole::Position, pos); } } + +QtNodes::AbstractGraphModel::ConnectionIdSet const &SimpleGraphModel::emptyConnections() +{ + static ConnectionIdSet const empty{}; + return empty; +} + +void SimpleGraphModel::indexConnection(ConnectionId const connectionId) +{ + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); +} + +void SimpleGraphModel::unindexConnection(ConnectionId const connectionId) +{ + auto eraseFromNode = [&](NodeId nodeId) { + auto nodeIt = _nodeConnections.find(nodeId); + if (nodeIt == _nodeConnections.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + _nodeConnections.erase(nodeIt); + } + }; + + auto eraseFromPortMap = + [&](std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex) { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + }; + + eraseFromNode(connectionId.inNodeId); + eraseFromNode(connectionId.outNodeId); + eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); + eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); +} diff --git a/examples/vertical_layout/SimpleGraphModel.hpp b/examples/vertical_layout/SimpleGraphModel.hpp index f9d1fd4d..8a3a9fb4 100644 --- a/examples/vertical_layout/SimpleGraphModel.hpp +++ b/examples/vertical_layout/SimpleGraphModel.hpp @@ -8,6 +8,8 @@ #include #include +#include + using ConnectionId = QtNodes::ConnectionId; using ConnectionPolicy = QtNodes::ConnectionPolicy; using NodeFlag = QtNodes::NodeFlag; @@ -38,13 +40,13 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel ~SimpleGraphModel() override; - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; bool connectionExists(ConnectionId const connectionId) const override; @@ -92,7 +94,14 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - std::unordered_set _nodeIds; + using ConnectionsByPort = std::unordered_map; + + static ConnectionIdSet const &emptyConnections(); + + void indexConnection(ConnectionId const connectionId); + void unindexConnection(ConnectionId const connectionId); + + NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. /// In your case it could be anything else representing a graph, for example, a @@ -101,7 +110,10 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel /// /// This data structure contains the graph connectivity information in both /// directions, i.e. from Node1 to Node2 and from Node2 to Node1. - std::unordered_set _connectivity; + ConnectionIdSet _connectivity; + std::unordered_map _nodeConnections; + std::unordered_map _inConnectionsByPort; + std::unordered_map _outConnectionsByPort; mutable std::unordered_map _nodeGeometryData; diff --git a/include/QtNodes/internal/AbstractGraphModel.hpp b/include/QtNodes/internal/AbstractGraphModel.hpp index cf8e2491..af4ab9e9 100644 --- a/include/QtNodes/internal/AbstractGraphModel.hpp +++ b/include/QtNodes/internal/AbstractGraphModel.hpp @@ -26,8 +26,11 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject { Q_OBJECT public: + using NodeIdSet = std::unordered_set; + using ConnectionIdSet = std::unordered_set; + /// Generates a new unique NodeId. - virtual NodeId newNodeId() = 0; + [[nodiscard]] virtual NodeId newNodeId() = 0; /// @brief Returns the full set of unique Node Ids. /** @@ -35,25 +38,34 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * Ids for all the nodes in the graph. From an Id it should be * possible to trace back to the model's internal representation of * the node. + * + * The returned reference must stay valid until the next mutating call on + * the graph model. */ - virtual std::unordered_set allNodeIds() const = 0; + [[nodiscard]] virtual NodeIdSet const &allNodeIds() const = 0; /** * A collection of all input and output connections for the given `nodeId`. + * + * The returned reference must stay valid until the next mutating call on + * the graph model. */ - virtual std::unordered_set allConnectionIds(NodeId const nodeId) const = 0; + [[nodiscard]] virtual ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const = 0; /// @brief Returns all connected Node Ids for given port. /** * The returned set of nodes and port indices correspond to the type * opposite to the given `portType`. + * + * The returned reference must stay valid until the next mutating call on + * the graph model. */ - virtual std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex index) const = 0; + [[nodiscard]] virtual ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex index) const = 0; /// Checks if two nodes with the given `connectionId` are connected. - virtual bool connectionExists(ConnectionId const connectionId) const = 0; + [[nodiscard]] virtual bool connectionExists(ConnectionId const connectionId) const = 0; /// Creates a new node instance in the derived class. /** @@ -62,7 +74,7 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * model on its own, it helps to distinguish between possible node * types and create a correct instance inside. */ - virtual NodeId addNode(QString const nodeType = QString()) = 0; + [[nodiscard]] virtual NodeId addNode(QString const nodeType = QString()) = 0; /// Model decides if a conection with a given connection Id possible. /** @@ -71,7 +83,7 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * It is possible to override the function and connect non-equal * data types. */ - virtual bool connectionPossible(ConnectionId const connectionId) const = 0; + [[nodiscard]] virtual bool connectionPossible(ConnectionId const connectionId) const = 0; /// Defines if detaching the connection is possible. virtual bool detachPossible(ConnectionId const) const { return true; } @@ -91,7 +103,7 @@ class NODE_EDITOR_PUBLIC AbstractGraphModel : public QObject * @returns `true` if there is data in the model associated with the * given `nodeId`. */ - virtual bool nodeExists(NodeId const nodeId) const = 0; + [[nodiscard]] virtual bool nodeExists(NodeId const nodeId) const = 0; /// @brief Returns node-related data for requested NodeRole. /** diff --git a/include/QtNodes/internal/BasicGraphicsScene.hpp b/include/QtNodes/internal/BasicGraphicsScene.hpp index ff9e26d0..129c9ae9 100644 --- a/include/QtNodes/internal/BasicGraphicsScene.hpp +++ b/include/QtNodes/internal/BasicGraphicsScene.hpp @@ -104,10 +104,10 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene * @brief Creates a list of the connections that are incident only to nodes within a * given group. * @param groupID ID of the desired group. - * @return List of (pointers of) connections whose both endpoints belong to members of - * the specified group. + * @return List of connections whose both endpoints belong to members of the specified + * group. */ - std::vector> connectionsWithinGroup(GroupId groupID); + std::vector connectionsWithinGroup(GroupId groupID); /** * @brief Creates a group in the scene containing the given nodes. * @param nodes Reference to the list of nodes to be included in the group. diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index d0867866..59f33d92 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -33,26 +33,26 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel public: DataFlowGraphModel(std::shared_ptr registry); - std::shared_ptr dataModelRegistry() { return _registry; } + [[nodiscard]] std::shared_ptr dataModelRegistry() { return _registry; } public: - std::unordered_set allNodeIds() const override; + NodeIdSet const &allNodeIds() const override; - std::unordered_set allConnectionIds(NodeId const nodeId) const override; + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override; - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override; + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override; - bool connectionExists(ConnectionId const connectionId) const override; + [[nodiscard]] bool connectionExists(ConnectionId const connectionId) const override; - NodeId addNode(QString const nodeType) override; + [[nodiscard]] NodeId addNode(QString const nodeType) override; - bool connectionPossible(ConnectionId const connectionId) const override; + [[nodiscard]] bool connectionPossible(ConnectionId const connectionId) const override; void addConnection(ConnectionId const connectionId) override; - bool nodeExists(NodeId const nodeId) const override; + [[nodiscard]] bool nodeExists(NodeId const nodeId) const override; QVariant nodeData(NodeId nodeId, NodeRole role) const override; @@ -151,6 +151,8 @@ private Q_SLOTS: NodeId _nextNodeId; + NodeIdSet _nodeIds; + std::unordered_map> _models; std::unordered_set _connectivity; diff --git a/include/QtNodes/internal/GroupGraphicsObject.hpp b/include/QtNodes/internal/GroupGraphicsObject.hpp index 2d21e62b..74826471 100644 --- a/include/QtNodes/internal/GroupGraphicsObject.hpp +++ b/include/QtNodes/internal/GroupGraphicsObject.hpp @@ -155,10 +155,10 @@ class GroupGraphicsObject void unsetPossibleChild(); /** - * @brief Returns all the connections that are incident strictly within the - * nodes of this group. - */ - std::vector> connections() const; + * @brief Returns all the connections that are incident strictly within the + * nodes of this group. + */ + std::vector connections() const; /** * @brief Sets the position of the group. diff --git a/include/QtNodes/internal/NodeDelegateModelRegistry.hpp b/include/QtNodes/internal/NodeDelegateModelRegistry.hpp index 86a462f5..2f00fb7d 100644 --- a/include/QtNodes/internal/NodeDelegateModelRegistry.hpp +++ b/include/QtNodes/internal/NodeDelegateModelRegistry.hpp @@ -66,7 +66,7 @@ class NODE_EDITOR_PUBLIC NodeDelegateModelRegistry } - std::unique_ptr create(QString const &modelName); + [[nodiscard]] std::unique_ptr create(QString const &modelName); RegisteredModelCreatorsMap const ®isteredModelCreators() const; diff --git a/src/AbstractGraphModel.cpp b/src/AbstractGraphModel.cpp index fbf9a488..c16594a9 100644 --- a/src/AbstractGraphModel.cpp +++ b/src/AbstractGraphModel.cpp @@ -2,6 +2,8 @@ #include +#include + namespace QtNodes { void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, @@ -22,9 +24,14 @@ void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, auto clampedLast = std::min(last, portCount - 1); for (PortIndex portIndex = first; portIndex <= clampedLast; ++portIndex) { - std::unordered_set conns = connections(nodeId, portType, portIndex); + std::vector conns; + auto const &attachedConnections = connections(nodeId, portType, portIndex); + conns.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + conns.push_back(connectionId); + } - for (auto connectionId : conns) { + for (auto const connectionId : conns) { deleteConnection(connectionId); } } @@ -32,9 +39,14 @@ void AbstractGraphModel::portsAboutToBeDeleted(NodeId const nodeId, std::size_t const nRemovedPorts = clampedLast - first + 1; for (PortIndex portIndex = clampedLast + 1; portIndex < portCount; ++portIndex) { - std::unordered_set conns = connections(nodeId, portType, portIndex); + std::vector conns; + auto const &attachedConnections = connections(nodeId, portType, portIndex); + conns.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + conns.push_back(connectionId); + } - for (auto connectionId : conns) { + for (auto const connectionId : conns) { // Erases the information about the port on one side; auto c = makeIncompleteConnectionId(connectionId, portType); @@ -76,9 +88,14 @@ void AbstractGraphModel::portsAboutToBeInserted(NodeId const nodeId, std::size_t const nNewPorts = last - first + 1; for (PortIndex portIndex = first; portIndex < portCount; ++portIndex) { - std::unordered_set conns = connections(nodeId, portType, portIndex); + std::vector conns; + auto const &attachedConnections = connections(nodeId, portType, portIndex); + conns.reserve(attachedConnections.size()); + for (auto const &connectionId : attachedConnections) { + conns.push_back(connectionId); + } - for (auto connectionId : conns) { + for (auto const connectionId : conns) { // Erases the information about the port on one side; auto c = makeIncompleteConnectionId(connectionId, portType); diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index 84328a8e..8cf89dd2 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -245,19 +245,25 @@ void BasicGraphicsScene::resetDraftConnection() void BasicGraphicsScene::clearScene() { + std::vector nodeIds; auto const &allNodeIds = graphModel().allNodeIds(); + nodeIds.reserve(allNodeIds.size()); + for (auto const nodeId : allNodeIds) { + nodeIds.push_back(nodeId); + } - for (auto nodeId : allNodeIds) { + for (auto const nodeId : nodeIds) { graphModel().deleteNode(nodeId); } } -std::vector> BasicGraphicsScene::connectionsWithinGroup(GroupId groupID) +std::vector BasicGraphicsScene::connectionsWithinGroup(GroupId groupID) { if (!_groupingEnabled) return {}; - std::vector> ret{}; + std::vector ret{}; + ret.reserve(_connectionGraphicsObjects.size()); for (auto const &connection : _connectionGraphicsObjects) { auto outNode = nodeGraphicsObject(connection.first.outNodeId); @@ -266,7 +272,7 @@ std::vector> BasicGraphicsScene::connectionsWithin auto group1 = outNode->nodeGroup().lock(); auto group2 = inNode->nodeGroup().lock(); if (group1 && group2 && group1->id() == group2->id() && group1->id() == groupID) { - ret.push_back(std::make_shared(connection.first)); + ret.push_back(connection.first); } } } @@ -323,7 +329,7 @@ QMenu *BasicGraphicsScene::createSceneMenu(QPointF const scenePos) void BasicGraphicsScene::traverseGraphAndPopulateGraphicsObjects() { - auto allNodeIds = _graphModel.allNodeIds(); + auto const &allNodeIds = _graphModel.allNodeIds(); // First create all the nodes. for (NodeId const nodeId : allNodeIds) { @@ -462,9 +468,9 @@ void BasicGraphicsScene::freezeModelAndConnections(bool isFreeze) if (auto n = qgraphicsitem_cast(item)) { int portCount = graphModel().nodeData(n->nodeId(), NodeRole::OutPortCount).toInt(); for (int i = 0; i < portCount; i++) { - auto graphConnections = graphModel().connections(n->nodeId(), - QtNodes::PortType::Out, - QtNodes::PortIndex(i)); + auto const &graphConnections = graphModel().connections(n->nodeId(), + QtNodes::PortType::Out, + QtNodes::PortIndex(i)); for (auto const &c : graphConnections) { if (auto *cgo = connectionGraphicsObject(c)) { diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index 306396d8..8dc4c9d3 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -16,6 +16,8 @@ namespace QtNodes { namespace { +AbstractGraphModel::ConnectionIdSet const k_empty_connection_ids{}; + NodeId json_value_to_node_id(QJsonValue const &value) { NodeId nodeId = InvalidNodeId; @@ -45,42 +47,39 @@ DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr DataFlowGraphModel::allNodeIds() const +AbstractGraphModel::NodeIdSet const &DataFlowGraphModel::allNodeIds() const { - std::unordered_set nodeIds; - for_each(_models.begin(), _models.end(), [&nodeIds](auto const &p) { nodeIds.insert(p.first); }); - - return nodeIds; + return _nodeIds; } -std::unordered_set DataFlowGraphModel::allConnectionIds(NodeId const nodeId) const +AbstractGraphModel::ConnectionIdSet const & +DataFlowGraphModel::allConnectionIds(NodeId const nodeId) const { auto it = _nodeConnections.find(nodeId); if (it == _nodeConnections.end()) { - return {}; + return k_empty_connection_ids; } return it->second; } -std::unordered_set DataFlowGraphModel::connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const +AbstractGraphModel::ConnectionIdSet const &DataFlowGraphModel::connections( + NodeId nodeId, PortType portType, PortIndex portIndex) const { if (portType == PortType::None) { - return {}; + return k_empty_connection_ids; } auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort : _outConnectionsByPort; auto nodeIt = connectionsByPort.find(nodeId); if (nodeIt == connectionsByPort.end()) { - return {}; + return k_empty_connection_ids; } auto portIt = nodeIt->second.find(portIndex); if (portIt == nodeIt->second.end()) { - return {}; + return k_empty_connection_ids; } return portIt->second; @@ -100,6 +99,7 @@ NodeId DataFlowGraphModel::addNode(QString const nodeType) connectDelegateModel(model.get(), newId); + _nodeIds.insert(newId); _models[newId] = std::move(model); Q_EMIT nodeCreated(newId); @@ -136,7 +136,7 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con auto portVacant = [&](PortType const portType) { NodeId const nodeId = getNodeId(portType, connectionId); PortIndex const portIndex = getPortIndex(portType, connectionId); - auto const connected = connections(nodeId, portType, portIndex); + auto const &connected = connections(nodeId, portType, portIndex); auto policy = portData(nodeId, portType, portIndex, PortRole::ConnectionPolicyRole) .value(); @@ -614,11 +614,18 @@ bool DataFlowGraphModel::deleteConnection(ConnectionId const connectionId) bool DataFlowGraphModel::deleteNode(NodeId const nodeId) { // Delete connections to this node first. - auto connectionIds = allConnectionIds(nodeId); - for (auto &cId : connectionIds) { + std::vector connectionIds; + auto const &attachedConnections = allConnectionIds(nodeId); + connectionIds.reserve(attachedConnections.size()); + for (auto const &cId : attachedConnections) { + connectionIds.push_back(cId); + } + + for (auto const &cId : connectionIds) { deleteConnection(cId); } + _nodeIds.erase(nodeId); _nodeGeometryData.erase(nodeId); _models.erase(nodeId); @@ -705,6 +712,7 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) if (model) { connectDelegateModel(model.get(), restoredNodeId); + _nodeIds.insert(restoredNodeId); _models[restoredNodeId] = std::move(model); Q_EMIT nodeCreated(restoredNodeId); diff --git a/src/DataFlowGraphicsScene.cpp b/src/DataFlowGraphicsScene.cpp index a19add6f..1883099c 100644 --- a/src/DataFlowGraphicsScene.cpp +++ b/src/DataFlowGraphicsScene.cpp @@ -121,6 +121,80 @@ void validate_groups_json(QJsonObject const &sceneJson) } } +QJsonObject scene_json_with_groups(QtNodes::DataFlowGraphModel const &graphModel, + QtNodes::BasicGraphicsScene const &scene) +{ + QJsonObject sceneJson = graphModel.save(); + + QJsonArray groupsJsonArray; + for (auto const &groupEntry : scene.groups()) { + GroupId const groupId = groupEntry.first; + auto const &groupPtr = groupEntry.second; + + if (!groupPtr) { + continue; + } + + QJsonObject groupJson; + groupJson["id"] = static_cast(groupId); + groupJson["name"] = groupPtr->name(); + + QJsonArray nodeIdsJson; + for (NodeId const nodeId : groupPtr->nodeIDs()) { + nodeIdsJson.append(static_cast(nodeId)); + } + groupJson["nodes"] = nodeIdsJson; + groupJson["locked"] = groupPtr->groupGraphicsObject().locked(); + + groupsJsonArray.append(groupJson); + } + + if (!groupsJsonArray.isEmpty()) { + sceneJson["groups"] = groupsJsonArray; + } + + return sceneJson; +} + +void restore_groups_from_json(QJsonObject const &sceneJson, QtNodes::BasicGraphicsScene &scene) +{ + if (!sceneJson.contains("groups")) { + return; + } + + QJsonArray const groupsJsonArray = sceneJson["groups"].toArray(); + + for (QJsonValue const groupValue : groupsJsonArray) { + QJsonObject const groupObject = groupValue.toObject(); + + QJsonArray const nodeIdsJson = groupObject["nodes"].toArray(); + std::vector groupNodes; + groupNodes.reserve(nodeIdsJson.size()); + + for (QJsonValue const idValue : nodeIdsJson) { + NodeId nodeId = InvalidNodeId; + QtNodes::detail::read_node_id(idValue, nodeId); + if (auto *nodeObject = scene.nodeGraphicsObject(nodeId)) { + groupNodes.push_back(nodeObject); + } + } + + if (groupNodes.empty()) { + continue; + } + + QString const groupName = groupObject["name"].toString(); + GroupId groupId = InvalidGroupId; + QtNodes::detail::read_group_id(groupObject["id"], groupId); + + auto const groupWeak = scene.createGroup(groupNodes, groupName, groupId); + if (auto group = groupWeak.lock()) { + bool const locked = groupObject["locked"].toBool(true); + group->groupGraphicsObject().lock(locked); + } + } +} + } // namespace namespace QtNodes { @@ -254,30 +328,7 @@ bool DataFlowGraphicsScene::save() const QFile file(fileName); if (file.open(QIODevice::WriteOnly)) { - QJsonObject sceneJson = _graphModel.save(); - - QJsonArray groupsJsonArray; - for (auto const &[groupId, groupPtr] : groups()) { - if (!groupPtr) - continue; - - QJsonObject groupJson; - groupJson["id"] = static_cast(groupId); - groupJson["name"] = groupPtr->name(); - - QJsonArray nodeIdsJson; - for (NodeId const nodeId : groupPtr->nodeIDs()) { - nodeIdsJson.append(static_cast(nodeId)); - } - groupJson["nodes"] = nodeIdsJson; - groupJson["locked"] = groupPtr->groupGraphicsObject().locked(); - - groupsJsonArray.append(groupJson); - } - - if (!groupsJsonArray.isEmpty()) { - sceneJson["groups"] = groupsJsonArray; - } + QJsonObject sceneJson = scene_json_with_groups(_graphModel, *this); file.write(QJsonDocument(sceneJson).toJson()); return true; @@ -312,52 +363,28 @@ bool DataFlowGraphicsScene::load() try { validate_groups_json(sceneJson); - - DataFlowGraphModel stagingModel(_graphModel.dataModelRegistry()); - stagingModel.load(sceneJson); } catch (...) { return false; } + QJsonObject const previousSceneJson = scene_json_with_groups(_graphModel, *this); + clearScene(); try { _graphModel.load(sceneJson); + restore_groups_from_json(sceneJson, *this); } catch (...) { - return false; - } + clearScene(); - if (sceneJson.contains("groups")) { - QJsonArray const groupsJsonArray = sceneJson["groups"].toArray(); - - for (QJsonValue groupValue : groupsJsonArray) { - QJsonObject const groupObject = groupValue.toObject(); - - QJsonArray const nodeIdsJson = groupObject["nodes"].toArray(); - std::vector groupNodes; - groupNodes.reserve(nodeIdsJson.size()); - - for (QJsonValue idValue : nodeIdsJson) { - NodeId nodeId = InvalidNodeId; - detail::read_node_id(idValue, nodeId); - if (auto *nodeObject = nodeGraphicsObject(nodeId)) { - groupNodes.push_back(nodeObject); - } - } - - if (groupNodes.empty()) - continue; - - QString const groupName = groupObject["name"].toString(); - GroupId groupId = InvalidGroupId; - detail::read_group_id(groupObject["id"], groupId); - - auto const groupWeak = createGroup(groupNodes, groupName, groupId); - if (auto group = groupWeak.lock()) { - bool const locked = groupObject["locked"].toBool(true); - group->groupGraphicsObject().lock(locked); - } + try { + _graphModel.load(previousSceneJson); + restore_groups_from_json(previousSceneJson, *this); + } catch (...) { + clearScene(); } + + return false; } Q_EMIT sceneLoaded(); diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 2d6b52d1..89649d7f 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -104,6 +104,19 @@ void set_node_cache_mode( return; } + if (auto *nodeScene = dynamic_cast(scene)) { + for (auto const nodeId : nodeScene->graphModel().allNodeIds()) { + if (auto *item = nodeScene->nodeGraphicsObject(nodeId)) { + if (invalidate_cached_content && mode != QGraphicsItem::NoCache) { + item->setCacheMode(QGraphicsItem::NoCache); + } + item->setCacheMode(mode); + item->update(); + } + } + return; + } + for (QGraphicsItem *item : scene->items()) { if (qgraphicsitem_cast(item)) { if (invalidate_cached_content && mode != QGraphicsItem::NoCache) { diff --git a/src/GroupGraphicsObject.cpp b/src/GroupGraphicsObject.cpp index 19e59504..f844f266 100644 --- a/src/GroupGraphicsObject.cpp +++ b/src/GroupGraphicsObject.cpp @@ -182,7 +182,7 @@ void GroupGraphicsObject::unsetPossibleChild() updateGroupGeometry(); } -std::vector> GroupGraphicsObject::connections() const +std::vector GroupGraphicsObject::connections() const { return _scene.connectionsWithinGroup(group().id()); } diff --git a/src/NodeGroup.cpp b/src/NodeGroup.cpp index 6b70d86c..fdf89bd1 100644 --- a/src/NodeGroup.cpp +++ b/src/NodeGroup.cpp @@ -43,9 +43,9 @@ QByteArray NodeGroup::saveToFile() const groupJson["nodes"] = nodesJson; QJsonArray connectionsJson; - auto groupConnections = _groupGraphicsObject->connections(); + auto const &groupConnections = _groupGraphicsObject->connections(); for (auto const &connection : groupConnections) { - connectionsJson.append(toJson(*connection)); + connectionsJson.append(toJson(connection)); } groupJson["connections"] = connectionsJson; diff --git a/test/include/TestGraphModel.hpp b/test/include/TestGraphModel.hpp index ee8918aa..2c81094b 100644 --- a/test/include/TestGraphModel.hpp +++ b/test/include/TestGraphModel.hpp @@ -9,6 +9,7 @@ #include #include +#include using QtNodes::AbstractGraphModel; using QtNodes::ConnectionId; @@ -31,33 +32,39 @@ class TestGraphModel : public AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } - std::unordered_set allNodeIds() const override { return _nodeIds; } + NodeIdSet const &allNodeIds() const override { return _nodeIds; } - std::unordered_set allConnectionIds(NodeId const nodeId) const override + ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override { - std::unordered_set result; - for (const auto &conn : _connections) { - if (conn.inNodeId == nodeId || conn.outNodeId == nodeId) { - result.insert(conn); - } + auto const it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return empty_connections(); } - return result; + + return it->second; } - std::unordered_set connections(NodeId nodeId, - PortType portType, - PortIndex portIndex) const override + ConnectionIdSet const &connections(NodeId nodeId, + PortType portType, + PortIndex portIndex) const override { - std::unordered_set result; - for (const auto &conn : _connections) { - if (portType == PortType::In && conn.inNodeId == nodeId && conn.inPortIndex == portIndex) { - result.insert(conn); - } else if (portType == PortType::Out && conn.outNodeId == nodeId - && conn.outPortIndex == portIndex) { - result.insert(conn); - } + if (portType == PortType::None) { + return empty_connections(); } - return result; + + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto const nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return empty_connections(); + } + + auto const portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return empty_connections(); + } + + return portIt->second; } bool connectionExists(ConnectionId const connectionId) const override @@ -89,6 +96,10 @@ class TestGraphModel : public AbstractGraphModel { if (connectionPossible(connectionId)) { _connections.insert(connectionId); + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); Q_EMIT connectionCreated(connectionId); } } @@ -196,6 +207,16 @@ class TestGraphModel : public AbstractGraphModel auto it = _connections.find(connectionId); if (it != _connections.end()) { _connections.erase(it); + eraseIndexedConnection(_nodeConnections, connectionId.inNodeId, connectionId); + eraseIndexedConnection(_nodeConnections, connectionId.outNodeId, connectionId); + eraseIndexedConnection(_inConnectionsByPort, + connectionId.inNodeId, + connectionId.inPortIndex, + connectionId); + eraseIndexedConnection(_outConnectionsByPort, + connectionId.outNodeId, + connectionId.outPortIndex, + connectionId); Q_EMIT connectionDeleted(connectionId); return true; } @@ -207,15 +228,14 @@ class TestGraphModel : public AbstractGraphModel if (!nodeExists(nodeId)) return false; - // Remove all connections involving this node std::vector connectionsToRemove; - for (const auto &conn : _connections) { - if (conn.inNodeId == nodeId || conn.outNodeId == nodeId) { - connectionsToRemove.push_back(conn); - } + auto const &attachedConnections = allConnectionIds(nodeId); + connectionsToRemove.reserve(attachedConnections.size()); + for (auto const &conn : attachedConnections) { + connectionsToRemove.push_back(conn); } - for (const auto &conn : connectionsToRemove) { + for (auto const &conn : connectionsToRemove) { deleteConnection(conn); } @@ -277,8 +297,60 @@ class TestGraphModel : public AbstractGraphModel } private: + using ConnectionsByPort = std::unordered_map; + + static void eraseIndexedConnection(std::unordered_map &connectionsByNode, + NodeId nodeId, + ConnectionId const &connectionId) + { + auto nodeIt = connectionsByNode.find(nodeId); + if (nodeIt == connectionsByNode.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + connectionsByNode.erase(nodeIt); + } + } + + static void eraseIndexedConnection( + std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex, + ConnectionId const &connectionId) + { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + } + + static ConnectionIdSet const &empty_connections() + { + static ConnectionIdSet const empty{}; + return empty; + } + NodeId _nextNodeId = 1; - std::unordered_set _nodeIds; - std::unordered_set _connections; + NodeIdSet _nodeIds; + ConnectionIdSet _connections; + std::unordered_map _nodeConnections; + std::unordered_map _inConnectionsByPort; + std::unordered_map _outConnectionsByPort; std::unordered_map> _nodeData; }; From df998857282d83972e6c615df64203cf6adb3345 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 12:27:45 +0100 Subject: [PATCH 22/26] Extract shared connection indexing helper --- CMakeLists.txt | 1 + examples/custom_painter/SimpleGraphModel.cpp | 100 +------------ examples/custom_painter/SimpleGraphModel.hpp | 13 +- examples/dynamic_ports/DynamicPortsModel.cpp | 103 +------------ examples/dynamic_ports/DynamicPortsModel.hpp | 13 +- .../simple_graph_model/SimpleGraphModel.cpp | 101 +------------ .../simple_graph_model/SimpleGraphModel.hpp | 13 +- examples/vertical_layout/SimpleGraphModel.cpp | 101 +------------ examples/vertical_layout/SimpleGraphModel.hpp | 13 +- include/QtNodes/ConnectionIdIndex | 1 + .../QtNodes/internal/ConnectionIdIndex.hpp | 140 ++++++++++++++++++ .../QtNodes/internal/DataFlowGraphModel.hpp | 16 +- include/QtNodes/internal/NodeGroup.hpp | 3 +- src/DataFlowGraphModel.cpp | 98 +----------- src/NodeGroup.cpp | 7 +- test/include/TestGraphModel.hpp | 102 +------------ 16 files changed, 196 insertions(+), 629 deletions(-) create mode 100644 include/QtNodes/ConnectionIdIndex create mode 100644 include/QtNodes/internal/ConnectionIdIndex.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 498ad3b5..31e6164f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ set(HPP_HEADER_FILES include/QtNodes/internal/BasicGraphicsScene.hpp include/QtNodes/internal/Compiler.hpp include/QtNodes/internal/ConnectionGraphicsObject.hpp + include/QtNodes/internal/ConnectionIdIndex.hpp include/QtNodes/internal/ConnectionIdHash.hpp include/QtNodes/internal/ConnectionIdUtils.hpp include/QtNodes/internal/ConnectionState.hpp diff --git a/examples/custom_painter/SimpleGraphModel.cpp b/examples/custom_painter/SimpleGraphModel.cpp index d13fdd96..526ccfd1 100644 --- a/examples/custom_painter/SimpleGraphModel.cpp +++ b/examples/custom_painter/SimpleGraphModel.cpp @@ -16,39 +16,18 @@ QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() con QtNodes::AbstractGraphModel::ConnectionIdSet const & SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - auto const it = _nodeConnections.find(nodeId); - if (it == _nodeConnections.end()) { - return emptyConnections(); - } - - return it->second; + return _connectionIndex.allConnectionIds(nodeId); } QtNodes::AbstractGraphModel::ConnectionIdSet const & SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - if (portType == PortType::None) { - return emptyConnections(); - } - - auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort - : _outConnectionsByPort; - auto const nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return emptyConnections(); - } - - auto const portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return emptyConnections(); - } - - return portIt->second; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId SimpleGraphModel::addNode(QString const nodeType) @@ -63,13 +42,12 @@ NodeId SimpleGraphModel::addNode(QString const nodeType) bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const { - return _connectivity.find(connectionId) == _connectivity.end(); + return !_connectionIndex.contains(connectionId); } void SimpleGraphModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); - indexConnection(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -193,15 +171,7 @@ bool SimpleGraphModel::setPortData( bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - _connectivity.erase(it); - unindexConnection(connectionId); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -265,61 +235,3 @@ void SimpleGraphModel::loadNode(QJsonObject const &nodeJson) setNodeData(restoredNodeId, NodeRole::Position, pos); } } - -QtNodes::AbstractGraphModel::ConnectionIdSet const &SimpleGraphModel::emptyConnections() -{ - static ConnectionIdSet const empty{}; - return empty; -} - -void SimpleGraphModel::indexConnection(ConnectionId const connectionId) -{ - _nodeConnections[connectionId.inNodeId].insert(connectionId); - _nodeConnections[connectionId.outNodeId].insert(connectionId); - _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); - _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); -} - -void SimpleGraphModel::unindexConnection(ConnectionId const connectionId) -{ - auto eraseFromNode = [&](NodeId nodeId) { - auto nodeIt = _nodeConnections.find(nodeId); - if (nodeIt == _nodeConnections.end()) { - return; - } - - nodeIt->second.erase(connectionId); - if (nodeIt->second.empty()) { - _nodeConnections.erase(nodeIt); - } - }; - - auto eraseFromPortMap = - [&](std::unordered_map &connectionsByPort, - NodeId nodeId, - PortIndex portIndex) { - auto nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return; - } - - auto portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return; - } - - portIt->second.erase(connectionId); - if (portIt->second.empty()) { - nodeIt->second.erase(portIt); - } - - if (nodeIt->second.empty()) { - connectionsByPort.erase(nodeIt); - } - }; - - eraseFromNode(connectionId.inNodeId); - eraseFromNode(connectionId.outNodeId); - eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); - eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); -} diff --git a/examples/custom_painter/SimpleGraphModel.hpp b/examples/custom_painter/SimpleGraphModel.hpp index 3f6cd841..1bb4c563 100644 --- a/examples/custom_painter/SimpleGraphModel.hpp +++ b/examples/custom_painter/SimpleGraphModel.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -81,18 +82,8 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - using ConnectionsByPort = std::unordered_map; - - static ConnectionIdSet const &emptyConnections(); - - void indexConnection(ConnectionId const connectionId); - void unindexConnection(ConnectionId const connectionId); - NodeIdSet _nodeIds; - ConnectionIdSet _connectivity; - std::unordered_map _nodeConnections; - std::unordered_map _inConnectionsByPort; - std::unordered_map _outConnectionsByPort; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; NodeId _nextNodeId; }; diff --git a/examples/dynamic_ports/DynamicPortsModel.cpp b/examples/dynamic_ports/DynamicPortsModel.cpp index e1b28f2f..1af2970a 100644 --- a/examples/dynamic_ports/DynamicPortsModel.cpp +++ b/examples/dynamic_ports/DynamicPortsModel.cpp @@ -22,39 +22,18 @@ QtNodes::AbstractGraphModel::NodeIdSet const &DynamicPortsModel::allNodeIds() co QtNodes::AbstractGraphModel::ConnectionIdSet const & DynamicPortsModel::allConnectionIds(NodeId const nodeId) const { - auto const it = _nodeConnections.find(nodeId); - if (it == _nodeConnections.end()) { - return emptyConnections(); - } - - return it->second; + return _connectionIndex.allConnectionIds(nodeId); } QtNodes::AbstractGraphModel::ConnectionIdSet const & DynamicPortsModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - if (portType == PortType::None) { - return emptyConnections(); - } - - auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort - : _outConnectionsByPort; - auto const nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return emptyConnections(); - } - - auto const portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return emptyConnections(); - } - - return portIt->second; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool DynamicPortsModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId DynamicPortsModel::addNode(QString const nodeType) @@ -71,13 +50,12 @@ NodeId DynamicPortsModel::addNode(QString const nodeType) bool DynamicPortsModel::connectionPossible(ConnectionId const connectionId) const { - return !connectionExists(connectionId); + return !_connectionIndex.contains(connectionId); } void DynamicPortsModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); - indexConnection(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -249,16 +227,7 @@ bool DynamicPortsModel::setPortData( bool DynamicPortsModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - unindexConnection(connectionId); - }; + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -321,7 +290,7 @@ QJsonObject DynamicPortsModel::save() const sceneJson["nodes"] = nodesJsonArray; QJsonArray connJsonArray; - for (auto const &cid : _connectivity) { + for (auto const &cid : _connectionIndex.connectivity()) { connJsonArray.append(QtNodes::toJson(cid)); } sceneJson["connections"] = connJsonArray; @@ -414,61 +383,3 @@ void DynamicPortsModel::removePort(NodeId nodeId, PortType portType, PortIndex p Q_EMIT nodeUpdated(nodeId); } - -QtNodes::AbstractGraphModel::ConnectionIdSet const &DynamicPortsModel::emptyConnections() -{ - static ConnectionIdSet const empty{}; - return empty; -} - -void DynamicPortsModel::indexConnection(ConnectionId const connectionId) -{ - _nodeConnections[connectionId.inNodeId].insert(connectionId); - _nodeConnections[connectionId.outNodeId].insert(connectionId); - _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); - _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); -} - -void DynamicPortsModel::unindexConnection(ConnectionId const connectionId) -{ - auto eraseFromNode = [&](NodeId nodeId) { - auto nodeIt = _nodeConnections.find(nodeId); - if (nodeIt == _nodeConnections.end()) { - return; - } - - nodeIt->second.erase(connectionId); - if (nodeIt->second.empty()) { - _nodeConnections.erase(nodeIt); - } - }; - - auto eraseFromPortMap = - [&](std::unordered_map &connectionsByPort, - NodeId nodeId, - PortIndex portIndex) { - auto nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return; - } - - auto portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return; - } - - portIt->second.erase(connectionId); - if (portIt->second.empty()) { - nodeIt->second.erase(portIt); - } - - if (nodeIt->second.empty()) { - connectionsByPort.erase(nodeIt); - } - }; - - eraseFromNode(connectionId.inNodeId); - eraseFromNode(connectionId.outNodeId); - eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); - eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); -} diff --git a/examples/dynamic_ports/DynamicPortsModel.hpp b/examples/dynamic_ports/DynamicPortsModel.hpp index c88841a9..878c12c2 100644 --- a/examples/dynamic_ports/DynamicPortsModel.hpp +++ b/examples/dynamic_ports/DynamicPortsModel.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -102,23 +103,13 @@ class DynamicPortsModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - using ConnectionsByPort = std::unordered_map; - - static ConnectionIdSet const &emptyConnections(); - - void indexConnection(ConnectionId const connectionId); - void unindexConnection(ConnectionId const connectionId); - NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. /// In your case it could be anything else representing a graph, for example, a /// table. Or a collection of structs with pointers to each other. Or an /// abstract syntax tree, you name it. - ConnectionIdSet _connectivity; - std::unordered_map _nodeConnections; - std::unordered_map _inConnectionsByPort; - std::unordered_map _outConnectionsByPort; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; diff --git a/examples/simple_graph_model/SimpleGraphModel.cpp b/examples/simple_graph_model/SimpleGraphModel.cpp index 567b142e..830cf13e 100644 --- a/examples/simple_graph_model/SimpleGraphModel.cpp +++ b/examples/simple_graph_model/SimpleGraphModel.cpp @@ -19,39 +19,18 @@ QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() con QtNodes::AbstractGraphModel::ConnectionIdSet const & SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - auto const it = _nodeConnections.find(nodeId); - if (it == _nodeConnections.end()) { - return emptyConnections(); - } - - return it->second; + return _connectionIndex.allConnectionIds(nodeId); } QtNodes::AbstractGraphModel::ConnectionIdSet const & SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - if (portType == PortType::None) { - return emptyConnections(); - } - - auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort - : _outConnectionsByPort; - auto const nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return emptyConnections(); - } - - auto const portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return emptyConnections(); - } - - return portIt->second; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId SimpleGraphModel::addNode(QString const nodeType) @@ -67,13 +46,12 @@ NodeId SimpleGraphModel::addNode(QString const nodeType) bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const { - return _connectivity.find(connectionId) == _connectivity.end(); + return !_connectionIndex.contains(connectionId); } void SimpleGraphModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); - indexConnection(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -227,16 +205,7 @@ bool SimpleGraphModel::setPortData( bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - unindexConnection(connectionId); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -302,61 +271,3 @@ void SimpleGraphModel::loadNode(QJsonObject const &nodeJson) setNodeData(restoredNodeId, NodeRole::Position, pos); } } - -QtNodes::AbstractGraphModel::ConnectionIdSet const &SimpleGraphModel::emptyConnections() -{ - static ConnectionIdSet const empty{}; - return empty; -} - -void SimpleGraphModel::indexConnection(ConnectionId const connectionId) -{ - _nodeConnections[connectionId.inNodeId].insert(connectionId); - _nodeConnections[connectionId.outNodeId].insert(connectionId); - _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); - _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); -} - -void SimpleGraphModel::unindexConnection(ConnectionId const connectionId) -{ - auto eraseFromNode = [&](NodeId nodeId) { - auto nodeIt = _nodeConnections.find(nodeId); - if (nodeIt == _nodeConnections.end()) { - return; - } - - nodeIt->second.erase(connectionId); - if (nodeIt->second.empty()) { - _nodeConnections.erase(nodeIt); - } - }; - - auto eraseFromPortMap = - [&](std::unordered_map &connectionsByPort, - NodeId nodeId, - PortIndex portIndex) { - auto nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return; - } - - auto portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return; - } - - portIt->second.erase(connectionId); - if (portIt->second.empty()) { - nodeIt->second.erase(portIt); - } - - if (nodeIt->second.empty()) { - connectionsByPort.erase(nodeIt); - } - }; - - eraseFromNode(connectionId.inNodeId); - eraseFromNode(connectionId.outNodeId); - eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); - eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); -} diff --git a/examples/simple_graph_model/SimpleGraphModel.hpp b/examples/simple_graph_model/SimpleGraphModel.hpp index 92a09eb2..724dda79 100644 --- a/examples/simple_graph_model/SimpleGraphModel.hpp +++ b/examples/simple_graph_model/SimpleGraphModel.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -93,13 +94,6 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - using ConnectionsByPort = std::unordered_map; - - static ConnectionIdSet const &emptyConnections(); - - void indexConnection(ConnectionId const connectionId); - void unindexConnection(ConnectionId const connectionId); - NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. @@ -109,10 +103,7 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel /// /// This data structure contains the graph connectivity information in both /// directions, i.e. from Node1 to Node2 and from Node2 to Node1. - ConnectionIdSet _connectivity; - std::unordered_map _nodeConnections; - std::unordered_map _inConnectionsByPort; - std::unordered_map _outConnectionsByPort; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; diff --git a/examples/vertical_layout/SimpleGraphModel.cpp b/examples/vertical_layout/SimpleGraphModel.cpp index 49bc7ae2..bc743208 100644 --- a/examples/vertical_layout/SimpleGraphModel.cpp +++ b/examples/vertical_layout/SimpleGraphModel.cpp @@ -19,39 +19,18 @@ QtNodes::AbstractGraphModel::NodeIdSet const &SimpleGraphModel::allNodeIds() con QtNodes::AbstractGraphModel::ConnectionIdSet const & SimpleGraphModel::allConnectionIds(NodeId const nodeId) const { - auto const it = _nodeConnections.find(nodeId); - if (it == _nodeConnections.end()) { - return emptyConnections(); - } - - return it->second; + return _connectionIndex.allConnectionIds(nodeId); } QtNodes::AbstractGraphModel::ConnectionIdSet const & SimpleGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - if (portType == PortType::None) { - return emptyConnections(); - } - - auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort - : _outConnectionsByPort; - auto const nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return emptyConnections(); - } - - auto const portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return emptyConnections(); - } - - return portIt->second; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool SimpleGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId SimpleGraphModel::addNode(QString const nodeType) @@ -67,13 +46,12 @@ NodeId SimpleGraphModel::addNode(QString const nodeType) bool SimpleGraphModel::connectionPossible(ConnectionId const connectionId) const { - return _connectivity.find(connectionId) == _connectivity.end(); + return !_connectionIndex.contains(connectionId); } void SimpleGraphModel::addConnection(ConnectionId const connectionId) { - _connectivity.insert(connectionId); - indexConnection(connectionId); + _connectionIndex.add(connectionId); Q_EMIT connectionCreated(connectionId); } @@ -227,16 +205,7 @@ bool SimpleGraphModel::setPortData( bool SimpleGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - unindexConnection(connectionId); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) Q_EMIT connectionDeleted(connectionId); @@ -302,61 +271,3 @@ void SimpleGraphModel::loadNode(QJsonObject const &nodeJson) setNodeData(restoredNodeId, NodeRole::Position, pos); } } - -QtNodes::AbstractGraphModel::ConnectionIdSet const &SimpleGraphModel::emptyConnections() -{ - static ConnectionIdSet const empty{}; - return empty; -} - -void SimpleGraphModel::indexConnection(ConnectionId const connectionId) -{ - _nodeConnections[connectionId.inNodeId].insert(connectionId); - _nodeConnections[connectionId.outNodeId].insert(connectionId); - _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); - _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); -} - -void SimpleGraphModel::unindexConnection(ConnectionId const connectionId) -{ - auto eraseFromNode = [&](NodeId nodeId) { - auto nodeIt = _nodeConnections.find(nodeId); - if (nodeIt == _nodeConnections.end()) { - return; - } - - nodeIt->second.erase(connectionId); - if (nodeIt->second.empty()) { - _nodeConnections.erase(nodeIt); - } - }; - - auto eraseFromPortMap = - [&](std::unordered_map &connectionsByPort, - NodeId nodeId, - PortIndex portIndex) { - auto nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return; - } - - auto portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return; - } - - portIt->second.erase(connectionId); - if (portIt->second.empty()) { - nodeIt->second.erase(portIt); - } - - if (nodeIt->second.empty()) { - connectionsByPort.erase(nodeIt); - } - }; - - eraseFromNode(connectionId.inNodeId); - eraseFromNode(connectionId.outNodeId); - eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); - eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); -} diff --git a/examples/vertical_layout/SimpleGraphModel.hpp b/examples/vertical_layout/SimpleGraphModel.hpp index 8a3a9fb4..5ac09858 100644 --- a/examples/vertical_layout/SimpleGraphModel.hpp +++ b/examples/vertical_layout/SimpleGraphModel.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -94,13 +95,6 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel NodeId newNodeId() override { return _nextNodeId++; } private: - using ConnectionsByPort = std::unordered_map; - - static ConnectionIdSet const &emptyConnections(); - - void indexConnection(ConnectionId const connectionId); - void unindexConnection(ConnectionId const connectionId); - NodeIdSet _nodeIds; /// [Important] This is a user defined data structure backing your model. @@ -110,10 +104,7 @@ class SimpleGraphModel : public QtNodes::AbstractGraphModel /// /// This data structure contains the graph connectivity information in both /// directions, i.e. from Node1 to Node2 and from Node2 to Node1. - ConnectionIdSet _connectivity; - std::unordered_map _nodeConnections; - std::unordered_map _inConnectionsByPort; - std::unordered_map _outConnectionsByPort; + QtNodes::ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; diff --git a/include/QtNodes/ConnectionIdIndex b/include/QtNodes/ConnectionIdIndex new file mode 100644 index 00000000..974ad9dd --- /dev/null +++ b/include/QtNodes/ConnectionIdIndex @@ -0,0 +1 @@ +#include "internal/ConnectionIdIndex.hpp" diff --git a/include/QtNodes/internal/ConnectionIdIndex.hpp b/include/QtNodes/internal/ConnectionIdIndex.hpp new file mode 100644 index 00000000..32c3788f --- /dev/null +++ b/include/QtNodes/internal/ConnectionIdIndex.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include "ConnectionIdHash.hpp" + +#include +#include + +namespace QtNodes { + +class ConnectionIdIndex +{ +public: + using ConnectionSet = std::unordered_set; + using ConnectionsByPort = std::unordered_map; + +public: + ConnectionSet const &connectivity() const { return _connectivity; } + + ConnectionSet const &allConnectionIds(NodeId const nodeId) const + { + auto it = _nodeConnections.find(nodeId); + if (it == _nodeConnections.end()) { + return emptyConnections(); + } + + return it->second; + } + + ConnectionSet const &connections(NodeId nodeId, PortType portType, PortIndex portIndex) const + { + if (portType == PortType::None) { + return emptyConnections(); + } + + auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort + : _outConnectionsByPort; + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return emptyConnections(); + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return emptyConnections(); + } + + return portIt->second; + } + + bool contains(ConnectionId const connectionId) const + { + return _connectivity.find(connectionId) != _connectivity.end(); + } + + void add(ConnectionId const connectionId) + { + if (_connectivity.insert(connectionId).second) { + indexConnection(connectionId); + } + } + + bool remove(ConnectionId const connectionId) + { + auto it = _connectivity.find(connectionId); + if (it == _connectivity.end()) { + return false; + } + + _connectivity.erase(it); + unindexConnection(connectionId); + return true; + } + +private: + static ConnectionSet const &emptyConnections() + { + static ConnectionSet const empty{}; + return empty; + } + + void indexConnection(ConnectionId const connectionId) + { + _nodeConnections[connectionId.inNodeId].insert(connectionId); + _nodeConnections[connectionId.outNodeId].insert(connectionId); + _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); + _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); + } + + void unindexConnection(ConnectionId const connectionId) + { + auto eraseFromNode = [&](NodeId nodeId) { + auto nodeIt = _nodeConnections.find(nodeId); + if (nodeIt == _nodeConnections.end()) { + return; + } + + nodeIt->second.erase(connectionId); + if (nodeIt->second.empty()) { + _nodeConnections.erase(nodeIt); + } + }; + + auto eraseFromPortMap = + [&](std::unordered_map &connectionsByPort, + NodeId nodeId, + PortIndex portIndex) { + auto nodeIt = connectionsByPort.find(nodeId); + if (nodeIt == connectionsByPort.end()) { + return; + } + + auto portIt = nodeIt->second.find(portIndex); + if (portIt == nodeIt->second.end()) { + return; + } + + portIt->second.erase(connectionId); + if (portIt->second.empty()) { + nodeIt->second.erase(portIt); + } + + if (nodeIt->second.empty()) { + connectionsByPort.erase(nodeIt); + } + }; + + eraseFromNode(connectionId.inNodeId); + eraseFromNode(connectionId.outNodeId); + eraseFromPortMap(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); + eraseFromPortMap(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); + } + +private: + ConnectionSet _connectivity; + std::unordered_map _nodeConnections; + std::unordered_map _inConnectionsByPort; + std::unordered_map _outConnectionsByPort; +}; + +} // namespace QtNodes diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index 59f33d92..be984260 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -1,6 +1,7 @@ #pragma once #include "AbstractGraphModel.hpp" +#include "ConnectionIdIndex.hpp" #include "ConnectionIdUtils.hpp" #include "NodeDelegateModelRegistry.hpp" #include "Serializable.hpp" @@ -108,9 +109,6 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel void inPortDataWasSet(NodeId const, PortType const, PortIndex const); private: - using Connection_set = std::unordered_set; - using Connections_by_port = std::unordered_map; - NodeId newNodeId() override { if (_nextNodeId == InvalidNodeId) { @@ -122,10 +120,6 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel void connectDelegateModel(NodeDelegateModel *model, NodeId nodeId); - void indexConnection(ConnectionId const connectionId); - - void unindexConnection(ConnectionId const connectionId); - void sendConnectionCreation(ConnectionId const connectionId); void sendConnectionDeletion(ConnectionId const connectionId); @@ -155,13 +149,7 @@ private Q_SLOTS: std::unordered_map> _models; - std::unordered_set _connectivity; - - std::unordered_map _nodeConnections; - - std::unordered_map _inConnectionsByPort; - - std::unordered_map _outConnectionsByPort; + ConnectionIdIndex _connectionIndex; mutable std::unordered_map _nodeGeometryData; }; diff --git a/include/QtNodes/internal/NodeGroup.hpp b/include/QtNodes/internal/NodeGroup.hpp index 94c3af7c..bc03bc6b 100644 --- a/include/QtNodes/internal/NodeGroup.hpp +++ b/include/QtNodes/internal/NodeGroup.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -134,6 +135,6 @@ public Q_SLOTS: * @brief Static variable to count the number of instances of groups that * were created during execution. Used when automatically naming groups. */ - static int _groupCount; + static std::atomic _groupCount; }; } // namespace QtNodes diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index 8dc4c9d3..9a40a190 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -1,6 +1,5 @@ #include "DataFlowGraphModel.hpp" -#include "ConnectionIdHash.hpp" #include "ConnectionIdUtils.hpp" #include "Definitions.hpp" @@ -16,8 +15,6 @@ namespace QtNodes { namespace { -AbstractGraphModel::ConnectionIdSet const k_empty_connection_ids{}; - NodeId json_value_to_node_id(QJsonValue const &value) { NodeId nodeId = InvalidNodeId; @@ -55,39 +52,18 @@ AbstractGraphModel::NodeIdSet const &DataFlowGraphModel::allNodeIds() const AbstractGraphModel::ConnectionIdSet const & DataFlowGraphModel::allConnectionIds(NodeId const nodeId) const { - auto it = _nodeConnections.find(nodeId); - if (it == _nodeConnections.end()) { - return k_empty_connection_ids; - } - - return it->second; + return _connectionIndex.allConnectionIds(nodeId); } AbstractGraphModel::ConnectionIdSet const &DataFlowGraphModel::connections( NodeId nodeId, PortType portType, PortIndex portIndex) const { - if (portType == PortType::None) { - return k_empty_connection_ids; - } - - auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort - : _outConnectionsByPort; - auto nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return k_empty_connection_ids; - } - - auto portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return k_empty_connection_ids; - } - - return portIt->second; + return _connectionIndex.connections(nodeId, portType, portIndex); } bool DataFlowGraphModel::connectionExists(ConnectionId const connectionId) const { - return (_connectivity.find(connectionId) != _connectivity.end()); + return _connectionIndex.contains(connectionId); } NodeId DataFlowGraphModel::addNode(QString const nodeType) @@ -197,8 +173,7 @@ void DataFlowGraphModel::addConnection(ConnectionId const connectionId) return; } - _connectivity.insert(connectionId); - indexConnection(connectionId); + _connectionIndex.add(connectionId); sendConnectionCreation(connectionId); @@ -251,58 +226,6 @@ void DataFlowGraphModel::connectDelegateModel(NodeDelegateModel *model, NodeId n }); } -void DataFlowGraphModel::indexConnection(ConnectionId const connectionId) -{ - _nodeConnections[connectionId.outNodeId].insert(connectionId); - _nodeConnections[connectionId.inNodeId].insert(connectionId); - _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); - _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); -} - -void DataFlowGraphModel::unindexConnection(ConnectionId const connectionId) -{ - auto erase_from_node = [&](NodeId nodeId) { - auto nodeIt = _nodeConnections.find(nodeId); - if (nodeIt == _nodeConnections.end()) { - return; - } - - nodeIt->second.erase(connectionId); - if (nodeIt->second.empty()) { - _nodeConnections.erase(nodeIt); - } - }; - - auto erase_from_port_map = - [&](std::unordered_map &connectionsByPort, - NodeId nodeId, - PortIndex portIndex) { - auto nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return; - } - - auto portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return; - } - - portIt->second.erase(connectionId); - if (portIt->second.empty()) { - nodeIt->second.erase(portIt); - } - - if (nodeIt->second.empty()) { - connectionsByPort.erase(nodeIt); - } - }; - - erase_from_node(connectionId.outNodeId); - erase_from_node(connectionId.inNodeId); - erase_from_port_map(_outConnectionsByPort, connectionId.outNodeId, connectionId.outPortIndex); - erase_from_port_map(_inConnectionsByPort, connectionId.inNodeId, connectionId.inPortIndex); -} - void DataFlowGraphModel::sendConnectionCreation(ConnectionId const connectionId) { Q_EMIT connectionCreated(connectionId); @@ -590,18 +513,9 @@ bool DataFlowGraphModel::setPortData( bool DataFlowGraphModel::deleteConnection(ConnectionId const connectionId) { - bool disconnected = false; - - auto it = _connectivity.find(connectionId); - - if (it != _connectivity.end()) { - disconnected = true; - - _connectivity.erase(it); - } + bool const disconnected = _connectionIndex.remove(connectionId); if (disconnected) { - unindexConnection(connectionId); sendConnectionDeletion(connectionId); propagateEmptyDataTo(getNodeId(PortType::In, connectionId), @@ -665,7 +579,7 @@ QJsonObject DataFlowGraphModel::save() const sceneJson["nodes"] = nodesJsonArray; QJsonArray connJsonArray; - for (auto const &cid : _connectivity) { + for (auto const &cid : _connectionIndex.connectivity()) { connJsonArray.append(toJson(cid)); } sceneJson["connections"] = connJsonArray; diff --git a/src/NodeGroup.cpp b/src/NodeGroup.cpp index fdf89bd1..b8e9af99 100644 --- a/src/NodeGroup.cpp +++ b/src/NodeGroup.cpp @@ -5,6 +5,7 @@ #include #include +#include #include using QtNodes::GroupGraphicsObject; @@ -12,7 +13,7 @@ using QtNodes::NodeGraphicsObject; using QtNodes::NodeGroup; using QtNodes::NodeId; -int NodeGroup::_groupCount = 0; +std::atomic NodeGroup::_groupCount{0}; NodeGroup::NodeGroup(std::vector nodes, GroupId groupId, @@ -24,7 +25,7 @@ NodeGroup::NodeGroup(std::vector nodes, , _childNodes(std::move(nodes)) , _groupGraphicsObject(nullptr) { - _groupCount++; + _groupCount.fetch_add(1, std::memory_order_relaxed); } NodeGroup::~NodeGroup() = default; @@ -104,7 +105,7 @@ bool NodeGroup::empty() const int NodeGroup::groupCount() { - return _groupCount; + return _groupCount.load(std::memory_order_relaxed); } void NodeGroup::addNode(NodeGraphicsObject *node) diff --git a/test/include/TestGraphModel.hpp b/test/include/TestGraphModel.hpp index 2c81094b..55970ef2 100644 --- a/test/include/TestGraphModel.hpp +++ b/test/include/TestGraphModel.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -36,40 +37,19 @@ class TestGraphModel : public AbstractGraphModel ConnectionIdSet const &allConnectionIds(NodeId const nodeId) const override { - auto const it = _nodeConnections.find(nodeId); - if (it == _nodeConnections.end()) { - return empty_connections(); - } - - return it->second; + return _connection_index.allConnectionIds(nodeId); } ConnectionIdSet const &connections(NodeId nodeId, PortType portType, PortIndex portIndex) const override { - if (portType == PortType::None) { - return empty_connections(); - } - - auto const &connectionsByPort = (portType == PortType::In) ? _inConnectionsByPort - : _outConnectionsByPort; - auto const nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return empty_connections(); - } - - auto const portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return empty_connections(); - } - - return portIt->second; + return _connection_index.connections(nodeId, portType, portIndex); } bool connectionExists(ConnectionId const connectionId) const override { - return _connections.find(connectionId) != _connections.end(); + return _connection_index.contains(connectionId); } NodeId addNode(QString const nodeType = QString()) override @@ -95,11 +75,7 @@ class TestGraphModel : public AbstractGraphModel void addConnection(ConnectionId const connectionId) override { if (connectionPossible(connectionId)) { - _connections.insert(connectionId); - _nodeConnections[connectionId.inNodeId].insert(connectionId); - _nodeConnections[connectionId.outNodeId].insert(connectionId); - _inConnectionsByPort[connectionId.inNodeId][connectionId.inPortIndex].insert(connectionId); - _outConnectionsByPort[connectionId.outNodeId][connectionId.outPortIndex].insert(connectionId); + _connection_index.add(connectionId); Q_EMIT connectionCreated(connectionId); } } @@ -204,19 +180,7 @@ class TestGraphModel : public AbstractGraphModel bool deleteConnection(ConnectionId const connectionId) override { - auto it = _connections.find(connectionId); - if (it != _connections.end()) { - _connections.erase(it); - eraseIndexedConnection(_nodeConnections, connectionId.inNodeId, connectionId); - eraseIndexedConnection(_nodeConnections, connectionId.outNodeId, connectionId); - eraseIndexedConnection(_inConnectionsByPort, - connectionId.inNodeId, - connectionId.inPortIndex, - connectionId); - eraseIndexedConnection(_outConnectionsByPort, - connectionId.outNodeId, - connectionId.outPortIndex, - connectionId); + if (_connection_index.remove(connectionId)) { Q_EMIT connectionDeleted(connectionId); return true; } @@ -297,60 +261,8 @@ class TestGraphModel : public AbstractGraphModel } private: - using ConnectionsByPort = std::unordered_map; - - static void eraseIndexedConnection(std::unordered_map &connectionsByNode, - NodeId nodeId, - ConnectionId const &connectionId) - { - auto nodeIt = connectionsByNode.find(nodeId); - if (nodeIt == connectionsByNode.end()) { - return; - } - - nodeIt->second.erase(connectionId); - if (nodeIt->second.empty()) { - connectionsByNode.erase(nodeIt); - } - } - - static void eraseIndexedConnection( - std::unordered_map &connectionsByPort, - NodeId nodeId, - PortIndex portIndex, - ConnectionId const &connectionId) - { - auto nodeIt = connectionsByPort.find(nodeId); - if (nodeIt == connectionsByPort.end()) { - return; - } - - auto portIt = nodeIt->second.find(portIndex); - if (portIt == nodeIt->second.end()) { - return; - } - - portIt->second.erase(connectionId); - if (portIt->second.empty()) { - nodeIt->second.erase(portIt); - } - - if (nodeIt->second.empty()) { - connectionsByPort.erase(nodeIt); - } - } - - static ConnectionIdSet const &empty_connections() - { - static ConnectionIdSet const empty{}; - return empty; - } - NodeId _nextNodeId = 1; NodeIdSet _nodeIds; - ConnectionIdSet _connections; - std::unordered_map _nodeConnections; - std::unordered_map _inConnectionsByPort; - std::unordered_map _outConnectionsByPort; + QtNodes::ConnectionIdIndex _connection_index; std::unordered_map> _nodeData; }; From 2a8de7e989354a1eb16049fc89a1617570621a65 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 12:37:44 +0100 Subject: [PATCH 23/26] Trim validation header and tighten trivial APIs --- CMakeLists.txt | 1 + .../QtNodes/internal/ConnectionIdIndex.hpp | 6 +- .../QtNodes/internal/DataFlowGraphModel.hpp | 7 +- include/QtNodes/internal/Definitions.hpp | 6 +- .../QtNodes/internal/NodeDelegateModel.hpp | 4 +- .../QtNodes/internal/NodeGraphicsObject.hpp | 10 +- include/QtNodes/internal/NodeGroup.hpp | 2 +- .../internal/SerializationValidation.hpp | 144 ++--------------- src/NodeGroup.cpp | 2 +- src/SerializationValidation.cpp | 149 ++++++++++++++++++ 10 files changed, 180 insertions(+), 151 deletions(-) create mode 100644 src/SerializationValidation.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 31e6164f..2cd85c33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ set(CPP_SOURCE_FILES src/NodeState.cpp src/NodeStyle.cpp src/StyleCollection.cpp + src/SerializationValidation.cpp src/UndoCommands.cpp src/locateNode.cpp src/GroupGraphicsObject.cpp diff --git a/include/QtNodes/internal/ConnectionIdIndex.hpp b/include/QtNodes/internal/ConnectionIdIndex.hpp index 32c3788f..24f8a739 100644 --- a/include/QtNodes/internal/ConnectionIdIndex.hpp +++ b/include/QtNodes/internal/ConnectionIdIndex.hpp @@ -14,7 +14,7 @@ class ConnectionIdIndex using ConnectionsByPort = std::unordered_map; public: - ConnectionSet const &connectivity() const { return _connectivity; } + ConnectionSet const &connectivity() const noexcept { return _connectivity; } ConnectionSet const &allConnectionIds(NodeId const nodeId) const { @@ -47,7 +47,7 @@ class ConnectionIdIndex return portIt->second; } - bool contains(ConnectionId const connectionId) const + bool contains(ConnectionId const connectionId) const noexcept { return _connectivity.find(connectionId) != _connectivity.end(); } @@ -72,7 +72,7 @@ class ConnectionIdIndex } private: - static ConnectionSet const &emptyConnections() + static ConnectionSet const &emptyConnections() noexcept { static ConnectionSet const empty{}; return empty; diff --git a/include/QtNodes/internal/DataFlowGraphModel.hpp b/include/QtNodes/internal/DataFlowGraphModel.hpp index be984260..da6fb8a4 100644 --- a/include/QtNodes/internal/DataFlowGraphModel.hpp +++ b/include/QtNodes/internal/DataFlowGraphModel.hpp @@ -34,7 +34,10 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel public: DataFlowGraphModel(std::shared_ptr registry); - [[nodiscard]] std::shared_ptr dataModelRegistry() { return _registry; } + [[nodiscard]] std::shared_ptr dataModelRegistry() noexcept + { + return _registry; + } public: NodeIdSet const &allNodeIds() const override; @@ -103,7 +106,7 @@ class NODE_EDITOR_PUBLIC DataFlowGraphModel } /// Loops do not make any sense in uni-direction data propagation - bool loopsEnabled() const override { return false; } + bool loopsEnabled() const noexcept override { return false; } Q_SIGNALS: void inPortDataWasSet(NodeId const, PortType const, PortIndex const); diff --git a/include/QtNodes/internal/Definitions.hpp b/include/QtNodes/internal/Definitions.hpp index be35a7fd..3083f6bd 100644 --- a/include/QtNodes/internal/Definitions.hpp +++ b/include/QtNodes/internal/Definitions.hpp @@ -111,18 +111,18 @@ struct ConnectionId PortIndex inPortIndex; }; -inline bool operator==(ConnectionId const &a, ConnectionId const &b) +inline bool operator==(ConnectionId const &a, ConnectionId const &b) noexcept { return a.outNodeId == b.outNodeId && a.outPortIndex == b.outPortIndex && a.inNodeId == b.inNodeId && a.inPortIndex == b.inPortIndex; } -inline bool operator!=(ConnectionId const &a, ConnectionId const &b) +inline bool operator!=(ConnectionId const &a, ConnectionId const &b) noexcept { return !(a == b); } -inline void invertConnection(ConnectionId &id) +inline void invertConnection(ConnectionId &id) noexcept { std::swap(id.outNodeId, id.inNodeId); std::swap(id.outPortIndex, id.inPortIndex); diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index 3b39e0d8..58eb2e9f 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -27,9 +27,9 @@ struct NodeValidationState Warning = 1, ///< Some inputs are missing or questionable, processing may be unreliable. Error = 2, ///< Inputs or settings are invalid, preventing successful computation. }; - bool isValid() const { return _state == State::Valid; }; + bool isValid() const noexcept { return _state == State::Valid; }; QString message() const { return _stateMessage; } - State state() const { return _state; } + State state() const noexcept { return _state; } State _state{State::Valid}; QString _stateMessage{""}; diff --git a/include/QtNodes/internal/NodeGraphicsObject.hpp b/include/QtNodes/internal/NodeGraphicsObject.hpp index bd707291..08d7e952 100644 --- a/include/QtNodes/internal/NodeGraphicsObject.hpp +++ b/include/QtNodes/internal/NodeGraphicsObject.hpp @@ -26,7 +26,7 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject // Needed for qgraphicsitem_cast enum { Type = UserType + 1 }; - int type() const override { return Type; } + int type() const noexcept override { return Type; } public: NodeGraphicsObject(BasicGraphicsScene &scene, NodeId node); @@ -38,15 +38,15 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject BasicGraphicsScene *nodeScene() const; - NodeId nodeId() { return _nodeId; } + NodeId nodeId() noexcept { return _nodeId; } - NodeId nodeId() const { return _nodeId; } + NodeId nodeId() const noexcept { return _nodeId; } NodeState &nodeState() { return _nodeState; } NodeState const &nodeState() const { return _nodeState; } - GraphicsView *currentGraphicsView() const { return _currentGraphicsView; } + GraphicsView *currentGraphicsView() const noexcept { return _currentGraphicsView; } QRectF boundingRect() const override; @@ -75,7 +75,7 @@ class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject void setNodeGroup(std::shared_ptr group); /// Unsets NodeGroup, setting it to an empty pointer. - void unsetNodeGroup() { _nodeGroup = std::weak_ptr(); } + void unsetNodeGroup() noexcept { _nodeGroup = std::weak_ptr(); } /// Getter for the NodeGroup object. std::weak_ptr nodeGroup() const { return _nodeGroup; } diff --git a/include/QtNodes/internal/NodeGroup.hpp b/include/QtNodes/internal/NodeGroup.hpp index bc03bc6b..ec09e26f 100644 --- a/include/QtNodes/internal/NodeGroup.hpp +++ b/include/QtNodes/internal/NodeGroup.hpp @@ -90,7 +90,7 @@ class NODE_EDITOR_PUBLIC NodeGroup : public QObject * @brief Returns the number of groups created during the program's execution. * Used when automatically naming groups. */ - static int groupCount(); + static int groupCount() noexcept; public Q_SLOTS: /** diff --git a/include/QtNodes/internal/SerializationValidation.hpp b/include/QtNodes/internal/SerializationValidation.hpp index 8f785061..638050d2 100644 --- a/include/QtNodes/internal/SerializationValidation.hpp +++ b/include/QtNodes/internal/SerializationValidation.hpp @@ -8,150 +8,26 @@ #include #include -#include - namespace QtNodes::detail { -inline bool read_unsigned_number(QJsonValue const &value, quint64 maxValue, quint64 &result) -{ - if (!value.isDouble()) { - return false; - } - - double const parsed = value.toDouble(); - if (!std::isfinite(parsed) || parsed < 0.0 || parsed > static_cast(maxValue)) { - return false; - } - - quint64 const integral = static_cast(parsed); - if (parsed != static_cast(integral)) { - return false; - } - - result = integral; - return true; -} - -inline bool read_node_id(QJsonValue const &value, NodeId &nodeId) -{ - quint64 parsed = 0; - - if (!read_unsigned_number(value, InvalidNodeId - 1ull, parsed)) { - return false; - } - - nodeId = static_cast(parsed); - return true; -} - -inline bool read_group_id(QJsonValue const &value, GroupId &groupId) -{ - quint64 parsed = 0; - - if (!read_unsigned_number(value, InvalidGroupId - 1ull, parsed)) { - return false; - } - - groupId = static_cast(parsed); - return true; -} - -inline bool read_port_index(QJsonValue const &value, PortIndex &portIndex) -{ - quint64 parsed = 0; - - if (!read_unsigned_number(value, InvalidPortIndex - 1ull, parsed)) { - return false; - } - - portIndex = static_cast(parsed); - return true; -} - -inline bool read_finite_number(QJsonValue const &value, double &result) -{ - if (!value.isDouble()) { - return false; - } - - double const parsed = value.toDouble(); - if (!std::isfinite(parsed)) { - return false; - } - - result = parsed; - return true; -} - -inline bool read_required_object(QJsonObject const &obj, QString const &key, QJsonObject &result) -{ - auto const it = obj.find(key); - - if (it == obj.end() || !it->isObject()) { - return false; - } - - result = it->toObject(); - return true; -} - -inline bool read_required_array(QJsonObject const &obj, QString const &key, QJsonArray &result) -{ - auto const it = obj.find(key); - - if (it == obj.end() || !it->isArray()) { - return false; - } - - result = it->toArray(); - return true; -} - -inline bool read_required_string(QJsonObject const &obj, QString const &key, QString &result) -{ - auto const it = obj.find(key); - - if (it == obj.end() || !it->isString()) { - return false; - } - - result = it->toString(); - return true; -} +bool read_unsigned_number(QJsonValue const &value, quint64 maxValue, quint64 &result); -inline bool read_optional_bool(QJsonObject const &obj, QString const &key, bool &result) -{ - auto const it = obj.find(key); +bool read_node_id(QJsonValue const &value, NodeId &nodeId); - if (it == obj.end()) { - return true; - } +bool read_group_id(QJsonValue const &value, GroupId &groupId); - if (!it->isBool()) { - return false; - } +bool read_port_index(QJsonValue const &value, PortIndex &portIndex); - result = it->toBool(); - return true; -} +bool read_finite_number(QJsonValue const &value, double &result); -inline bool read_required_point(QJsonObject const &obj, QString const &key, QPointF &result) -{ - QJsonObject pointObject; +bool read_required_object(QJsonObject const &obj, QString const &key, QJsonObject &result); - if (!read_required_object(obj, key, pointObject)) { - return false; - } +bool read_required_array(QJsonObject const &obj, QString const &key, QJsonArray &result); - double x = 0.0; - double y = 0.0; +bool read_required_string(QJsonObject const &obj, QString const &key, QString &result); - if (!read_finite_number(pointObject["x"], x) || !read_finite_number(pointObject["y"], y)) { - return false; - } +bool read_optional_bool(QJsonObject const &obj, QString const &key, bool &result); - result = QPointF(x, y); - return true; -} +bool read_required_point(QJsonObject const &obj, QString const &key, QPointF &result); } // namespace QtNodes::detail diff --git a/src/NodeGroup.cpp b/src/NodeGroup.cpp index b8e9af99..d7757d43 100644 --- a/src/NodeGroup.cpp +++ b/src/NodeGroup.cpp @@ -103,7 +103,7 @@ bool NodeGroup::empty() const return _childNodes.empty(); } -int NodeGroup::groupCount() +int NodeGroup::groupCount() noexcept { return _groupCount.load(std::memory_order_relaxed); } diff --git a/src/SerializationValidation.cpp b/src/SerializationValidation.cpp new file mode 100644 index 00000000..e47eac40 --- /dev/null +++ b/src/SerializationValidation.cpp @@ -0,0 +1,149 @@ +#include "SerializationValidation.hpp" + +#include + +namespace QtNodes::detail { + +bool read_unsigned_number(QJsonValue const &value, quint64 maxValue, quint64 &result) +{ + if (!value.isDouble()) { + return false; + } + + double const parsed = value.toDouble(); + if (!std::isfinite(parsed) || parsed < 0.0 || parsed > static_cast(maxValue)) { + return false; + } + + quint64 const integral = static_cast(parsed); + if (parsed != static_cast(integral)) { + return false; + } + + result = integral; + return true; +} + +bool read_node_id(QJsonValue const &value, NodeId &nodeId) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidNodeId - 1ull, parsed)) { + return false; + } + + nodeId = static_cast(parsed); + return true; +} + +bool read_group_id(QJsonValue const &value, GroupId &groupId) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidGroupId - 1ull, parsed)) { + return false; + } + + groupId = static_cast(parsed); + return true; +} + +bool read_port_index(QJsonValue const &value, PortIndex &portIndex) +{ + quint64 parsed = 0; + + if (!read_unsigned_number(value, InvalidPortIndex - 1ull, parsed)) { + return false; + } + + portIndex = static_cast(parsed); + return true; +} + +bool read_finite_number(QJsonValue const &value, double &result) +{ + if (!value.isDouble()) { + return false; + } + + double const parsed = value.toDouble(); + if (!std::isfinite(parsed)) { + return false; + } + + result = parsed; + return true; +} + +bool read_required_object(QJsonObject const &obj, QString const &key, QJsonObject &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isObject()) { + return false; + } + + result = it->toObject(); + return true; +} + +bool read_required_array(QJsonObject const &obj, QString const &key, QJsonArray &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isArray()) { + return false; + } + + result = it->toArray(); + return true; +} + +bool read_required_string(QJsonObject const &obj, QString const &key, QString &result) +{ + auto const it = obj.find(key); + + if (it == obj.end() || !it->isString()) { + return false; + } + + result = it->toString(); + return true; +} + +bool read_optional_bool(QJsonObject const &obj, QString const &key, bool &result) +{ + auto const it = obj.find(key); + + if (it == obj.end()) { + return true; + } + + if (!it->isBool()) { + return false; + } + + result = it->toBool(); + return true; +} + +bool read_required_point(QJsonObject const &obj, QString const &key, QPointF &result) +{ + QJsonObject pointObject; + + if (!read_required_object(obj, key, pointObject)) { + return false; + } + + double x = 0.0; + double y = 0.0; + + if (!read_finite_number(pointObject["x"], x) || !read_finite_number(pointObject["y"], y)) { + return false; + } + + result = QPointF(x, y); + return true; +} + +} // namespace QtNodes::detail From 28162bf85b7097a83cc0bfd7c66dcf237af9f7fc Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 12:50:50 +0100 Subject: [PATCH 24/26] Unify validation APIs and stage scene loads --- docs/api/index.rst | 2 +- docs/getting-started/concepts.rst | 2 +- docs/guide/advanced.rst | 2 +- docs/guide/graph-models.rst | 34 +++++++++---------- examples/calculator/DivisionModel.hpp | 20 +++++------ examples/custom_painter/SimpleGraphModel.cpp | 2 +- examples/dynamic_ports/DynamicPortsModel.cpp | 2 +- examples/node_validation/ValidatedModel.hpp | 24 +++++-------- .../simple_graph_model/SimpleGraphModel.cpp | 2 +- examples/vertical_layout/SimpleGraphModel.cpp | 2 +- include/QtNodes/internal/Definitions.hpp | 10 +++--- .../QtNodes/internal/NodeDelegateModel.hpp | 7 ++++ src/DataFlowGraphModel.cpp | 4 +-- src/DataFlowGraphicsScene.cpp | 23 ++++++++++--- src/DefaultNodePainter.cpp | 8 ++--- src/NodeGraphicsObject.cpp | 6 ++-- test/src/TestNodeValidation.cpp | 13 ++----- 17 files changed, 84 insertions(+), 79 deletions(-) diff --git a/docs/api/index.rst b/docs/api/index.rst index 9c041d0a..577b4e7f 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -98,7 +98,7 @@ Quick Reference - Type, Position, Size, Caption, CaptionVisible, Style, InternalData, InPortCount, OutPortCount, Widget, ValidationState, ProcessingStatus * - ``PortRole`` - - Data, DataType, ConnectionPolicyRole, CaptionVisible, Caption + - Data, DataType, ConnectionPolicy, CaptionVisible, Caption * - ``NodeFlag`` - NoFlags, Resizable, Locked * - ``NodeValidationState::State`` diff --git a/docs/getting-started/concepts.rst b/docs/getting-started/concepts.rst index cfd7691d..d178e7cb 100644 --- a/docs/getting-started/concepts.rst +++ b/docs/getting-started/concepts.rst @@ -131,7 +131,7 @@ Key roles: ``Type``, ``Position``, ``Caption``, ``InPortCount``, ``OutPortCount` // Get port data type auto type = model.portData(nodeId, PortType::In, 0, PortRole::DataType); -Key roles: ``Data``, ``DataType``, ``Caption``, ``ConnectionPolicyRole`` +Key roles: ``Data``, ``DataType``, ``Caption``, ``ConnectionPolicy`` See :doc:`/guide/graph-models` for the complete role reference. diff --git a/docs/guide/advanced.rst b/docs/guide/advanced.rst index 601e4ef0..137ff350 100644 --- a/docs/guide/advanced.rst +++ b/docs/guide/advanced.rst @@ -199,7 +199,7 @@ Control how many connections a port accepts: QVariant MyModel::portData(NodeId nodeId, PortType portType, PortIndex portIndex, PortRole role) const override { - if (role == PortRole::ConnectionPolicyRole) { + if (role == PortRole::ConnectionPolicy) { if (portType == PortType::In) { // Inputs accept only one connection return QVariant::fromValue(ConnectionPolicy::One); diff --git a/docs/guide/graph-models.rst b/docs/guide/graph-models.rst index e6dec43d..6daf0d3e 100644 --- a/docs/guide/graph-models.rst +++ b/docs/guide/graph-models.rst @@ -89,7 +89,13 @@ Remove connections first, then the node: return false; // Remove all connections involving this node - for (auto& conn : allConnectionIds(nodeId)) { + std::vector attachedConnections; + auto const &connections = allConnectionIds(nodeId); + attachedConnections.reserve(connections.size()); + for (auto const &conn : connections) { + attachedConnections.push_back(conn); + } + for (auto const &conn : attachedConnections) { deleteConnection(conn); } @@ -183,23 +189,10 @@ Return connections filtered by node and port: .. code-block:: cpp - std::unordered_set + AbstractGraphModel::ConnectionIdSet const & MyGraphModel::connections(NodeId nodeId, PortType portType, PortIndex portIndex) const { - std::unordered_set result; - for (const auto& conn : _connections) { - if (portType == PortType::In && - conn.inNodeId == nodeId && - conn.inPortIndex == portIndex) { - result.insert(conn); - } - else if (portType == PortType::Out && - conn.outNodeId == nodeId && - conn.outPortIndex == portIndex) { - result.insert(conn); - } - } - return result; + return _connectionIndex.connections(nodeId, portType, portIndex); } **Connection Validation** @@ -244,9 +237,16 @@ Implement ``portData()`` for port-specific information: * - ``DataType`` - ``NodeDataType`` - Type descriptor for compatibility checks - * - ``ConnectionPolicyRole`` + * - ``ConnectionPolicy`` - ``ConnectionPolicy`` - ``One`` (single connection) or ``Many`` + +.. note:: + + ``allNodeIds()``, ``allConnectionIds()``, and ``connections()`` return + references to storage owned by the model. Implementations must keep those + containers alive for the duration of the call site rather than constructing + and returning temporaries. * - ``Caption`` - ``QString`` - Port label text diff --git a/examples/calculator/DivisionModel.hpp b/examples/calculator/DivisionModel.hpp index 1b7865c7..bf97fd10 100644 --- a/examples/calculator/DivisionModel.hpp +++ b/examples/calculator/DivisionModel.hpp @@ -55,27 +55,25 @@ class DivisionModel : public MathOperationDataModel auto n1 = _number1.lock(); auto n2 = _number2.lock(); - QtNodes::NodeValidationState state; if (n2 && (n2->number() == 0.0)) { - state._state = QtNodes::NodeValidationState::State::Error; - state._stateMessage = QStringLiteral("Division by zero error"); - setValidationState(state); + setValidationState(QtNodes::NodeValidationState( + QtNodes::NodeValidationState::State::Error, + QStringLiteral("Division by zero error"))); _result.reset(); - } else if ( n2 && (n2->number() < 1e-5)) { - state._state = QtNodes::NodeValidationState::State::Warning; - state._stateMessage = QStringLiteral("Very small divident. Result might overflow"); - setValidationState(state); + } else if (n2 && (n2->number() < 1e-5)) { + setValidationState(QtNodes::NodeValidationState( + QtNodes::NodeValidationState::State::Warning, + QStringLiteral("Very small divident. Result might overflow"))); if (n1) { _result = std::make_shared(n1->number() / n2->number()); } else { _result.reset(); } } else if (n1 && n2) { - setValidationState(state); + setValidationState(QtNodes::NodeValidationState()); _result = std::make_shared(n1->number() / n2->number()); } else { - QtNodes::NodeValidationState state; - setValidationState(state); + setValidationState(QtNodes::NodeValidationState()); _result.reset(); } diff --git a/examples/custom_painter/SimpleGraphModel.cpp b/examples/custom_painter/SimpleGraphModel.cpp index 526ccfd1..c041ce8c 100644 --- a/examples/custom_painter/SimpleGraphModel.cpp +++ b/examples/custom_painter/SimpleGraphModel.cpp @@ -144,7 +144,7 @@ QVariant SimpleGraphModel::portData(NodeId nodeId, case PortRole::DataType: return QVariant(); - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); case PortRole::CaptionVisible: diff --git a/examples/dynamic_ports/DynamicPortsModel.cpp b/examples/dynamic_ports/DynamicPortsModel.cpp index 1af2970a..f3b17a18 100644 --- a/examples/dynamic_ports/DynamicPortsModel.cpp +++ b/examples/dynamic_ports/DynamicPortsModel.cpp @@ -193,7 +193,7 @@ QVariant DynamicPortsModel::portData(NodeId nodeId, return QVariant(); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); break; diff --git a/examples/node_validation/ValidatedModel.hpp b/examples/node_validation/ValidatedModel.hpp index 1c702fbc..bb5264d8 100644 --- a/examples/node_validation/ValidatedModel.hpp +++ b/examples/node_validation/ValidatedModel.hpp @@ -61,10 +61,8 @@ class ValidatedModel : public NodeDelegateModel if (!textData || textData->isEmpty()) { // No input - set Empty status setNodeProcessingStatus(NodeProcessingStatus::Empty); - NodeValidationState state; - state._state = NodeValidationState::State::Warning; - state._stateMessage = "No input data"; - setValidationState(state); + setValidationState(NodeValidationState(NodeValidationState::State::Warning, + "No input data")); _outputData.reset(); if (_label) @@ -107,10 +105,9 @@ class ValidatedModel : public NodeDelegateModel if (text.length() < _minLength) { // Validation failed - NodeValidationState state; - state._state = NodeValidationState::State::Error; - state._stateMessage = QString("Text must be at least %1 characters").arg(_minLength); - setValidationState(state); + setValidationState(NodeValidationState( + NodeValidationState::State::Error, + QString("Text must be at least %1 characters").arg(_minLength))); setNodeProcessingStatus(NodeProcessingStatus::Failed); @@ -121,10 +118,8 @@ class ValidatedModel : public NodeDelegateModel Q_EMIT dataInvalidated(0); } else if (text.length() < _minLength * 2) { // Partial success - warning - NodeValidationState state; - state._state = NodeValidationState::State::Warning; - state._stateMessage = "Text is short but acceptable"; - setValidationState(state); + setValidationState(NodeValidationState(NodeValidationState::State::Warning, + "Text is short but acceptable")); setNodeProcessingStatus(NodeProcessingStatus::Partial); @@ -135,10 +130,7 @@ class ValidatedModel : public NodeDelegateModel Q_EMIT dataUpdated(0); } else { // Full success - NodeValidationState state; - state._state = NodeValidationState::State::Valid; - state._stateMessage = ""; - setValidationState(state); + setValidationState(NodeValidationState()); setNodeProcessingStatus(NodeProcessingStatus::Updated); diff --git a/examples/simple_graph_model/SimpleGraphModel.cpp b/examples/simple_graph_model/SimpleGraphModel.cpp index 830cf13e..c11f1e0f 100644 --- a/examples/simple_graph_model/SimpleGraphModel.cpp +++ b/examples/simple_graph_model/SimpleGraphModel.cpp @@ -171,7 +171,7 @@ QVariant SimpleGraphModel::portData(NodeId nodeId, return QVariant(); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); break; diff --git a/examples/vertical_layout/SimpleGraphModel.cpp b/examples/vertical_layout/SimpleGraphModel.cpp index bc743208..b2f9e453 100644 --- a/examples/vertical_layout/SimpleGraphModel.cpp +++ b/examples/vertical_layout/SimpleGraphModel.cpp @@ -171,7 +171,7 @@ QVariant SimpleGraphModel::portData(NodeId nodeId, return QVariant(); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: return QVariant::fromValue(ConnectionPolicy::One); break; diff --git a/include/QtNodes/internal/Definitions.hpp b/include/QtNodes/internal/Definitions.hpp index 3083f6bd..a5dcdf9a 100644 --- a/include/QtNodes/internal/Definitions.hpp +++ b/include/QtNodes/internal/Definitions.hpp @@ -54,11 +54,11 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(NodeFlags) * Constants for fetching port-related information from the GraphModel. */ enum class PortRole { - Data = 0, ///< `std::shared_ptr`. - DataType = 1, ///< `QString` describing the port data type. - ConnectionPolicyRole = 2, ///< `enum` ConnectionPolicyRole - CaptionVisible = 3, ///< `bool` for caption visibility. - Caption = 4, ///< `QString` for port caption. + Data = 0, ///< `std::shared_ptr`. + DataType = 1, ///< `QString` describing the port data type. + ConnectionPolicy = 2, ///< `enum` ConnectionPolicy + CaptionVisible = 3, ///< `bool` for caption visibility. + Caption = 4, ///< `QString` for port caption. }; Q_ENUM_NS(PortRole) diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index 58eb2e9f..f98c9f81 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -27,10 +27,17 @@ struct NodeValidationState Warning = 1, ///< Some inputs are missing or questionable, processing may be unreliable. Error = 2, ///< Inputs or settings are invalid, preventing successful computation. }; + + NodeValidationState(State state = State::Valid, QString message = QString()) + : _state(state) + , _stateMessage(std::move(message)) + {} + bool isValid() const noexcept { return _state == State::Valid; }; QString message() const { return _stateMessage; } State state() const noexcept { return _state; } +private: State _state{State::Valid}; QString _stateMessage{""}; }; diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index 9a40a190..4902a46a 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -114,7 +114,7 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con PortIndex const portIndex = getPortIndex(portType, connectionId); auto const &connected = connections(nodeId, portType, portIndex); - auto policy = portData(nodeId, portType, portIndex, PortRole::ConnectionPolicyRole) + auto policy = portData(nodeId, portType, portIndex, PortRole::ConnectionPolicy) .value(); return connected.empty() || (policy == ConnectionPolicy::Many); @@ -455,7 +455,7 @@ QVariant DataFlowGraphModel::portData(NodeId nodeId, result = QVariant::fromValue(model->dataType(portType, portIndex)); break; - case PortRole::ConnectionPolicyRole: + case PortRole::ConnectionPolicy: result = QVariant::fromValue(model->portConnectionPolicy(portType, portIndex)); break; diff --git a/src/DataFlowGraphicsScene.cpp b/src/DataFlowGraphicsScene.cpp index 1883099c..2964928c 100644 --- a/src/DataFlowGraphicsScene.cpp +++ b/src/DataFlowGraphicsScene.cpp @@ -195,6 +195,14 @@ void restore_groups_from_json(QJsonObject const &sceneJson, QtNodes::BasicGraphi } } +void load_scene_json(QJsonObject const &sceneJson, + QtNodes::DataFlowGraphModel &graphModel, + QtNodes::BasicGraphicsScene &scene) +{ + graphModel.load(sceneJson); + restore_groups_from_json(sceneJson, scene); +} + } // namespace namespace QtNodes { @@ -367,19 +375,26 @@ bool DataFlowGraphicsScene::load() return false; } + DataFlowGraphModel stagingModel(_graphModel.dataModelRegistry()); + DataFlowGraphicsScene stagingScene(stagingModel); + + try { + load_scene_json(sceneJson, stagingModel, stagingScene); + } catch (...) { + return false; + } + QJsonObject const previousSceneJson = scene_json_with_groups(_graphModel, *this); clearScene(); try { - _graphModel.load(sceneJson); - restore_groups_from_json(sceneJson, *this); + load_scene_json(sceneJson, _graphModel, *this); } catch (...) { clearScene(); try { - _graphModel.load(previousSceneJson); - restore_groups_from_json(previousSceneJson, *this); + load_scene_json(previousSceneJson, _graphModel, *this); } catch (...) { clearScene(); } diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 0cb8ce9b..f1add82b 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -416,7 +416,7 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo if (var.canConvert()) { auto state = var.value(); - switch (state._state) { + switch (state.state()) { case NodeValidationState::State::Error: { invalid = true; color = nodeStyle.ErrorColor; @@ -706,15 +706,15 @@ void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObjec return; auto state = var.value(); - if (state._state == NodeValidationState::State::Valid) + if (state.isValid()) return; QSize size = geometry.size(nodeId); QSize const iconSize(16, 16); - QColor color = (state._state == NodeValidationState::State::Error) ? nodeStyle.ErrorColor - : nodeStyle.WarningColor; + QColor color = (state.state() == NodeValidationState::State::Error) ? nodeStyle.ErrorColor + : nodeStyle.WarningColor; qreal const dpr = painter->device() ? painter->device()->devicePixelRatioF() : 1.0; diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 6f5698e3..98da8aa1 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -222,8 +222,8 @@ void NodeGraphicsObject::updateValidationTooltip() QVariant var = _graphModel.nodeData(_nodeId, NodeRole::ValidationState); if (var.canConvert()) { auto state = var.value(); - if (state._state != NodeValidationState::State::Valid) { - tooltip = state._stateMessage; + if (!state.isValid()) { + tooltip = state.message(); } } setToolTip(tooltip); @@ -282,7 +282,7 @@ void NodeGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent *event) .portData(_nodeId, portToCheck, portIndex, - PortRole::ConnectionPolicyRole) + PortRole::ConnectionPolicy) .value(); if (!connected.empty() && outPolicy == ConnectionPolicy::One) { diff --git a/test/src/TestNodeValidation.cpp b/test/src/TestNodeValidation.cpp index b7b3b96e..0cd643ea 100644 --- a/test/src/TestNodeValidation.cpp +++ b/test/src/TestNodeValidation.cpp @@ -43,10 +43,7 @@ class TestValidatedModel : public NodeDelegateModel // Expose validation methods for testing void setTestValidationState(NodeValidationState::State state, QString const &message) { - NodeValidationState vs; - vs._state = state; - vs._stateMessage = message; - setValidationState(vs); + setValidationState(NodeValidationState(state, message)); } void setTestProcessingStatus(NodeProcessingStatus status) { setNodeProcessingStatus(status); } @@ -64,9 +61,7 @@ TEST_CASE("NodeValidationState basic functionality", "[validation]") SECTION("Validation state can be set to Warning") { - NodeValidationState state; - state._state = NodeValidationState::State::Warning; - state._stateMessage = "Test warning"; + NodeValidationState state(NodeValidationState::State::Warning, "Test warning"); CHECK_FALSE(state.isValid()); CHECK(state.state() == NodeValidationState::State::Warning); @@ -75,9 +70,7 @@ TEST_CASE("NodeValidationState basic functionality", "[validation]") SECTION("Validation state can be set to Error") { - NodeValidationState state; - state._state = NodeValidationState::State::Error; - state._stateMessage = "Test error"; + NodeValidationState state(NodeValidationState::State::Error, "Test error"); CHECK_FALSE(state.isValid()); CHECK(state.state() == NodeValidationState::State::Error); From a9fa64d9160e9dca74604dbc1eb026a9d44dbb08 Mon Sep 17 00:00:00 2001 From: Ioannis Makris Date: Wed, 25 Mar 2026 13:17:51 +0100 Subject: [PATCH 25/26] Split BasicGraphicsScene group handling --- CMakeLists.txt | 1 + src/BasicGraphicsScene.cpp | 401 ------------------------------- src/BasicGraphicsSceneGroups.cpp | 398 ++++++++++++++++++++++++++++++ 3 files changed, 399 insertions(+), 401 deletions(-) create mode 100644 src/BasicGraphicsSceneGroups.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2cd85c33..fb2fdec9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ set(CPP_SOURCE_FILES src/AbstractGraphModel.cpp src/AbstractNodeGeometry.cpp src/BasicGraphicsScene.cpp + src/BasicGraphicsSceneGroups.cpp src/ConnectionGraphicsObject.cpp src/ConnectionState.cpp src/ConnectionStyle.cpp diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index 8cf89dd2..f9c52df6 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -11,91 +11,13 @@ #include "GraphicsView.hpp" #include "NodeDelegateModel.hpp" #include "NodeGraphicsObject.hpp" -#include "SerializationValidation.hpp" #include -#include -#include -#include -#include -#include #include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include #include -#include -#include - -namespace { - -using QtNodes::ConnectionId; -using QtNodes::GroupId; -using QtNodes::InvalidGroupId; -using QtNodes::InvalidNodeId; -using QtNodes::NodeId; - -NodeId jsonValueToNodeId(QJsonValue const &value) -{ - NodeId nodeId = InvalidNodeId; - - if (!QtNodes::detail::read_node_id(value, nodeId)) { - return InvalidNodeId; - } - - return nodeId; -} - -void validate_group_json(QJsonObject const &groupJson) -{ - QString groupName; - if (!QtNodes::detail::read_required_string(groupJson, "name", groupName)) { - throw std::logic_error("Serialized group contains invalid name"); - } - Q_UNUSED(groupName); - - QJsonArray nodesJson; - if (!QtNodes::detail::read_required_array(groupJson, "nodes", nodesJson)) { - throw std::logic_error("Serialized group contains invalid nodes array"); - } - - QJsonArray connectionsJson; - if (!QtNodes::detail::read_required_array(groupJson, "connections", connectionsJson)) { - throw std::logic_error("Serialized group contains invalid connections array"); - } - - for (QJsonValue const &nodeValue : nodesJson) { - if (!nodeValue.isObject()) { - throw std::logic_error("Serialized group contains invalid node entry"); - } - } - - for (QJsonValue const &connectionValue : connectionsJson) { - if (!connectionValue.isObject()) { - throw std::logic_error("Serialized group contains invalid connection entry"); - } - - ConnectionId connId; - if (!QtNodes::tryFromJson(connectionValue.toObject(), connId)) { - throw std::logic_error("Serialized group contains invalid connection id"); - } - } -} - -} // namespace namespace QtNodes { @@ -257,29 +179,6 @@ void BasicGraphicsScene::clearScene() } } -std::vector BasicGraphicsScene::connectionsWithinGroup(GroupId groupID) -{ - if (!_groupingEnabled) - return {}; - - std::vector ret{}; - ret.reserve(_connectionGraphicsObjects.size()); - - for (auto const &connection : _connectionGraphicsObjects) { - auto outNode = nodeGraphicsObject(connection.first.outNodeId); - auto inNode = nodeGraphicsObject(connection.first.inNodeId); - if (outNode && inNode) { - auto group1 = outNode->nodeGroup().lock(); - auto group2 = inNode->nodeGroup().lock(); - if (group1 && group2 && group1->id() == group2->id() && group1->id() == groupID) { - ret.push_back(connection.first); - } - } - } - - return ret; -} - NodeGraphicsObject *BasicGraphicsScene::nodeGraphicsObject(NodeId nodeId) { NodeGraphicsObject *ngo = nullptr; @@ -489,55 +388,6 @@ void BasicGraphicsScene::freezeModelAndConnections(bool isFreeze) } } -std::weak_ptr BasicGraphicsScene::createGroup(std::vector &nodes, - QString groupName, - GroupId groupId) -{ - if (!_groupingEnabled) - return std::weak_ptr(); - - if (nodes.empty()) - return std::weak_ptr(); - - for (auto *node : nodes) { - if (!node->nodeGroup().expired()) - removeNodeFromGroup(node->nodeId()); - } - - if (groupName.isEmpty()) { - groupName = "Group " + QString::number(NodeGroup::groupCount()); - } - - if (groupId == InvalidGroupId) { - groupId = nextGroupId(); - } else { - if (_groups.count(groupId) != 0) { - throw std::runtime_error("Group identifier collision"); - } - - if (groupId >= _nextGroupId && _nextGroupId != InvalidGroupId) { - _nextGroupId = groupId + 1; - } - } - - auto group = std::make_shared(nodes, groupId, groupName, this); - auto ggo = std::make_unique(*this, *group); - - group->setGraphicsObject(std::move(ggo)); - - for (auto &nodePtr : nodes) { - auto node = _nodeGraphicsObjects[nodePtr->nodeId()].get(); - - node->setNodeGroup(group); - } - - std::weak_ptr groupWeakPtr = group; - - _groups[group->id()] = std::move(group); - - return groupWeakPtr; -} - namespace { template std::vector selectedItemsOfType(QGraphicsScene const *scene) @@ -570,42 +420,6 @@ std::vector BasicGraphicsScene::selectedGroups() const return selectedItemsOfType(this); } -void BasicGraphicsScene::addNodeToGroup(NodeId nodeId, GroupId groupId) -{ - if (!_groupingEnabled) - return; - - auto groupIt = _groups.find(groupId); - auto nodeIt = _nodeGraphicsObjects.find(nodeId); - if (groupIt == _groups.end() || nodeIt == _nodeGraphicsObjects.end()) - return; - - auto group = groupIt->second; - auto node = nodeIt->second.get(); - group->addNode(node); - node->setNodeGroup(group); -} - -void BasicGraphicsScene::removeNodeFromGroup(NodeId nodeId) -{ - if (!_groupingEnabled) - return; - - auto nodeIt = _nodeGraphicsObjects.find(nodeId); - if (nodeIt == _nodeGraphicsObjects.end()) - return; - - auto group = nodeIt->second->nodeGroup().lock(); - if (group) { - group->removeNode(nodeIt->second.get()); - if (group->empty()) { - _groups.erase(group->id()); - } - } - nodeIt->second->unsetNodeGroup(); - nodeIt->second->lock(false); -} - std::weak_ptr BasicGraphicsScene::createGroupFromSelection(QString groupName) { if (!_groupingEnabled) @@ -615,118 +429,6 @@ std::weak_ptr BasicGraphicsScene::createGroupFromSelection(Q return createGroup(nodes, groupName); } -NodeGraphicsObject &BasicGraphicsScene::loadNodeToMap(QJsonObject nodeJson, bool keepOriginalId) -{ - NodeId newNodeId = InvalidNodeId; - - if (keepOriginalId) { - newNodeId = jsonValueToNodeId(nodeJson["id"]); - if (newNodeId == InvalidNodeId) { - throw std::logic_error("Invalid node id in serialized node"); - } - } else { - newNodeId = _graphModel.newNodeId(); - nodeJson["id"] = static_cast(newNodeId); - } - - _graphModel.loadNode(nodeJson); - - auto *nodeObject = nodeGraphicsObject(newNodeId); - if (!nodeObject) { - auto graphicsObject = std::make_unique(*this, newNodeId); - nodeObject = graphicsObject.get(); - _nodeGraphicsObjects[newNodeId] = std::move(graphicsObject); - } - - return *nodeObject; -} - -void BasicGraphicsScene::loadConnectionToMap(QJsonObject const &connectionJson, - std::unordered_map const &nodeIdMap) -{ - ConnectionId connId; - if (!tryFromJson(connectionJson, connId)) { - throw std::logic_error("Invalid serialized connection"); - } - - auto const outIt = nodeIdMap.find(connId.outNodeId); - auto const inIt = nodeIdMap.find(connId.inNodeId); - - if (outIt == nodeIdMap.end() || inIt == nodeIdMap.end()) { - throw std::logic_error("Serialized connection references unknown node id"); - } - - ConnectionId remapped{outIt->second, connId.outPortIndex, inIt->second, connId.inPortIndex}; - - if (_graphModel.connectionExists(remapped)) { - return; - } - - if (!_graphModel.connectionPossible(remapped)) { - throw std::logic_error("Serialized connection is not valid for restored nodes"); - } - - _graphModel.addConnection(remapped); -} - -std::pair, std::unordered_map> -BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) -{ - if (!_groupingEnabled) - return {std::weak_ptr(), {}}; - - validate_group_json(groupJson); - - // since the new nodes will have the same IDs as in the file and the connections - // need these old IDs to be restored, we must create new IDs and map them to the - // old ones so the connections are properly restored - std::unordered_map IDsMap{}; - std::unordered_map nodeIdMap{}; - - std::vector group_children{}; - std::vector createdNodeIds{}; - - try { - QJsonArray nodesJson = groupJson["nodes"].toArray(); - for (const QJsonValueRef nodeJson : nodesJson) { - QJsonObject nodeObject = nodeJson.toObject(); - NodeId const oldNodeId = jsonValueToNodeId(nodeObject["id"]); - - NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, false); - NodeId const newNodeId = nodeRef.nodeId(); - - createdNodeIds.push_back(newNodeId); - - if (oldNodeId != InvalidNodeId) { - nodeIdMap.emplace(oldNodeId, newNodeId); - IDsMap.emplace(static_cast(oldNodeId), static_cast(newNodeId)); - } - - group_children.push_back(&nodeRef); - } - - QJsonArray connectionJsonArray = groupJson["connections"].toArray(); - for (auto connection : connectionJsonArray) { - loadConnectionToMap(connection.toObject(), nodeIdMap); - } - - return std::make_pair(createGroup(group_children, groupJson["name"].toString()), IDsMap); - } catch (...) { - for (NodeId const nodeId : createdNodeIds) { - if (_graphModel.nodeExists(nodeId)) { - _graphModel.deleteNode(nodeId); - } - } - - throw; - } -} - -std::unordered_map> const &BasicGraphicsScene::groups() const -{ - return _groups; -} - QMenu *BasicGraphicsScene::createStdMenu(QPointF const scenePos) { Q_UNUSED(scenePos); @@ -807,107 +509,4 @@ QMenu *BasicGraphicsScene::createGroupMenu(QPointF const scenePos, GroupGraphics return menu; } -void BasicGraphicsScene::saveGroupFile(GroupId groupID) -{ - if (!_groupingEnabled) - return; - - QString fileName = QFileDialog::getSaveFileName(nullptr, - tr("Save Node Group"), - QDir::homePath(), - tr("Node Group files (*.group)")); - - if (!fileName.isEmpty()) { - if (!fileName.endsWith("group", Qt::CaseInsensitive)) - fileName += ".group"; - - if (auto groupIt = _groups.find(groupID); groupIt != _groups.end()) { - QFile file(fileName); - if (file.open(QIODevice::WriteOnly)) { - file.write(groupIt->second->saveToFile()); - } else { - qDebug() << "Error saving group file!"; - } - } else { - qDebug() << "Error! Couldn't find group while saving."; - } - } -} - -std::weak_ptr BasicGraphicsScene::loadGroupFile() -{ - if (!_groupingEnabled) - return std::weak_ptr(); - - QString fileName = QFileDialog::getOpenFileName(nullptr, - tr("Open Node Group"), - QDir::currentPath(), - tr("Node Group files (*.group)")); - - if (!QFileInfo::exists(fileName)) - return std::weak_ptr(); - - QFile file(fileName); - - if (!file.open(QIODevice::ReadOnly)) { - qDebug() << "Error loading group file!"; - return std::weak_ptr(); - } - - struct Current_dir_guard - { - QString path; - - ~Current_dir_guard() - { - if (!path.isEmpty()) { - QDir::setCurrent(path); - } - } - }; - - Current_dir_guard currentDirGuard{QDir::currentPath()}; - QDir d = QFileInfo(fileName).absoluteDir(); - QString absolute = d.absolutePath(); - QDir::setCurrent(absolute); - - QByteArray wholeFile = file.readAll(); - - QJsonParseError parseError{}; - QJsonDocument const groupDocument = QJsonDocument::fromJson(wholeFile, &parseError); - if (parseError.error != QJsonParseError::NoError || !groupDocument.isObject()) { - return std::weak_ptr(); - } - - const QJsonObject fileJson = groupDocument.object(); - - try { - return restoreGroup(fileJson).first; - } catch (std::exception const &ex) { - qWarning() << "Failed to load group file:" << ex.what(); - return std::weak_ptr(); - } catch (...) { - qWarning() << "Failed to load group file due to an unknown error"; - return std::weak_ptr(); - } -} - -GroupId BasicGraphicsScene::nextGroupId() -{ - if (_nextGroupId == InvalidGroupId) { - throw std::runtime_error("No available group identifiers"); - } - - while (_groups.count(_nextGroupId) != 0) { - ++_nextGroupId; - if (_nextGroupId == InvalidGroupId) { - throw std::runtime_error("No available group identifiers"); - } - } - - GroupId const newId = _nextGroupId; - ++_nextGroupId; - return newId; -} - } // namespace QtNodes diff --git a/src/BasicGraphicsSceneGroups.cpp b/src/BasicGraphicsSceneGroups.cpp new file mode 100644 index 00000000..0d18e338 --- /dev/null +++ b/src/BasicGraphicsSceneGroups.cpp @@ -0,0 +1,398 @@ +#include "BasicGraphicsScene.hpp" + +#include "ConnectionIdUtils.hpp" +#include "NodeGraphicsObject.hpp" +#include "SerializationValidation.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +using QtNodes::ConnectionId; +using QtNodes::GroupId; +using QtNodes::InvalidGroupId; +using QtNodes::InvalidNodeId; +using QtNodes::NodeId; + +NodeId json_value_to_node_id(QJsonValue const &value) +{ + NodeId nodeId = InvalidNodeId; + + if (!QtNodes::detail::read_node_id(value, nodeId)) { + return InvalidNodeId; + } + + return nodeId; +} + +void validate_group_json(QJsonObject const &groupJson) +{ + QString groupName; + if (!QtNodes::detail::read_required_string(groupJson, "name", groupName)) { + throw std::logic_error("Serialized group contains invalid name"); + } + Q_UNUSED(groupName); + + QJsonArray nodesJson; + if (!QtNodes::detail::read_required_array(groupJson, "nodes", nodesJson)) { + throw std::logic_error("Serialized group contains invalid nodes array"); + } + + QJsonArray connectionsJson; + if (!QtNodes::detail::read_required_array(groupJson, "connections", connectionsJson)) { + throw std::logic_error("Serialized group contains invalid connections array"); + } + + for (QJsonValue const &nodeValue : nodesJson) { + if (!nodeValue.isObject()) { + throw std::logic_error("Serialized group contains invalid node entry"); + } + } + + for (QJsonValue const &connectionValue : connectionsJson) { + if (!connectionValue.isObject()) { + throw std::logic_error("Serialized group contains invalid connection entry"); + } + + ConnectionId connId; + if (!QtNodes::tryFromJson(connectionValue.toObject(), connId)) { + throw std::logic_error("Serialized group contains invalid connection id"); + } + } +} + +} // namespace + +namespace QtNodes { + +std::vector BasicGraphicsScene::connectionsWithinGroup(GroupId groupID) +{ + if (!_groupingEnabled) + return {}; + + std::vector ret{}; + ret.reserve(_connectionGraphicsObjects.size()); + + for (auto const &connection : _connectionGraphicsObjects) { + auto outNode = nodeGraphicsObject(connection.first.outNodeId); + auto inNode = nodeGraphicsObject(connection.first.inNodeId); + if (outNode && inNode) { + auto group1 = outNode->nodeGroup().lock(); + auto group2 = inNode->nodeGroup().lock(); + if (group1 && group2 && group1->id() == group2->id() && group1->id() == groupID) { + ret.push_back(connection.first); + } + } + } + + return ret; +} + +std::weak_ptr BasicGraphicsScene::createGroup(std::vector &nodes, + QString groupName, + GroupId groupId) +{ + if (!_groupingEnabled || nodes.empty()) + return {}; + + for (auto *node : nodes) { + if (!node->nodeGroup().expired()) + removeNodeFromGroup(node->nodeId()); + } + + if (groupName.isEmpty()) { + groupName = "Group " + QString::number(NodeGroup::groupCount()); + } + + if (groupId == InvalidGroupId) { + groupId = nextGroupId(); + } else { + if (_groups.count(groupId) != 0) { + throw std::runtime_error("Group identifier collision"); + } + + if (groupId >= _nextGroupId && _nextGroupId != InvalidGroupId) { + _nextGroupId = groupId + 1; + } + } + + auto group = std::make_shared(nodes, groupId, groupName, this); + auto ggo = std::make_unique(*this, *group); + + group->setGraphicsObject(std::move(ggo)); + + for (auto *nodePtr : nodes) { + auto *node = _nodeGraphicsObjects[nodePtr->nodeId()].get(); + node->setNodeGroup(group); + } + + std::weak_ptr groupWeakPtr = group; + _groups[group->id()] = std::move(group); + return groupWeakPtr; +} + +void BasicGraphicsScene::addNodeToGroup(NodeId nodeId, GroupId groupId) +{ + if (!_groupingEnabled) + return; + + auto groupIt = _groups.find(groupId); + auto nodeIt = _nodeGraphicsObjects.find(nodeId); + if (groupIt == _groups.end() || nodeIt == _nodeGraphicsObjects.end()) + return; + + auto group = groupIt->second; + auto *node = nodeIt->second.get(); + group->addNode(node); + node->setNodeGroup(group); +} + +void BasicGraphicsScene::removeNodeFromGroup(NodeId nodeId) +{ + if (!_groupingEnabled) + return; + + auto nodeIt = _nodeGraphicsObjects.find(nodeId); + if (nodeIt == _nodeGraphicsObjects.end()) + return; + + auto group = nodeIt->second->nodeGroup().lock(); + if (group) { + group->removeNode(nodeIt->second.get()); + if (group->empty()) { + _groups.erase(group->id()); + } + } + nodeIt->second->unsetNodeGroup(); + nodeIt->second->lock(false); +} + +NodeGraphicsObject &BasicGraphicsScene::loadNodeToMap(QJsonObject nodeJson, bool keepOriginalId) +{ + NodeId newNodeId = InvalidNodeId; + + if (keepOriginalId) { + newNodeId = json_value_to_node_id(nodeJson["id"]); + if (newNodeId == InvalidNodeId) { + throw std::logic_error("Invalid node id in serialized node"); + } + } else { + newNodeId = _graphModel.newNodeId(); + nodeJson["id"] = static_cast(newNodeId); + } + + _graphModel.loadNode(nodeJson); + + auto *nodeObject = nodeGraphicsObject(newNodeId); + if (!nodeObject) { + auto graphicsObject = std::make_unique(*this, newNodeId); + nodeObject = graphicsObject.get(); + _nodeGraphicsObjects[newNodeId] = std::move(graphicsObject); + } + + return *nodeObject; +} + +void BasicGraphicsScene::loadConnectionToMap(QJsonObject const &connectionJson, + std::unordered_map const &nodeIdMap) +{ + ConnectionId connId; + if (!tryFromJson(connectionJson, connId)) { + throw std::logic_error("Invalid serialized connection"); + } + + auto const outIt = nodeIdMap.find(connId.outNodeId); + auto const inIt = nodeIdMap.find(connId.inNodeId); + + if (outIt == nodeIdMap.end() || inIt == nodeIdMap.end()) { + throw std::logic_error("Serialized connection references unknown node id"); + } + + ConnectionId remapped{outIt->second, connId.outPortIndex, inIt->second, connId.inPortIndex}; + + if (_graphModel.connectionExists(remapped)) { + return; + } + + if (!_graphModel.connectionPossible(remapped)) { + throw std::logic_error("Serialized connection is not valid for restored nodes"); + } + + _graphModel.addConnection(remapped); +} + +std::pair, std::unordered_map> +BasicGraphicsScene::restoreGroup(QJsonObject const &groupJson) +{ + if (!_groupingEnabled) + return {{}, {}}; + + validate_group_json(groupJson); + + std::unordered_map IDsMap{}; + std::unordered_map nodeIdMap{}; + std::vector groupChildren{}; + std::vector createdNodeIds{}; + + try { + QJsonArray const nodesJson = groupJson["nodes"].toArray(); + for (QJsonValue const &nodeJson : nodesJson) { + QJsonObject nodeObject = nodeJson.toObject(); + NodeId const oldNodeId = json_value_to_node_id(nodeObject["id"]); + + NodeGraphicsObject &nodeRef = loadNodeToMap(nodeObject, false); + NodeId const newNodeId = nodeRef.nodeId(); + + createdNodeIds.push_back(newNodeId); + + if (oldNodeId != InvalidNodeId) { + nodeIdMap.emplace(oldNodeId, newNodeId); + IDsMap.emplace(static_cast(oldNodeId), static_cast(newNodeId)); + } + + groupChildren.push_back(&nodeRef); + } + + QJsonArray const connectionJsonArray = groupJson["connections"].toArray(); + for (QJsonValue const &connection : connectionJsonArray) { + loadConnectionToMap(connection.toObject(), nodeIdMap); + } + + return {createGroup(groupChildren, groupJson["name"].toString()), IDsMap}; + } catch (...) { + for (NodeId const nodeId : createdNodeIds) { + if (_graphModel.nodeExists(nodeId)) { + _graphModel.deleteNode(nodeId); + } + } + + throw; + } +} + +std::unordered_map> const &BasicGraphicsScene::groups() const +{ + return _groups; +} + +void BasicGraphicsScene::saveGroupFile(GroupId groupID) +{ + if (!_groupingEnabled) + return; + + QString fileName = QFileDialog::getSaveFileName(nullptr, + tr("Save Node Group"), + QDir::homePath(), + tr("Node Group files (*.group)")); + + if (fileName.isEmpty()) + return; + + if (!fileName.endsWith("group", Qt::CaseInsensitive)) + fileName += ".group"; + + auto groupIt = _groups.find(groupID); + if (groupIt == _groups.end()) { + qDebug() << "Error! Couldn't find group while saving."; + return; + } + + QFile file(fileName); + if (file.open(QIODevice::WriteOnly)) { + file.write(groupIt->second->saveToFile()); + } else { + qDebug() << "Error saving group file!"; + } +} + +std::weak_ptr BasicGraphicsScene::loadGroupFile() +{ + if (!_groupingEnabled) + return {}; + + QString fileName = QFileDialog::getOpenFileName(nullptr, + tr("Open Node Group"), + QDir::currentPath(), + tr("Node Group files (*.group)")); + + if (!QFileInfo::exists(fileName)) + return {}; + + QFile file(fileName); + + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "Error loading group file!"; + return {}; + } + + struct CurrentDirGuard + { + QString path; + + ~CurrentDirGuard() + { + if (!path.isEmpty()) { + QDir::setCurrent(path); + } + } + }; + + CurrentDirGuard currentDirGuard{QDir::currentPath()}; + QDir const directory = QFileInfo(fileName).absoluteDir(); + QDir::setCurrent(directory.absolutePath()); + + QByteArray const wholeFile = file.readAll(); + + QJsonParseError parseError{}; + QJsonDocument const groupDocument = QJsonDocument::fromJson(wholeFile, &parseError); + if (parseError.error != QJsonParseError::NoError || !groupDocument.isObject()) { + return {}; + } + + try { + return restoreGroup(groupDocument.object()).first; + } catch (std::exception const &ex) { + qWarning() << "Failed to load group file:" << ex.what(); + return {}; + } catch (...) { + qWarning() << "Failed to load group file due to an unknown error"; + return {}; + } +} + +GroupId BasicGraphicsScene::nextGroupId() +{ + if (_nextGroupId == InvalidGroupId) { + throw std::runtime_error("No available group identifiers"); + } + + while (_groups.count(_nextGroupId) != 0) { + ++_nextGroupId; + if (_nextGroupId == InvalidGroupId) { + throw std::runtime_error("No available group identifiers"); + } + } + + GroupId const newId = _nextGroupId; + ++_nextGroupId; + return newId; +} + +} // namespace QtNodes From 1a8f49b6f433873a70a72335fd529e1fc27deedf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 12:33:33 +0000 Subject: [PATCH 26/26] Address remaining review items: Catch2 v3, noexcept, style encapsulation, backward compat - Upgrade Catch2 from v2.13.7 to v3.7.1 via FetchContent; remove test_main.cpp - Add noexcept to trivial getters across NodeState, ConnectionState, ConnectionGraphicsObject, BasicGraphicsScene, ConnectionIdUtils, ConnectionIdIndex - Encapsulate NodeStyle and GraphicsViewStyle public members behind getter methods, matching the pattern already used by ConnectionStyle - Restore backward compatibility for legacy "intNodeId" JSON key in tryFromJson() connection deserialization https://claude.ai/code/session_01PgU81XZwKqXaPr628mVdQj --- external/Catch2/CMakeLists.txt | 19 +-- .../QtNodes/internal/BasicGraphicsScene.hpp | 6 +- .../internal/ConnectionGraphicsObject.hpp | 12 +- .../QtNodes/internal/ConnectionIdIndex.hpp | 4 +- .../QtNodes/internal/ConnectionIdUtils.hpp | 19 ++- include/QtNodes/internal/ConnectionState.hpp | 6 +- .../QtNodes/internal/GraphicsViewStyle.hpp | 11 +- include/QtNodes/internal/NodeState.hpp | 6 +- include/QtNodes/internal/NodeStyle.hpp | 103 ++++++++----- src/AbstractNodeGeometry.cpp | 2 +- src/BasicGraphicsScene.cpp | 4 +- src/ConnectionGraphicsObject.cpp | 4 +- src/ConnectionState.cpp | 6 +- src/DefaultNodeGeometryBase.cpp | 2 +- src/DefaultNodePainter.cpp | 48 +++--- src/GraphicsView.cpp | 6 +- src/GraphicsViewStyle.cpp | 16 +- src/NodeDelegateModel.cpp | 32 ++-- src/NodeGraphicsObject.cpp | 2 +- src/NodeState.cpp | 2 +- src/NodeStyle.cpp | 137 +++++++++++------- test/CMakeLists.txt | 3 +- test/src/TestAbstractGraphModel.cpp | 2 +- test/src/TestAbstractGraphModelSignals.cpp | 2 +- test/src/TestBasicGraphicsScene.cpp | 4 +- test/src/TestConnectionId.cpp | 2 +- test/src/TestCopyPaste.cpp | 2 +- test/src/TestCustomPainters.cpp | 2 +- test/src/TestDataFlow.cpp | 2 +- test/src/TestDataFlowGraphModel.cpp | 2 +- test/src/TestLoopDetection.cpp | 2 +- test/src/TestNodeDelegateModelRegistry.cpp | 2 +- test/src/TestNodeGroup.cpp | 2 +- test/src/TestNodeValidation.cpp | 2 +- test/src/TestSerialization.cpp | 2 +- test/src/TestUIInteraction.cpp | 2 +- test/src/TestUndoCommands.cpp | 2 +- test/src/TestZoomFeatures.cpp | 2 +- test/test_main.cpp | 2 - 39 files changed, 282 insertions(+), 204 deletions(-) delete mode 100644 test/test_main.cpp diff --git a/external/Catch2/CMakeLists.txt b/external/Catch2/CMakeLists.txt index 4cc7e8c8..a85e23f5 100644 --- a/external/Catch2/CMakeLists.txt +++ b/external/Catch2/CMakeLists.txt @@ -1,13 +1,10 @@ -if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/single_include/catch2/catch.hpp") - file(DOWNLOAD https://raw.githubusercontent.com/catchorg/Catch2/v2.13.7/single_include/catch2/catch.hpp - "${CMAKE_CURRENT_BINARY_DIR}/single_include/catch2/catch.hpp" - EXPECTED_HASH SHA256=ea379c4a3cb5799027b1eb451163dff065a3d641aaba23bf4e24ee6b536bd9bc - ) -endif() +include(FetchContent) -add_library(Catch2 INTERFACE) -add_library(Catch2::Catch2 ALIAS Catch2) -target_include_directories(Catch2 - INTERFACE - "${CMAKE_CURRENT_BINARY_DIR}/single_include" +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.7.1 + GIT_SHALLOW TRUE ) + +FetchContent_MakeAvailable(Catch2) diff --git a/include/QtNodes/internal/BasicGraphicsScene.hpp b/include/QtNodes/internal/BasicGraphicsScene.hpp index 129c9ae9..c3d1b207 100644 --- a/include/QtNodes/internal/BasicGraphicsScene.hpp +++ b/include/QtNodes/internal/BasicGraphicsScene.hpp @@ -44,9 +44,9 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene public: /// @returns associated AbstractGraphModel. - AbstractGraphModel const &graphModel() const; + AbstractGraphModel const &graphModel() const noexcept; - AbstractGraphModel &graphModel(); + AbstractGraphModel &graphModel() noexcept; AbstractNodeGeometry const &nodeGeometry() const; @@ -190,7 +190,7 @@ class NODE_EDITOR_PUBLIC BasicGraphicsScene : public QGraphicsScene */ ConnectionGraphicsObject *connectionGraphicsObject(ConnectionId connectionId); - Qt::Orientation orientation() const { return _orientation; } + Qt::Orientation orientation() const noexcept { return _orientation; } void setOrientation(Qt::Orientation const orientation); diff --git a/include/QtNodes/internal/ConnectionGraphicsObject.hpp b/include/QtNodes/internal/ConnectionGraphicsObject.hpp index 7371391a..f4a49164 100644 --- a/include/QtNodes/internal/ConnectionGraphicsObject.hpp +++ b/include/QtNodes/internal/ConnectionGraphicsObject.hpp @@ -25,7 +25,7 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject // Needed for qgraphicsitem_cast enum { Type = UserType + 2 }; - int type() const override { return Type; } + int type() const noexcept override { return Type; } public: ConnectionGraphicsObject(BasicGraphicsScene &scene, ConnectionId const connectionId); @@ -37,7 +37,7 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject BasicGraphicsScene *nodeScene() const; - ConnectionId const &connectionId() const; + ConnectionId const &connectionId() const noexcept; QRectF boundingRect() const override; @@ -45,9 +45,9 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject QPointF const &endPoint(PortType portType) const; - QPointF out() const { return _out; } + QPointF out() const noexcept { return _out; } - QPointF in() const { return _in; } + QPointF in() const noexcept { return _in; } std::pair pointsC1C2() const; @@ -57,11 +57,11 @@ class NODE_EDITOR_PUBLIC ConnectionGraphicsObject : public QGraphicsObject /// Cached painter stroke for hit testing, rebuilt only when endpoints change. QPainterPath const &cachedStrokePath() const; - int cachedSamplePointCount() const { return k_path_sample_count; } + int cachedSamplePointCount() const noexcept { return k_path_sample_count; } QPointF const &cachedSamplePoint(int index) const; - QPointF const &cachedMidPoint() const; + QPointF const &cachedMidPoint() const noexcept; void setEndPoint(PortType portType, QPointF const &point); diff --git a/include/QtNodes/internal/ConnectionIdIndex.hpp b/include/QtNodes/internal/ConnectionIdIndex.hpp index 24f8a739..cb01ed8b 100644 --- a/include/QtNodes/internal/ConnectionIdIndex.hpp +++ b/include/QtNodes/internal/ConnectionIdIndex.hpp @@ -16,7 +16,7 @@ class ConnectionIdIndex public: ConnectionSet const &connectivity() const noexcept { return _connectivity; } - ConnectionSet const &allConnectionIds(NodeId const nodeId) const + ConnectionSet const &allConnectionIds(NodeId const nodeId) const noexcept { auto it = _nodeConnections.find(nodeId); if (it == _nodeConnections.end()) { @@ -26,7 +26,7 @@ class ConnectionIdIndex return it->second; } - ConnectionSet const &connections(NodeId nodeId, PortType portType, PortIndex portIndex) const + ConnectionSet const &connections(NodeId nodeId, PortType portType, PortIndex portIndex) const noexcept { if (portType == PortType::None) { return emptyConnections(); diff --git a/include/QtNodes/internal/ConnectionIdUtils.hpp b/include/QtNodes/internal/ConnectionIdUtils.hpp index a9e14cba..7a606419 100644 --- a/include/QtNodes/internal/ConnectionIdUtils.hpp +++ b/include/QtNodes/internal/ConnectionIdUtils.hpp @@ -11,7 +11,7 @@ namespace QtNodes { -inline NodeId getNodeId(PortType portType, ConnectionId const &connectionId) +inline NodeId getNodeId(PortType portType, ConnectionId const &connectionId) noexcept { NodeId id = InvalidNodeId; @@ -24,7 +24,7 @@ inline NodeId getNodeId(PortType portType, ConnectionId const &connectionId) return id; } -inline PortIndex getPortIndex(PortType portType, ConnectionId const &connectionId) +inline PortIndex getPortIndex(PortType portType, ConnectionId const &connectionId) noexcept { PortIndex index = InvalidPortIndex; @@ -37,7 +37,7 @@ inline PortIndex getPortIndex(PortType portType, ConnectionId const &connectionI return index; } -inline PortType oppositePort(PortType port) +inline PortType oppositePort(PortType port) noexcept { switch (port) { case PortType::In: @@ -49,12 +49,12 @@ inline PortType oppositePort(PortType port) } } -inline bool isPortIndexValid(PortIndex index) +inline bool isPortIndexValid(PortIndex index) noexcept { return index != InvalidPortIndex; } -inline bool isPortTypeValid(PortType portType) +inline bool isPortTypeValid(PortType portType) noexcept { return portType != PortType::None; } @@ -138,11 +138,16 @@ inline bool tryFromJson(QJsonObject const &connJson, ConnectionId &connId) if (!detail::read_node_id(connJson["outNodeId"], outNodeId) || !detail::read_port_index(connJson["outPortIndex"], outPortIndex) - || !detail::read_node_id(connJson["inNodeId"], inNodeId) || !detail::read_port_index(connJson["inPortIndex"], inPortIndex)) { return false; } + // Support both "inNodeId" (correct) and "intNodeId" (legacy typo) + if (!detail::read_node_id(connJson["inNodeId"], inNodeId) + && !detail::read_node_id(connJson["intNodeId"], inNodeId)) { + return false; + } + connId = ConnectionId{outNodeId, outPortIndex, inNodeId, inPortIndex}; return true; } @@ -157,7 +162,7 @@ inline ConnectionId fromJson(QJsonObject const &connJson) return connId; } -inline NodeRole portCountRole(PortType portType) +inline NodeRole portCountRole(PortType portType) noexcept { return (portType == PortType::Out) ? NodeRole::OutPortCount : NodeRole::InPortCount; } diff --git a/include/QtNodes/internal/ConnectionState.hpp b/include/QtNodes/internal/ConnectionState.hpp index 6d1a5fcb..c4d8f7bb 100644 --- a/include/QtNodes/internal/ConnectionState.hpp +++ b/include/QtNodes/internal/ConnectionState.hpp @@ -36,8 +36,8 @@ class NODE_EDITOR_PUBLIC ConnectionState PortType requiredPort() const; bool requiresPort() const; - bool hovered() const; - void setHovered(bool hovered); + bool hovered() const noexcept; + void setHovered(bool hovered) noexcept; bool frozen() const { return _frozen; } void setFrozen(bool frozen) { _frozen = frozen; } @@ -46,7 +46,7 @@ class NODE_EDITOR_PUBLIC ConnectionState /// Caches NodeId for further interaction. void setLastHoveredNode(NodeId const nodeId); - NodeId lastHoveredNode() const; + NodeId lastHoveredNode() const noexcept; void resetLastHoveredNode(); diff --git a/include/QtNodes/internal/GraphicsViewStyle.hpp b/include/QtNodes/internal/GraphicsViewStyle.hpp index 32f51b9e..e213ca6f 100644 --- a/include/QtNodes/internal/GraphicsViewStyle.hpp +++ b/include/QtNodes/internal/GraphicsViewStyle.hpp @@ -25,8 +25,13 @@ class NODE_EDITOR_PUBLIC GraphicsViewStyle : public Style QJsonObject toJson() const override; public: - QColor BackgroundColor; - QColor FineGridColor; - QColor CoarseGridColor; + QColor backgroundColor() const; + QColor fineGridColor() const; + QColor coarseGridColor() const; + +private: + QColor _BackgroundColor; + QColor _FineGridColor; + QColor _CoarseGridColor; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/NodeState.hpp b/include/QtNodes/internal/NodeState.hpp index 82394430..f1af78f9 100644 --- a/include/QtNodes/internal/NodeState.hpp +++ b/include/QtNodes/internal/NodeState.hpp @@ -23,13 +23,13 @@ class NODE_EDITOR_PUBLIC NodeState NodeState(NodeGraphicsObject &ngo); public: - bool hovered() const { return _hovered; } + bool hovered() const noexcept { return _hovered; } - void setHovered(bool hovered = true) { _hovered = hovered; } + void setHovered(bool hovered = true) noexcept { _hovered = hovered; } void setResizing(bool resizing); - bool resizing() const; + bool resizing() const noexcept; ConnectionGraphicsObject const *connectionForReaction() const; diff --git a/include/QtNodes/internal/NodeStyle.hpp b/include/QtNodes/internal/NodeStyle.hpp index aa26ef83..098bedd6 100644 --- a/include/QtNodes/internal/NodeStyle.hpp +++ b/include/QtNodes/internal/NodeStyle.hpp @@ -53,39 +53,74 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style QColor backgroundColor() const; public: - QColor NormalBoundaryColor; - QColor SelectedBoundaryColor; - QColor GradientColor0; - QColor GradientColor1; - QColor GradientColor2; - QColor GradientColor3; - QColor ShadowColor; - bool ShadowEnabled{false}; - QColor FontColor; - QColor FontColorFaded; - - QColor ConnectionPointColor; - QColor FilledConnectionPointColor; - - QColor WarningColor; - QColor ErrorColor; - QColor ToolTipIconColor; - - float PenWidth{0.0f}; - float HoveredPenWidth{0.0f}; - - float ConnectionPointDiameter{0.0f}; - - float Opacity{0.0f}; - - // Status icons - initialized in constructor after Q_INIT_RESOURCE - QIcon statusUpdated; - QIcon statusProcessing; - QIcon statusPending; - QIcon statusInvalid; - QIcon statusEmpty; - QIcon statusPartial; - - ProcessingIconStyle processingIconStyle{}; + QColor normalBoundaryColor() const; + QColor selectedBoundaryColor() const; + QColor gradientColor0() const; + QColor gradientColor1() const; + QColor gradientColor2() const; + QColor gradientColor3() const; + QColor shadowColor() const; + bool shadowEnabled() const; + QColor fontColor() const; + QColor fontColorFaded() const; + QColor connectionPointColor() const; + QColor filledConnectionPointColor() const; + QColor warningColor() const; + QColor errorColor() const; + QColor toolTipIconColor() const; + float penWidth() const; + float hoveredPenWidth() const; + float connectionPointDiameter() const; + float opacity() const; + QIcon const &statusUpdated() const; + QIcon const &statusProcessing() const; + QIcon const &statusPending() const; + QIcon const &statusInvalid() const; + QIcon const &statusEmpty() const; + QIcon const &statusPartial() const; + ProcessingIconStyle const &processingIconStyle() const; + + void setStatusUpdated(QIcon const &icon); + void setStatusProcessing(QIcon const &icon); + void setStatusPending(QIcon const &icon); + void setStatusInvalid(QIcon const &icon); + void setStatusEmpty(QIcon const &icon); + void setStatusPartial(QIcon const &icon); + void setProcessingIconStyle(ProcessingIconStyle const &style); + +private: + QColor _NormalBoundaryColor; + QColor _SelectedBoundaryColor; + QColor _GradientColor0; + QColor _GradientColor1; + QColor _GradientColor2; + QColor _GradientColor3; + QColor _ShadowColor; + bool _ShadowEnabled{false}; + QColor _FontColor; + QColor _FontColorFaded; + + QColor _ConnectionPointColor; + QColor _FilledConnectionPointColor; + + QColor _WarningColor; + QColor _ErrorColor; + QColor _ToolTipIconColor; + + float _PenWidth{0.0f}; + float _HoveredPenWidth{0.0f}; + + float _ConnectionPointDiameter{0.0f}; + + float _Opacity{0.0f}; + + QIcon _statusUpdated; + QIcon _statusProcessing; + QIcon _statusPending; + QIcon _statusInvalid; + QIcon _statusEmpty; + QIcon _statusPartial; + + ProcessingIconStyle _processingIconStyle{}; }; } // namespace QtNodes diff --git a/src/AbstractNodeGeometry.cpp b/src/AbstractNodeGeometry.cpp index 0b20bccc..12629a04 100644 --- a/src/AbstractNodeGeometry.cpp +++ b/src/AbstractNodeGeometry.cpp @@ -35,7 +35,7 @@ PortIndex AbstractNodeGeometry::checkPortHit(NodeId const nodeId, if (portType == PortType::None) return result; - double const tolerance = 2.0 * nodeStyle.ConnectionPointDiameter; + double const tolerance = 2.0 * nodeStyle.connectionPointDiameter(); size_t const n = _graphModel.nodeData(nodeId, portCountRole(portType)); diff --git a/src/BasicGraphicsScene.cpp b/src/BasicGraphicsScene.cpp index f9c52df6..1a7d776e 100644 --- a/src/BasicGraphicsScene.cpp +++ b/src/BasicGraphicsScene.cpp @@ -73,12 +73,12 @@ BasicGraphicsScene::BasicGraphicsScene(AbstractGraphModel &graphModel, QObject * BasicGraphicsScene::~BasicGraphicsScene() = default; -AbstractGraphModel const &BasicGraphicsScene::graphModel() const +AbstractGraphModel const &BasicGraphicsScene::graphModel() const noexcept { return _graphModel; } -AbstractGraphModel &BasicGraphicsScene::graphModel() +AbstractGraphModel &BasicGraphicsScene::graphModel() noexcept { return _graphModel; } diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp index fc6b8008..9cee2cfe 100644 --- a/src/ConnectionGraphicsObject.cpp +++ b/src/ConnectionGraphicsObject.cpp @@ -93,7 +93,7 @@ BasicGraphicsScene *ConnectionGraphicsObject::nodeScene() const return sceneObject; } -ConnectionId const &ConnectionGraphicsObject::connectionId() const +ConnectionId const &ConnectionGraphicsObject::connectionId() const noexcept { return _connectionId; } @@ -169,7 +169,7 @@ QPointF const &ConnectionGraphicsObject::cachedSamplePoint(int index) const return _cachedSamplePoints[index]; } -QPointF const &ConnectionGraphicsObject::cachedMidPoint() const +QPointF const &ConnectionGraphicsObject::cachedMidPoint() const noexcept { rebuildCachedGeometry(); return _cachedMidPoint; diff --git a/src/ConnectionState.cpp b/src/ConnectionState.cpp index b2a9d90b..aa8fa4eb 100644 --- a/src/ConnectionState.cpp +++ b/src/ConnectionState.cpp @@ -30,12 +30,12 @@ bool ConnectionState::requiresPort() const return id.outNodeId == InvalidNodeId || id.inNodeId == InvalidNodeId; } -bool ConnectionState::hovered() const +bool ConnectionState::hovered() const noexcept { return _hovered; } -void ConnectionState::setHovered(bool hovered) +void ConnectionState::setHovered(bool hovered) noexcept { _hovered = hovered; } @@ -45,7 +45,7 @@ void ConnectionState::setLastHoveredNode(NodeId const nodeId) _lastHoveredNode = nodeId; } -NodeId ConnectionState::lastHoveredNode() const +NodeId ConnectionState::lastHoveredNode() const noexcept { return _lastHoveredNode; } diff --git a/src/DefaultNodeGeometryBase.cpp b/src/DefaultNodeGeometryBase.cpp index 747c5adc..bd39e480 100644 --- a/src/DefaultNodeGeometryBase.cpp +++ b/src/DefaultNodeGeometryBase.cpp @@ -28,7 +28,7 @@ QRectF DefaultNodeGeometryBase::boundingRect(NodeId const nodeId) const QSize s = size(nodeId); std::optional fallback_style; NodeStyle const &style = node_rendering::resolved_node_style(_graphModel, nodeId, fallback_style); - QMarginsF const margins = node_rendering::node_visual_margins(style.ShadowEnabled); + QMarginsF const margins = node_rendering::node_visual_margins(style.shadowEnabled()); QRectF r(QPointF(0, 0), s); diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index f1add82b..1078ca6a 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -411,19 +411,19 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo QVariant var = model.nodeData(nodeId, NodeRole::ValidationState); bool invalid = false; - QColor color = ngo.isSelected() ? nodeStyle.SelectedBoundaryColor - : nodeStyle.NormalBoundaryColor; + QColor color = ngo.isSelected() ? nodeStyle.selectedBoundaryColor() + : nodeStyle.normalBoundaryColor(); if (var.canConvert()) { auto state = var.value(); switch (state.state()) { case NodeValidationState::State::Error: { invalid = true; - color = nodeStyle.ErrorColor; + color = nodeStyle.errorColor(); } break; case NodeValidationState::State::Warning: { invalid = true; - color = nodeStyle.WarningColor; + color = nodeStyle.warningColor(); } break; default: break; @@ -438,15 +438,15 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo // and stretched to fit the node. One atlas per (color, DPR), size- // independent. Much faster than QGraphicsDropShadowEffect and smoother // than stacked translucent rounded rects. - if (nodeStyle.ShadowEnabled) { - draw_nine_slice_shadow(painter, nodeStyle.ShadowColor, boundary); + if (nodeStyle.shadowEnabled()) { + draw_nine_slice_shadow(painter, nodeStyle.shadowColor(), boundary); } if (ngo.nodeState().hovered()) { - painter->setPen(QPen(color, nodeStyle.HoveredPenWidth)); + painter->setPen(QPen(color, nodeStyle.hoveredPenWidth())); } else { - painter->setPen(QPen(color, nodeStyle.PenWidth)); + painter->setPen(QPen(color, nodeStyle.penWidth())); } if (invalid) { @@ -454,10 +454,10 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo } else { QLinearGradient gradient(QPointF(0.0, 0.0), QPointF(2.0, size.height())); - gradient.setColorAt(0.0, nodeStyle.GradientColor0); - gradient.setColorAt(0.10, nodeStyle.GradientColor1); - gradient.setColorAt(0.90, nodeStyle.GradientColor2); - gradient.setColorAt(1.0, nodeStyle.GradientColor3); + gradient.setColorAt(0.0, nodeStyle.gradientColor0()); + gradient.setColorAt(0.10, nodeStyle.gradientColor1()); + gradient.setColorAt(0.90, nodeStyle.gradientColor2()); + gradient.setColorAt(1.0, nodeStyle.gradientColor3()); painter->setBrush(gradient); } @@ -472,7 +472,7 @@ void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObj auto const &connectionStyle = StyleCollection::connectionStyle(); - float diameter = nodeStyle.ConnectionPointDiameter; + float diameter = nodeStyle.connectionPointDiameter(); auto reducedDiameter = diameter * 0.6; for (PortType portType : {PortType::Out, PortType::In}) { @@ -517,7 +517,7 @@ void DefaultNodePainter::drawConnectionPoints(QPainter *painter, NodeGraphicsObj if (connectionStyle.useDataDefinedColors()) { painter->setBrush(connectionStyle.normalColor(dataType.id)); } else { - painter->setBrush(nodeStyle.ConnectionPointColor); + painter->setBrush(nodeStyle.connectionPointColor()); } painter->drawEllipse(p, reducedDiameter * r, reducedDiameter * r); @@ -535,7 +535,7 @@ void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraph NodeId const nodeId = ngo.nodeId(); AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); - auto diameter = nodeStyle.ConnectionPointDiameter; + auto diameter = nodeStyle.connectionPointDiameter(); for (PortType portType : {PortType::Out, PortType::In}) { size_t const n = model.nodeData(nodeId, portCountRole(portType)).toUInt(); @@ -556,8 +556,8 @@ void DefaultNodePainter::drawFilledConnectionPoints(QPainter *painter, NodeGraph painter->setPen(c); painter->setBrush(c); } else { - painter->setPen(nodeStyle.FilledConnectionPointColor); - painter->setBrush(nodeStyle.FilledConnectionPointColor); + painter->setPen(nodeStyle.filledConnectionPointColor()); + painter->setBrush(nodeStyle.filledConnectionPointColor()); } painter->drawEllipse(p, diameter * 0.4, diameter * 0.4); @@ -589,7 +589,7 @@ void DefaultNodePainter::drawNodeCaption(QPainter *painter, NodeGraphicsObject & QPointF position = geometry.captionPosition(nodeId); painter->setRenderHint(QPainter::TextAntialiasing, true); - draw_text(painter, view, position, name, nodeStyle.FontColor, f); + draw_text(painter, view, position, name, nodeStyle.fontColor(), f); f.setBold(false); painter->setFont(f); @@ -612,9 +612,9 @@ void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject & QPointF p = geometry.portTextPosition(nodeId, portType, portIndex); if (connected.empty()) - painter->setPen(nodeStyle.FontColorFaded); + painter->setPen(nodeStyle.fontColorFaded()); else - painter->setPen(nodeStyle.FontColor); + painter->setPen(nodeStyle.fontColor()); QString s; @@ -626,8 +626,8 @@ void DefaultNodePainter::drawEntryLabels(QPainter *painter, NodeGraphicsObject & s = portData.value().name; } - QColor const textColor = connected.empty() ? nodeStyle.FontColorFaded - : nodeStyle.FontColor; + QColor const textColor = connected.empty() ? nodeStyle.fontColorFaded() + : nodeStyle.fontColor(); draw_text(painter, view, p, s, textColor, painter->font()); } } @@ -713,8 +713,8 @@ void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObjec QSize const iconSize(16, 16); - QColor color = (state.state() == NodeValidationState::State::Error) ? nodeStyle.ErrorColor - : nodeStyle.WarningColor; + QColor color = (state.state() == NodeValidationState::State::Error) ? nodeStyle.errorColor() + : nodeStyle.warningColor(); qreal const dpr = painter->device() ? painter->device()->devicePixelRatioF() : 1.0; diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index 89649d7f..54936edd 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -156,7 +156,7 @@ GraphicsView::GraphicsView(QWidget *parent) auto const &flowViewStyle = StyleCollection::flowViewStyle(); - setBackgroundBrush(flowViewStyle.BackgroundColor); + setBackgroundBrush(flowViewStyle.backgroundColor()); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -784,13 +784,13 @@ void GraphicsView::drawBackground(QPainter *painter, const QRectF &r) auto const &flowViewStyle = StyleCollection::flowViewStyle(); - QPen pfine(flowViewStyle.FineGridColor, 1.0); + QPen pfine(flowViewStyle.fineGridColor(), 1.0); pfine.setCosmetic(crisp_grid); painter->setPen(pfine); drawGrid(15); - QPen p(flowViewStyle.CoarseGridColor, 1.0); + QPen p(flowViewStyle.coarseGridColor(), 1.0); p.setCosmetic(crisp_grid); painter->setPen(p); diff --git a/src/GraphicsViewStyle.cpp b/src/GraphicsViewStyle.cpp index c09645f3..00ecf7ee 100644 --- a/src/GraphicsViewStyle.cpp +++ b/src/GraphicsViewStyle.cpp @@ -38,21 +38,25 @@ void GraphicsViewStyle::loadJson(QJsonObject const &json) { QJsonObject obj = json["GraphicsViewStyle"].toObject(); - readColor(obj, "BackgroundColor", BackgroundColor); - readColor(obj, "FineGridColor", FineGridColor); - readColor(obj, "CoarseGridColor", CoarseGridColor); + readColor(obj, "BackgroundColor", _BackgroundColor); + readColor(obj, "FineGridColor", _FineGridColor); + readColor(obj, "CoarseGridColor", _CoarseGridColor); } QJsonObject GraphicsViewStyle::toJson() const { QJsonObject obj; - writeColor(obj, "BackgroundColor", BackgroundColor); - writeColor(obj, "FineGridColor", FineGridColor); - writeColor(obj, "CoarseGridColor", CoarseGridColor); + writeColor(obj, "BackgroundColor", _BackgroundColor); + writeColor(obj, "FineGridColor", _FineGridColor); + writeColor(obj, "CoarseGridColor", _CoarseGridColor); QJsonObject root; root["GraphicsViewStyle"] = obj; return root; } + +QColor GraphicsViewStyle::backgroundColor() const { return _BackgroundColor; } +QColor GraphicsViewStyle::fineGridColor() const { return _FineGridColor; } +QColor GraphicsViewStyle::coarseGridColor() const { return _CoarseGridColor; } diff --git a/src/NodeDelegateModel.cpp b/src/NodeDelegateModel.cpp index 12b55695..b0f19b56 100644 --- a/src/NodeDelegateModel.cpp +++ b/src/NodeDelegateModel.cpp @@ -11,22 +11,22 @@ QIcon const &status_icon(NodeStyle const &style, NodeProcessingStatus status) { switch (status) { case NodeProcessingStatus::Updated: - return style.statusUpdated; + return style.statusUpdated(); case NodeProcessingStatus::Processing: - return style.statusProcessing; + return style.statusProcessing(); case NodeProcessingStatus::Pending: - return style.statusPending; + return style.statusPending(); case NodeProcessingStatus::Empty: - return style.statusEmpty; + return style.statusEmpty(); case NodeProcessingStatus::Failed: - return style.statusInvalid; + return style.statusInvalid(); case NodeProcessingStatus::Partial: - return style.statusPartial; + return style.statusPartial(); case NodeProcessingStatus::NoStatus: break; } - return style.statusEmpty; + return style.statusEmpty(); } } // namespace @@ -89,7 +89,7 @@ QImage NodeDelegateModel::processingStatusImage(qreal dpr) const { std::lock_guard lock(_processingStatusIconMutex); - int const resolution = _nodeStyle.processingIconStyle._resolution; + int const resolution = _nodeStyle.processingIconStyle()._resolution; if (_processingStatus == NodeProcessingStatus::NoStatus) { return {}; @@ -117,7 +117,7 @@ QImage NodeDelegateModel::processingStatusImage(qreal dpr) const ProcessingIconStyle NodeDelegateModel::processingIconStyle() const { std::lock_guard lock(_processingStatusIconMutex); - return _nodeStyle.processingIconStyle; + return _nodeStyle.processingIconStyle(); } void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap &pixmap) @@ -128,22 +128,22 @@ void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap case NodeProcessingStatus::NoStatus: break; case NodeProcessingStatus::Updated: - _nodeStyle.statusUpdated = QIcon(pixmap); + _nodeStyle.setStatusUpdated(QIcon(pixmap)); break; case NodeProcessingStatus::Processing: - _nodeStyle.statusProcessing = QIcon(pixmap); + _nodeStyle.setStatusProcessing(QIcon(pixmap)); break; case NodeProcessingStatus::Pending: - _nodeStyle.statusPending = QIcon(pixmap); + _nodeStyle.setStatusPending(QIcon(pixmap)); break; case NodeProcessingStatus::Empty: - _nodeStyle.statusEmpty = QIcon(pixmap); + _nodeStyle.setStatusEmpty(QIcon(pixmap)); break; case NodeProcessingStatus::Failed: - _nodeStyle.statusInvalid = QIcon(pixmap); + _nodeStyle.setStatusInvalid(QIcon(pixmap)); break; case NodeProcessingStatus::Partial: - _nodeStyle.statusPartial = QIcon(pixmap); + _nodeStyle.setStatusPartial(QIcon(pixmap)); break; } @@ -153,7 +153,7 @@ void NodeDelegateModel::setStatusIcon(NodeProcessingStatus status, const QPixmap void NodeDelegateModel::setStatusIconStyle(const ProcessingIconStyle &style) { std::lock_guard lock(_processingStatusIconMutex); - _nodeStyle.processingIconStyle = style; + _nodeStyle.setProcessingIconStyle(style); _processingStatusIconDirty = true; } diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index 98da8aa1..e441586a 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -98,7 +98,7 @@ NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) _nodeId, fallback_style); - setOpacity(nodeStyle.Opacity); + setOpacity(nodeStyle.opacity()); setAcceptHoverEvents(true); diff --git a/src/NodeState.cpp b/src/NodeState.cpp index f6ed43e1..b614f1d5 100644 --- a/src/NodeState.cpp +++ b/src/NodeState.cpp @@ -19,7 +19,7 @@ void NodeState::setResizing(bool resizing) _resizing = resizing; } -bool NodeState::resizing() const +bool NodeState::resizing() const noexcept { return _resizing; } diff --git a/src/NodeStyle.cpp b/src/NodeStyle.cpp index 9702373e..c9f33b7d 100644 --- a/src/NodeStyle.cpp +++ b/src/NodeStyle.cpp @@ -21,12 +21,12 @@ NodeStyle::NodeStyle() initResources(); // Initialize status icons after resources are loaded - statusUpdated = QIcon(":/status_icons/updated.svg"); - statusProcessing = QIcon(":/status_icons/processing.svg"); - statusPending = QIcon(":/status_icons/pending.svg"); - statusInvalid = QIcon(":/status_icons/failed.svg"); - statusEmpty = QIcon(":/status_icons/empty.svg"); - statusPartial = QIcon(":/status_icons/partial.svg"); + _statusUpdated = QIcon(":/status_icons/updated.svg"); + _statusProcessing = QIcon(":/status_icons/processing.svg"); + _statusPending = QIcon(":/status_icons/pending.svg"); + _statusInvalid = QIcon(":/status_icons/failed.svg"); + _statusEmpty = QIcon(":/status_icons/empty.svg"); + _statusPartial = QIcon(":/status_icons/partial.svg"); // This configuration is stored inside the compiled unit and is loaded statically loadJsonFile(":DefaultStyle.json"); @@ -53,52 +53,52 @@ void NodeStyle::loadJson(QJsonObject const &json) { QJsonObject obj = json["NodeStyle"].toObject(); - readColor(obj, "NormalBoundaryColor", NormalBoundaryColor); - readColor(obj, "SelectedBoundaryColor", SelectedBoundaryColor); - readColor(obj, "GradientColor0", GradientColor0); - readColor(obj, "GradientColor1", GradientColor1); - readColor(obj, "GradientColor2", GradientColor2); - readColor(obj, "GradientColor3", GradientColor3); - readColor(obj, "ShadowColor", ShadowColor); - readBool(obj, "ShadowEnabled", ShadowEnabled); - readColor(obj, "FontColor", FontColor); - readColor(obj, "FontColorFaded", FontColorFaded); - readColor(obj, "ConnectionPointColor", ConnectionPointColor); - readColor(obj, "FilledConnectionPointColor", FilledConnectionPointColor); - readColor(obj, "WarningColor", WarningColor); - readColor(obj, "ErrorColor", ErrorColor); - - readFloat(obj, "PenWidth", PenWidth); - readFloat(obj, "HoveredPenWidth", HoveredPenWidth); - readFloat(obj, "ConnectionPointDiameter", ConnectionPointDiameter); - - readFloat(obj, "Opacity", Opacity); + readColor(obj, "NormalBoundaryColor", _NormalBoundaryColor); + readColor(obj, "SelectedBoundaryColor", _SelectedBoundaryColor); + readColor(obj, "GradientColor0", _GradientColor0); + readColor(obj, "GradientColor1", _GradientColor1); + readColor(obj, "GradientColor2", _GradientColor2); + readColor(obj, "GradientColor3", _GradientColor3); + readColor(obj, "ShadowColor", _ShadowColor); + readBool(obj, "ShadowEnabled", _ShadowEnabled); + readColor(obj, "FontColor", _FontColor); + readColor(obj, "FontColorFaded", _FontColorFaded); + readColor(obj, "ConnectionPointColor", _ConnectionPointColor); + readColor(obj, "FilledConnectionPointColor", _FilledConnectionPointColor); + readColor(obj, "WarningColor", _WarningColor); + readColor(obj, "ErrorColor", _ErrorColor); + + readFloat(obj, "PenWidth", _PenWidth); + readFloat(obj, "HoveredPenWidth", _HoveredPenWidth); + readFloat(obj, "ConnectionPointDiameter", _ConnectionPointDiameter); + + readFloat(obj, "Opacity", _Opacity); } QJsonObject NodeStyle::toJson() const { QJsonObject obj; - writeColor(obj, "NormalBoundaryColor", NormalBoundaryColor); - writeColor(obj, "SelectedBoundaryColor", SelectedBoundaryColor); - writeColor(obj, "GradientColor0", GradientColor0); - writeColor(obj, "GradientColor1", GradientColor1); - writeColor(obj, "GradientColor2", GradientColor2); - writeColor(obj, "GradientColor3", GradientColor3); - writeColor(obj, "ShadowColor", ShadowColor); - writeBool(obj, "ShadowEnabled", ShadowEnabled); - writeColor(obj, "FontColor", FontColor); - writeColor(obj, "FontColorFaded", FontColorFaded); - writeColor(obj, "ConnectionPointColor", ConnectionPointColor); - writeColor(obj, "FilledConnectionPointColor", FilledConnectionPointColor); - writeColor(obj, "WarningColor", WarningColor); - writeColor(obj, "ErrorColor", ErrorColor); - - writeFloat(obj, "PenWidth", PenWidth); - writeFloat(obj, "HoveredPenWidth", HoveredPenWidth); - writeFloat(obj, "ConnectionPointDiameter", ConnectionPointDiameter); - - writeFloat(obj, "Opacity", Opacity); + writeColor(obj, "NormalBoundaryColor", _NormalBoundaryColor); + writeColor(obj, "SelectedBoundaryColor", _SelectedBoundaryColor); + writeColor(obj, "GradientColor0", _GradientColor0); + writeColor(obj, "GradientColor1", _GradientColor1); + writeColor(obj, "GradientColor2", _GradientColor2); + writeColor(obj, "GradientColor3", _GradientColor3); + writeColor(obj, "ShadowColor", _ShadowColor); + writeBool(obj, "ShadowEnabled", _ShadowEnabled); + writeColor(obj, "FontColor", _FontColor); + writeColor(obj, "FontColorFaded", _FontColorFaded); + writeColor(obj, "ConnectionPointColor", _ConnectionPointColor); + writeColor(obj, "FilledConnectionPointColor", _FilledConnectionPointColor); + writeColor(obj, "WarningColor", _WarningColor); + writeColor(obj, "ErrorColor", _ErrorColor); + + writeFloat(obj, "PenWidth", _PenWidth); + writeFloat(obj, "HoveredPenWidth", _HoveredPenWidth); + writeFloat(obj, "ConnectionPointDiameter", _ConnectionPointDiameter); + + writeFloat(obj, "Opacity", _Opacity); QJsonObject root; root["NodeStyle"] = obj; @@ -108,13 +108,48 @@ QJsonObject NodeStyle::toJson() const void NodeStyle::setBackgroundColor(QColor const &color) { - GradientColor0 = color; - GradientColor1 = color; - GradientColor2 = color; - GradientColor3 = color; + _GradientColor0 = color; + _GradientColor1 = color; + _GradientColor2 = color; + _GradientColor3 = color; } QColor NodeStyle::backgroundColor() const { - return GradientColor0; + return _GradientColor0; } + +QColor NodeStyle::normalBoundaryColor() const { return _NormalBoundaryColor; } +QColor NodeStyle::selectedBoundaryColor() const { return _SelectedBoundaryColor; } +QColor NodeStyle::gradientColor0() const { return _GradientColor0; } +QColor NodeStyle::gradientColor1() const { return _GradientColor1; } +QColor NodeStyle::gradientColor2() const { return _GradientColor2; } +QColor NodeStyle::gradientColor3() const { return _GradientColor3; } +QColor NodeStyle::shadowColor() const { return _ShadowColor; } +bool NodeStyle::shadowEnabled() const { return _ShadowEnabled; } +QColor NodeStyle::fontColor() const { return _FontColor; } +QColor NodeStyle::fontColorFaded() const { return _FontColorFaded; } +QColor NodeStyle::connectionPointColor() const { return _ConnectionPointColor; } +QColor NodeStyle::filledConnectionPointColor() const { return _FilledConnectionPointColor; } +QColor NodeStyle::warningColor() const { return _WarningColor; } +QColor NodeStyle::errorColor() const { return _ErrorColor; } +QColor NodeStyle::toolTipIconColor() const { return _ToolTipIconColor; } +float NodeStyle::penWidth() const { return _PenWidth; } +float NodeStyle::hoveredPenWidth() const { return _HoveredPenWidth; } +float NodeStyle::connectionPointDiameter() const { return _ConnectionPointDiameter; } +float NodeStyle::opacity() const { return _Opacity; } +QIcon const &NodeStyle::statusUpdated() const { return _statusUpdated; } +QIcon const &NodeStyle::statusProcessing() const { return _statusProcessing; } +QIcon const &NodeStyle::statusPending() const { return _statusPending; } +QIcon const &NodeStyle::statusInvalid() const { return _statusInvalid; } +QIcon const &NodeStyle::statusEmpty() const { return _statusEmpty; } +QIcon const &NodeStyle::statusPartial() const { return _statusPartial; } +ProcessingIconStyle const &NodeStyle::processingIconStyle() const { return _processingIconStyle; } + +void NodeStyle::setStatusUpdated(QIcon const &icon) { _statusUpdated = icon; } +void NodeStyle::setStatusProcessing(QIcon const &icon) { _statusProcessing = icon; } +void NodeStyle::setStatusPending(QIcon const &icon) { _statusPending = icon; } +void NodeStyle::setStatusInvalid(QIcon const &icon) { _statusInvalid = icon; } +void NodeStyle::setStatusEmpty(QIcon const &icon) { _statusEmpty = icon; } +void NodeStyle::setStatusPartial(QIcon const &icon) { _statusPartial = icon; } +void NodeStyle::setProcessingIconStyle(ProcessingIconStyle const &style) { _processingIconStyle = style; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6432f73b..f502ac61 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -5,7 +5,6 @@ else() endif() add_executable(test_nodes - test_main.cpp src/TestAbstractGraphModel.cpp src/TestAbstractGraphModelSignals.cpp src/TestDataFlowGraphModel.cpp @@ -36,7 +35,7 @@ target_include_directories(test_nodes target_link_libraries(test_nodes PRIVATE QtNodes::QtNodes - Catch2::Catch2 + Catch2::Catch2WithMain Qt${QT_VERSION_MAJOR}::Test ) diff --git a/test/src/TestAbstractGraphModel.cpp b/test/src/TestAbstractGraphModel.cpp index 0d2abb7e..0f7674c4 100644 --- a/test/src/TestAbstractGraphModel.cpp +++ b/test/src/TestAbstractGraphModel.cpp @@ -1,6 +1,6 @@ #include "TestGraphModel.hpp" -#include +#include #include #include diff --git a/test/src/TestAbstractGraphModelSignals.cpp b/test/src/TestAbstractGraphModelSignals.cpp index 9ec08436..be51051c 100644 --- a/test/src/TestAbstractGraphModelSignals.cpp +++ b/test/src/TestAbstractGraphModelSignals.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include diff --git a/test/src/TestBasicGraphicsScene.cpp b/test/src/TestBasicGraphicsScene.cpp index b52f08cf..4ab00ae1 100644 --- a/test/src/TestBasicGraphicsScene.cpp +++ b/test/src/TestBasicGraphicsScene.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include @@ -122,7 +122,7 @@ TEST_CASE("BasicGraphicsScene functionality", "[graphics]") auto *nodeGraphics = scene.nodeGraphicsObject(nodeId); REQUIRE(nodeGraphics != nullptr); - CHECK(nodeGraphics->opacity() == Approx(QtNodes::StyleCollection::nodeStyle().Opacity)); + CHECK(nodeGraphics->opacity() == Approx(QtNodes::StyleCollection::nodeStyle().opacity())); } } diff --git a/test/src/TestConnectionId.cpp b/test/src/TestConnectionId.cpp index 95f9a29c..de0e8096 100644 --- a/test/src/TestConnectionId.cpp +++ b/test/src/TestConnectionId.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include using QtNodes::ConnectionId; using QtNodes::NodeId; diff --git a/test/src/TestCopyPaste.cpp b/test/src/TestCopyPaste.cpp index 808b4cc1..3e4da465 100644 --- a/test/src/TestCopyPaste.cpp +++ b/test/src/TestCopyPaste.cpp @@ -1,7 +1,7 @@ #include "ApplicationSetup.hpp" #include "TestGraphModel.hpp" -#include +#include #include #include diff --git a/test/src/TestCustomPainters.cpp b/test/src/TestCustomPainters.cpp index 26d0402b..f38045b7 100644 --- a/test/src/TestCustomPainters.cpp +++ b/test/src/TestCustomPainters.cpp @@ -2,7 +2,7 @@ #include "TestGraphModel.hpp" #include "UITestHelper.hpp" -#include +#include #include #include diff --git a/test/src/TestDataFlow.cpp b/test/src/TestDataFlow.cpp index 95ec307b..a115dc0c 100644 --- a/test/src/TestDataFlow.cpp +++ b/test/src/TestDataFlow.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include diff --git a/test/src/TestDataFlowGraphModel.cpp b/test/src/TestDataFlowGraphModel.cpp index db0f7891..ca09bedd 100644 --- a/test/src/TestDataFlowGraphModel.cpp +++ b/test/src/TestDataFlowGraphModel.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include using QtNodes::ConnectionId; using QtNodes::DataFlowGraphModel; diff --git a/test/src/TestLoopDetection.cpp b/test/src/TestLoopDetection.cpp index 7e525503..460fb480 100644 --- a/test/src/TestLoopDetection.cpp +++ b/test/src/TestLoopDetection.cpp @@ -2,7 +2,7 @@ #include "TestGraphModel.hpp" #include "TestDataFlowNodes.hpp" -#include +#include #include #include diff --git a/test/src/TestNodeDelegateModelRegistry.cpp b/test/src/TestNodeDelegateModelRegistry.cpp index 4efb91b3..04c9e0d5 100644 --- a/test/src/TestNodeDelegateModelRegistry.cpp +++ b/test/src/TestNodeDelegateModelRegistry.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include using QtNodes::NodeDelegateModel; using QtNodes::NodeDelegateModelRegistry; diff --git a/test/src/TestNodeGroup.cpp b/test/src/TestNodeGroup.cpp index a7815e7d..0a416a4c 100644 --- a/test/src/TestNodeGroup.cpp +++ b/test/src/TestNodeGroup.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include diff --git a/test/src/TestNodeValidation.cpp b/test/src/TestNodeValidation.cpp index 0cd643ea..3db34bc4 100644 --- a/test/src/TestNodeValidation.cpp +++ b/test/src/TestNodeValidation.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/test/src/TestSerialization.cpp b/test/src/TestSerialization.cpp index 5b856154..d0d35094 100644 --- a/test/src/TestSerialization.cpp +++ b/test/src/TestSerialization.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include #include diff --git a/test/src/TestUIInteraction.cpp b/test/src/TestUIInteraction.cpp index cca0faeb..1d9d4d6f 100644 --- a/test/src/TestUIInteraction.cpp +++ b/test/src/TestUIInteraction.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include diff --git a/test/src/TestUndoCommands.cpp b/test/src/TestUndoCommands.cpp index 0ed033c0..63742a41 100644 --- a/test/src/TestUndoCommands.cpp +++ b/test/src/TestUndoCommands.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include diff --git a/test/src/TestZoomFeatures.cpp b/test/src/TestZoomFeatures.cpp index 36b3ba67..01f0ae3a 100644 --- a/test/src/TestZoomFeatures.cpp +++ b/test/src/TestZoomFeatures.cpp @@ -1,7 +1,7 @@ #include "ApplicationSetup.hpp" #include "TestGraphModel.hpp" -#include +#include #include #include diff --git a/test/test_main.cpp b/test/test_main.cpp deleted file mode 100644 index 4ed06df1..00000000 --- a/test/test_main.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include