#include "SquareConnectionPainter.h" #include "WarpGraphModel.h" #include #include #include #include #include #include #include #include #include #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 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 const cId = cgo.connectionId(); auto *sceneForChannel = cgo.nodeScene(); auto *mdl = sceneForChannel ? dynamic_cast(&sceneForChannel->graphModel()) : nullptr; bool connectionAlive = mdl ? mdl->connectionExists(cId) : true; double spread = static_cast(cId.outPortIndex) * kSpacing; if (mdl && connectionAlive) { auto ch = mdl->connectionChannel(cId); spread = (static_cast(ch.index) - (ch.count - 1) / 2.0) * kSpacing; } 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; } // 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); QPointF const c1(midX, out.y()); QPointF const c2(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 railOffset = 0.0; if (mdl && connectionAlive) { auto ch = mdl->connectionChannel(cId); railOffset = static_cast(ch.index) * kSpacing; } double rightX = out.x() + kMinStub + railOffset; double leftX = in.x() - kMinStub - railOffset; double midY = (out.y() + in.y()) / 2.0 + spread; auto *scene = cgo.nodeScene(); if (scene) { auto *outNGO = scene->nodeGraphicsObject(cId.outNodeId); auto *inNGO = scene->nodeGraphicsObject(cId.inNodeId); if (outNGO && inNGO) { QRectF const outRect = cgo.mapRectFromScene(outNGO->sceneBoundingRect()); QRectF const inRect = cgo.mapRectFromScene(inNGO->sceneBoundingRect()); double const rightEdge = std::max(outRect.right(), inRect.right()); rightX = std::max(rightX, rightEdge + kNodePad + railOffset); double const leftEdge = std::min(outRect.left(), inRect.left()); leftX = std::min(leftX, leftEdge - kNodePad - railOffset); double const topInner = std::min(outRect.bottom(), inRect.bottom()); double const botInner = std::max(outRect.top(), inRect.top()); if (botInner > topInner + 2.0 * kNodePad) { midY = std::clamp(midY, topInner + kNodePad, botInner - kNodePad); } else { double const above = std::min(outRect.top(), inRect.top()) - kNodePad; double const below = std::max(outRect.bottom(), inRect.bottom()) + kNodePad; double baseMidY = (out.y() + in.y()) / 2.0; midY = (std::abs(baseMidY - above) < std::abs(baseMidY - below)) ? above + spread : below + spread; } } } 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; } void SquareConnectionPainter::paint( QPainter *painter, QtNodes::ConnectionGraphicsObject const &cgo) const { auto const &style = QtNodes::StyleCollection::connectionStyle(); bool const hovered = cgo.connectionState().hovered(); bool const selected = cgo.isSelected(); bool const sketch = cgo.connectionState().requiresPort(); auto path = orthogonalPath(cgo); float peakLevel = 0.0f; auto *scene = cgo.nodeScene(); if (scene) { auto *model = dynamic_cast(&scene->graphModel()); if (model) { auto cId = cgo.connectionId(); if (model->connectionExists(cId)) { peakLevel = model->nodePeakLevel(cId.outNodeId); } } } auto activeColor = [&](QColor base) -> QColor { if (peakLevel < 0.005f) return base; float t = std::min(peakLevel * 2.0f, 1.0f); int r = static_cast(base.red() + t * (60 - base.red())); int g = static_cast(base.green() + t * (210 - base.green())); int b = static_cast(base.blue() + t * (80 - base.blue())); return QColor(std::clamp(r, 0, 255), std::clamp(g, 0, 255), std::clamp(b, 0, 255), base.alpha()); }; if (hovered || selected) { QPen pen; pen.setWidth(static_cast(2 * style.lineWidth())); pen.setColor(selected ? style.selectedHaloColor() : style.hoveredColor()); painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->drawPath(path); } if (sketch) { QPen pen; pen.setWidth(static_cast(style.constructionLineWidth())); pen.setColor(style.constructionColor()); pen.setStyle(Qt::DashLine); painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->drawPath(path); } else { QColor base = selected ? style.selectedColor() : style.normalColor(); QColor color = selected ? base : activeColor(base); float width = style.lineWidth(); if (!selected && peakLevel > 0.005f) width += peakLevel * 1.5f; QPen pen; pen.setWidthF(width); pen.setColor(color); painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->drawPath(path); } double const pointRadius = style.pointDiameter() / 2.0; painter->setPen(style.constructionColor()); painter->setBrush(style.constructionColor()); painter->drawEllipse(cgo.out(), pointRadius, pointRadius); painter->drawEllipse(cgo.in(), pointRadius, pointRadius); } QPainterPath SquareConnectionPainter::getPainterStroke( QtNodes::ConnectionGraphicsObject const &cgo) const { auto path = orthogonalPath(cgo); QPainterPathStroker stroker; stroker.setWidth(10.0); return stroker.createStroke(path); }