warp-pipe/gui/SquareConnectionPainter.cpp

262 lines
8.7 KiB
C++

#include "SquareConnectionPainter.h"
#include "WarpGraphModel.h"
#include <QtNodes/internal/BasicGraphicsScene.hpp>
#include <QtNodes/internal/ConnectionGraphicsObject.hpp>
#include <QtNodes/internal/ConnectionState.hpp>
#include <QtNodes/internal/Definitions.hpp>
#include <QtNodes/internal/NodeGraphicsObject.hpp>
#include <QtNodes/StyleCollection>
#include <QPainter>
#include <QPainterPath>
#include <QPainterPathStroker>
#include <algorithm>
#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(
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();
double const spread = static_cast<double>(cId.outPortIndex) * 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 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;
}
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<WarpGraphModel *>(&scene->graphModel());
if (model) {
auto cId = cgo.connectionId();
peakLevel = std::max(model->nodePeakLevel(cId.outNodeId),
model->nodePeakLevel(cId.inNodeId));
}
}
auto activeColor = [&](QColor base) -> QColor {
if (peakLevel < 0.005f)
return base;
float t = std::min(peakLevel * 2.0f, 1.0f);
int r = static_cast<int>(base.red() + t * (60 - base.red()));
int g = static_cast<int>(base.green() + t * (210 - base.green()));
int b = static_cast<int>(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<int>(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<int>(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);
}