From 65cd227f46af53dd0a0862317c544bd378246608 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 6 Feb 2026 12:39:11 -0700 Subject: [PATCH] Add line routing patches --- CMakeLists.txt | 2 + gui/SquareConnectionPainter.cpp | 165 +++++++++++++++--- patches/qtnodes-connection-boundingRect.patch | 19 ++ 3 files changed, 161 insertions(+), 25 deletions(-) create mode 100644 patches/qtnodes-connection-boundingRect.patch diff --git a/CMakeLists.txt b/CMakeLists.txt index 497e248..8624a08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,10 +71,12 @@ if(WARPPIPE_BUILD_GUI) find_package(Qt6 6.2 REQUIRED COMPONENTS Core Widgets) + set(_qtnodes_patch ${CMAKE_CURRENT_SOURCE_DIR}/patches/qtnodes-connection-boundingRect.patch) FetchContent_Declare( QtNodes GIT_REPOSITORY https://github.com/paceholder/nodeeditor GIT_TAG master + PATCH_COMMAND sh -c "git apply ${_qtnodes_patch} || true" ) FetchContent_MakeAvailable(QtNodes) diff --git a/gui/SquareConnectionPainter.cpp b/gui/SquareConnectionPainter.cpp index 752a7d6..ffa09b4 100644 --- a/gui/SquareConnectionPainter.cpp +++ b/gui/SquareConnectionPainter.cpp @@ -1,8 +1,10 @@ #include "SquareConnectionPainter.h" +#include #include #include #include +#include #include #include @@ -12,51 +14,164 @@ #include #include +namespace { + +/// Append a quadratic-rounded right-angle turn to @p path. +/// Draws a straight segment toward @p corner, a smooth quarter-turn, and +/// leaves the current position just past the turn headed toward @p after. +/// The radius is clamped so it never overshoots either the incoming or +/// outgoing segment. +void roundedCorner(QPainterPath &path, + QPointF const &corner, + QPointF const &after, + double maxR) { + QPointF const before = path.currentPosition(); + + double const dx1 = corner.x() - before.x(); + double const dy1 = corner.y() - before.y(); + double const len1 = std::sqrt(dx1 * dx1 + dy1 * dy1); + + double const dx2 = after.x() - corner.x(); + double const dy2 = after.y() - corner.y(); + double const len2 = std::sqrt(dx2 * dx2 + dy2 * dy2); + + double const r = std::min({maxR, len1 / 2.0, len2 / 2.0}); + + if (r < 0.5 || len1 < 0.5 || len2 < 0.5) { + path.lineTo(corner); + return; + } + + double const ux1 = dx1 / len1, uy1 = dy1 / len1; + double const ux2 = dx2 / len2, uy2 = dy2 / len2; + + QPointF const start(corner.x() - ux1 * r, corner.y() - uy1 * r); + QPointF const end(corner.x() + ux2 * r, corner.y() + uy2 * r); + + path.lineTo(start); + path.quadTo(corner, end); +} + +} // namespace + QPainterPath SquareConnectionPainter::orthogonalPath( QtNodes::ConnectionGraphicsObject const &cgo) const { - QPointF out = cgo.endPoint(QtNodes::PortType::Out); - QPointF in = cgo.endPoint(QtNodes::PortType::In); + QPointF const out = cgo.endPoint(QtNodes::PortType::Out); + QPointF const in = cgo.endPoint(QtNodes::PortType::In); constexpr double kRadius = 5.0; constexpr double kSpacing = 8.0; constexpr double kMinStub = 20.0; + constexpr double kNodePad = 15.0; - auto cId = cgo.connectionId(); - double spread = static_cast(cId.outPortIndex) * kSpacing; + auto const cId = cgo.connectionId(); + double const spread = static_cast(cId.outPortIndex) * kSpacing; - double dy = in.y() - out.y(); + double const dy = in.y() - out.y(); + // Straight line when ports are nearly level and input is to the right. if (std::abs(dy) < 0.5 && in.x() > out.x()) { QPainterPath path(out); path.lineTo(in); return path; } - double midX = (out.x() + in.x()) / 2.0 + spread; - midX = std::max(midX, out.x() + kMinStub); - if (in.x() > out.x()) + // A connection is "backward" when the input port is close to or left of + // the output port — the simple 3-segment Z-path would double back on + // itself and cut through nodes. Use a 5-segment S-shaped path instead. + bool const backward = (in.x() < out.x() + 2.0 * kMinStub); + + if (!backward) { + // ---- Forward: 3-segment Z-path ---- + double midX = (out.x() + in.x()) / 2.0 + spread; + midX = std::max(midX, out.x() + kMinStub); midX = std::min(midX, in.x() - kMinStub); - double r = std::min({kRadius, std::abs(dy) / 2.0, - std::abs(midX - out.x()), - std::abs(in.x() - midX)}); - r = std::max(r, 0.0); + QPointF const c1(midX, out.y()); + QPointF const c2(midX, in.y()); - double sy = (dy > 0) ? 1.0 : -1.0; - double hDir = (in.x() >= midX) ? 1.0 : -1.0; - - QPainterPath path(out); - - if (r > 0.5) { - path.lineTo(midX - r, out.y()); - path.quadTo(QPointF(midX, out.y()), QPointF(midX, out.y() + sy * r)); - path.lineTo(midX, in.y() - sy * r); - path.quadTo(QPointF(midX, in.y()), QPointF(midX + hDir * r, in.y())); - } else { - path.lineTo(midX, out.y()); - path.lineTo(midX, in.y()); + QPainterPath path(out); + roundedCorner(path, c1, c2, kRadius); + roundedCorner(path, c2, in, kRadius); + path.lineTo(in); + return path; } + // ---- Backward: 5-segment S-path ---- + // + // The path leaves the output port going right, routes around both + // nodes via two vertical rails (one on the right, one on the left), + // connected by a horizontal crossover, then enters the input port + // from the left: + // + // leftX rightX + // | | + // in ------+ | seg 5 + corner 4 + // | | seg 4 + // +------- midY --+ seg 3 + corners 2,3 + // | seg 2 + // out -------+ seg 1 + corner 1 + // + + double rightX = out.x() + kMinStub + spread; + double leftX = in.x() - kMinStub - spread; + double midY = (out.y() + in.y()) / 2.0; + + // Use actual node geometry when available so the path routes cleanly + // around both nodes instead of cutting through them. + auto *scene = cgo.nodeScene(); + if (scene) { + auto *outNGO = scene->nodeGraphicsObject(cId.outNodeId); + auto *inNGO = scene->nodeGraphicsObject(cId.inNodeId); + if (outNGO && inNGO) { + // Map node scene bounds into the CGO's local coordinate space + // (endPoint() values live there too). + QRectF const outRect = + cgo.mapRectFromScene(outNGO->sceneBoundingRect()); + QRectF const inRect = + cgo.mapRectFromScene(inNGO->sceneBoundingRect()); + + // Push vertical rails outside both nodes. + double const rightEdge = std::max(outRect.right(), inRect.right()); + rightX = std::max(rightX, rightEdge + kNodePad + spread); + + double const leftEdge = std::min(outRect.left(), inRect.left()); + leftX = std::min(leftX, leftEdge - kNodePad - spread); + + // Place the horizontal crossover in the gap between nodes when + // they don't overlap vertically; otherwise route above or below. + double const topInner = + std::min(outRect.bottom(), inRect.bottom()); + double const botInner = + std::max(outRect.top(), inRect.top()); + + if (botInner > topInner + 2.0 * kNodePad) { + // Vertical gap exists — clamp midY into the gap. + midY = std::clamp(midY, topInner + kNodePad, + botInner - kNodePad); + } else { + // Nodes overlap vertically — pick the shorter detour. + double const above = + std::min(outRect.top(), inRect.top()) - kNodePad; + double const below = + std::max(outRect.bottom(), inRect.bottom()) + kNodePad; + midY = (std::abs(midY - above) < std::abs(midY - below)) + ? above + : below; + } + } + } + + QPointF const p1(rightX, out.y()); + QPointF const p2(rightX, midY); + QPointF const p3(leftX, midY); + QPointF const p4(leftX, in.y()); + + QPainterPath path(out); + roundedCorner(path, p1, p2, kRadius); + roundedCorner(path, p2, p3, kRadius); + roundedCorner(path, p3, p4, kRadius); + roundedCorner(path, p4, in, kRadius); path.lineTo(in); return path; } diff --git a/patches/qtnodes-connection-boundingRect.patch b/patches/qtnodes-connection-boundingRect.patch new file mode 100644 index 0000000..00f969e --- /dev/null +++ b/patches/qtnodes-connection-boundingRect.patch @@ -0,0 +1,19 @@ +diff --git a/src/ConnectionGraphicsObject.cpp b/src/ConnectionGraphicsObject.cpp +index 05ae46b..9a1eeea 100644 +--- a/src/ConnectionGraphicsObject.cpp ++++ b/src/ConnectionGraphicsObject.cpp +@@ -107,6 +107,14 @@ QRectF ConnectionGraphicsObject::boundingRect() const + + QRectF commonRect = basicRect.united(c1c2Rect); + ++ // Include the painter stroke so custom painters that route beyond ++ // the default bezier control points are not clipped. ++ auto *ns = nodeScene(); ++ if (ns) { ++ QRectF strokeRect = ns->connectionPainter().getPainterStroke(*this).boundingRect(); ++ commonRect = commonRect.united(strokeRect); ++ } ++ + auto const &connectionStyle = StyleCollection::connectionStyle(); + float const diam = connectionStyle.pointDiameter(); + QPointF const cornerOffset(diam, diam);