Add line routing patches

This commit is contained in:
Joey Yakimowich-Payne 2026-02-06 12:39:11 -07:00
commit 65cd227f46
3 changed files with 161 additions and 25 deletions

View file

@ -71,10 +71,12 @@ if(WARPPIPE_BUILD_GUI)
find_package(Qt6 6.2 REQUIRED COMPONENTS Core Widgets) find_package(Qt6 6.2 REQUIRED COMPONENTS Core Widgets)
set(_qtnodes_patch ${CMAKE_CURRENT_SOURCE_DIR}/patches/qtnodes-connection-boundingRect.patch)
FetchContent_Declare( FetchContent_Declare(
QtNodes QtNodes
GIT_REPOSITORY https://github.com/paceholder/nodeeditor GIT_REPOSITORY https://github.com/paceholder/nodeeditor
GIT_TAG master GIT_TAG master
PATCH_COMMAND sh -c "git apply ${_qtnodes_patch} || true"
) )
FetchContent_MakeAvailable(QtNodes) FetchContent_MakeAvailable(QtNodes)

View file

@ -1,8 +1,10 @@
#include "SquareConnectionPainter.h" #include "SquareConnectionPainter.h"
#include <QtNodes/internal/BasicGraphicsScene.hpp>
#include <QtNodes/internal/ConnectionGraphicsObject.hpp> #include <QtNodes/internal/ConnectionGraphicsObject.hpp>
#include <QtNodes/internal/ConnectionState.hpp> #include <QtNodes/internal/ConnectionState.hpp>
#include <QtNodes/internal/Definitions.hpp> #include <QtNodes/internal/Definitions.hpp>
#include <QtNodes/internal/NodeGraphicsObject.hpp>
#include <QtNodes/StyleCollection> #include <QtNodes/StyleCollection>
#include <QPainter> #include <QPainter>
@ -12,51 +14,164 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
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( QPainterPath SquareConnectionPainter::orthogonalPath(
QtNodes::ConnectionGraphicsObject const &cgo) const { QtNodes::ConnectionGraphicsObject const &cgo) const {
QPointF out = cgo.endPoint(QtNodes::PortType::Out); QPointF const out = cgo.endPoint(QtNodes::PortType::Out);
QPointF in = cgo.endPoint(QtNodes::PortType::In); QPointF const in = cgo.endPoint(QtNodes::PortType::In);
constexpr double kRadius = 5.0; constexpr double kRadius = 5.0;
constexpr double kSpacing = 8.0; constexpr double kSpacing = 8.0;
constexpr double kMinStub = 20.0; constexpr double kMinStub = 20.0;
constexpr double kNodePad = 15.0;
auto cId = cgo.connectionId(); auto const cId = cgo.connectionId();
double spread = static_cast<double>(cId.outPortIndex) * kSpacing; double const spread = static_cast<double>(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()) { if (std::abs(dy) < 0.5 && in.x() > out.x()) {
QPainterPath path(out); QPainterPath path(out);
path.lineTo(in); path.lineTo(in);
return path; return path;
} }
double midX = (out.x() + in.x()) / 2.0 + spread; // A connection is "backward" when the input port is close to or left of
midX = std::max(midX, out.x() + kMinStub); // the output port — the simple 3-segment Z-path would double back on
if (in.x() > out.x()) // 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); midX = std::min(midX, in.x() - kMinStub);
double r = std::min({kRadius, std::abs(dy) / 2.0, QPointF const c1(midX, out.y());
std::abs(midX - out.x()), QPointF const c2(midX, in.y());
std::abs(in.x() - midX)});
r = std::max(r, 0.0);
double sy = (dy > 0) ? 1.0 : -1.0; QPainterPath path(out);
double hDir = (in.x() >= midX) ? 1.0 : -1.0; roundedCorner(path, c1, c2, kRadius);
roundedCorner(path, c2, in, kRadius);
QPainterPath path(out); path.lineTo(in);
return path;
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());
} }
// ---- 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); path.lineTo(in);
return path; return path;
} }

View file

@ -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);