Better routing

This commit is contained in:
Joey Yakimowich-Payne 2026-02-06 13:24:18 -07:00
commit 07e2fb4c5a
3 changed files with 99 additions and 27 deletions

View file

@ -66,7 +66,17 @@ QPainterPath SquareConnectionPainter::orthogonalPath(
constexpr double kNodePad = 15.0; constexpr double kNodePad = 15.0;
auto const cId = cgo.connectionId(); auto const cId = cgo.connectionId();
double const spread = static_cast<double>(cId.outPortIndex) * kSpacing;
double spread = static_cast<double>(cId.outPortIndex) * kSpacing;
auto *sceneForChannel = cgo.nodeScene();
if (sceneForChannel) {
auto *mdl = dynamic_cast<WarpGraphModel *>(&sceneForChannel->graphModel());
if (mdl) {
auto ch = mdl->connectionChannel(cId);
spread = (static_cast<double>(ch.index) - (ch.count - 1) / 2.0)
* kSpacing;
}
}
double const dy = in.y() - out.y(); double const dy = in.y() - out.y();
@ -114,51 +124,52 @@ QPainterPath SquareConnectionPainter::orthogonalPath(
// out -------+ seg 1 + corner 1 // out -------+ seg 1 + corner 1
// //
double rightX = out.x() + kMinStub + spread; double railOffset = 0.0;
double leftX = in.x() - kMinStub - spread; if (sceneForChannel) {
double midY = (out.y() + in.y()) / 2.0; auto *mdl2 = dynamic_cast<WarpGraphModel *>(&sceneForChannel->graphModel());
if (mdl2) {
auto ch = mdl2->connectionChannel(cId);
railOffset = static_cast<double>(ch.index) * kSpacing;
}
}
double rightX = out.x() + kMinStub + railOffset;
double leftX = in.x() - kMinStub - railOffset;
double midY = (out.y() + in.y()) / 2.0 + spread;
// Use actual node geometry when available so the path routes cleanly
// around both nodes instead of cutting through them.
auto *scene = cgo.nodeScene(); auto *scene = cgo.nodeScene();
if (scene) { if (scene) {
auto *outNGO = scene->nodeGraphicsObject(cId.outNodeId); auto *outNGO = scene->nodeGraphicsObject(cId.outNodeId);
auto *inNGO = scene->nodeGraphicsObject(cId.inNodeId); auto *inNGO = scene->nodeGraphicsObject(cId.inNodeId);
if (outNGO && inNGO) { if (outNGO && inNGO) {
// Map node scene bounds into the CGO's local coordinate space
// (endPoint() values live there too).
QRectF const outRect = QRectF const outRect =
cgo.mapRectFromScene(outNGO->sceneBoundingRect()); cgo.mapRectFromScene(outNGO->sceneBoundingRect());
QRectF const inRect = QRectF const inRect =
cgo.mapRectFromScene(inNGO->sceneBoundingRect()); cgo.mapRectFromScene(inNGO->sceneBoundingRect());
// Push vertical rails outside both nodes.
double const rightEdge = std::max(outRect.right(), inRect.right()); double const rightEdge = std::max(outRect.right(), inRect.right());
rightX = std::max(rightX, rightEdge + kNodePad + spread); rightX = std::max(rightX, rightEdge + kNodePad + railOffset);
double const leftEdge = std::min(outRect.left(), inRect.left()); double const leftEdge = std::min(outRect.left(), inRect.left());
leftX = std::min(leftX, leftEdge - kNodePad - spread); leftX = std::min(leftX, leftEdge - kNodePad - railOffset);
// Place the horizontal crossover in the gap between nodes when
// they don't overlap vertically; otherwise route above or below.
double const topInner = double const topInner =
std::min(outRect.bottom(), inRect.bottom()); std::min(outRect.bottom(), inRect.bottom());
double const botInner = double const botInner =
std::max(outRect.top(), inRect.top()); std::max(outRect.top(), inRect.top());
if (botInner > topInner + 2.0 * kNodePad) { if (botInner > topInner + 2.0 * kNodePad) {
// Vertical gap exists — clamp midY into the gap.
midY = std::clamp(midY, topInner + kNodePad, midY = std::clamp(midY, topInner + kNodePad,
botInner - kNodePad); botInner - kNodePad);
} else { } else {
// Nodes overlap vertically — pick the shorter detour.
double const above = double const above =
std::min(outRect.top(), inRect.top()) - kNodePad; std::min(outRect.top(), inRect.top()) - kNodePad;
double const below = double const below =
std::max(outRect.bottom(), inRect.bottom()) + kNodePad; std::max(outRect.bottom(), inRect.bottom()) + kNodePad;
midY = (std::abs(midY - above) < std::abs(midY - below)) double baseMidY = (out.y() + in.y()) / 2.0;
? above midY = (std::abs(baseMidY - above) < std::abs(baseMidY - below))
: below; ? above + spread
: below + spread;
} }
} }
} }

View file

@ -565,22 +565,38 @@ void WarpGraphModel::refreshFromClient() {
QtNodes::PortIndex inPortIdx = 0; QtNodes::PortIndex inPortIdx = 0;
bool found = false; bool found = false;
bool outPortFound = false;
bool inPortFound = false;
for (const auto &[qtId, nodeData] : m_nodes) { for (const auto &[qtId, nodeData] : m_nodes) {
for (size_t i = 0; i < nodeData.outputPorts.size(); ++i) { if (!outPortFound) {
if (nodeData.outputPorts[i].id.value == link.output_port.value) { for (size_t i = 0; i < nodeData.outputPorts.size(); ++i) {
outNodeIt = m_pwToQt.find(nodeData.info.id.value); if (nodeData.outputPorts[i].id.value ==
outPortIdx = static_cast<QtNodes::PortIndex>(i); link.output_port.value) {
outNodeIt = m_pwToQt.find(nodeData.info.id.value);
outPortIdx = static_cast<QtNodes::PortIndex>(i);
outPortFound = true;
break;
}
} }
} }
for (size_t i = 0; i < nodeData.inputPorts.size(); ++i) { if (!inPortFound) {
if (nodeData.inputPorts[i].id.value == link.input_port.value) { for (size_t i = 0; i < nodeData.inputPorts.size(); ++i) {
inNodeIt = m_pwToQt.find(nodeData.info.id.value); if (nodeData.inputPorts[i].id.value ==
inPortIdx = static_cast<QtNodes::PortIndex>(i); link.input_port.value) {
inNodeIt = m_pwToQt.find(nodeData.info.id.value);
inPortIdx = static_cast<QtNodes::PortIndex>(i);
inPortFound = true;
break;
}
} }
} }
if (outPortFound && inPortFound)
break;
} }
if (outNodeIt != m_pwToQt.end() && inNodeIt != m_pwToQt.end()) { if (outPortFound && inPortFound && outNodeIt != m_pwToQt.end() &&
inNodeIt != m_pwToQt.end()) {
found = true; found = true;
} }
@ -710,6 +726,8 @@ void WarpGraphModel::refreshFromClient() {
Q_EMIT nodeVolumeChanged(qtId, previous, cached); Q_EMIT nodeVolumeChanged(qtId, previous, cached);
} }
recomputeConnectionChannels();
m_refreshing = false; m_refreshing = false;
if (sceneChanged) { if (sceneChanged) {
Q_EMIT endBatchUpdate(); Q_EMIT endBatchUpdate();
@ -976,6 +994,40 @@ float WarpGraphModel::nodePeakLevel(QtNodes::NodeId nodeId) const {
return it != m_peakLevels.end() ? it->second : 0.0f; return it != m_peakLevels.end() ? it->second : 0.0f;
} }
void WarpGraphModel::recomputeConnectionChannels() {
m_connectionChannels.clear();
std::unordered_map<QtNodes::NodeId, std::vector<QtNodes::ConnectionId>> byTarget;
for (const auto &cId : m_connections)
byTarget[cId.inNodeId].push_back(cId);
for (auto &[targetId, conns] : byTarget) {
std::sort(conns.begin(), conns.end(),
[this](const auto &a, const auto &b) {
auto posA = m_positions.count(a.outNodeId)
? m_positions.at(a.outNodeId).y()
: 0.0;
auto posB = m_positions.count(b.outNodeId)
? m_positions.at(b.outNodeId).y()
: 0.0;
if (posA != posB)
return posA < posB;
return a.outPortIndex < b.outPortIndex;
});
int count = static_cast<int>(conns.size());
for (int i = 0; i < count; ++i)
m_connectionChannels[conns[i]] = {i, count};
}
}
WarpGraphModel::ConnectionChannel
WarpGraphModel::connectionChannel(QtNodes::ConnectionId cId) const {
auto it = m_connectionChannels.find(cId);
return it != m_connectionChannels.end() ? it->second
: ConnectionChannel{0, 1};
}
void WarpGraphModel::saveLayout(const QString &path) const { void WarpGraphModel::saveLayout(const QString &path) const {
ViewState vs{}; ViewState vs{};
saveLayout(path, vs); saveLayout(path, vs);

View file

@ -98,6 +98,12 @@ public:
void setNodePeakLevel(QtNodes::NodeId nodeId, float level); void setNodePeakLevel(QtNodes::NodeId nodeId, float level);
float nodePeakLevel(QtNodes::NodeId nodeId) const; float nodePeakLevel(QtNodes::NodeId nodeId) const;
struct ConnectionChannel {
int index = 0;
int count = 1;
};
ConnectionChannel connectionChannel(QtNodes::ConnectionId cId) const;
Q_SIGNALS: Q_SIGNALS:
void beginBatchUpdate(); void beginBatchUpdate();
void endBatchUpdate(); void endBatchUpdate();
@ -173,4 +179,7 @@ private:
std::unordered_map<QtNodes::NodeId, QPointer<QWidget>> m_volumeWidgets; std::unordered_map<QtNodes::NodeId, QPointer<QWidget>> m_volumeWidgets;
mutable std::unordered_map<QtNodes::NodeId, QVariant> m_styleCache; mutable std::unordered_map<QtNodes::NodeId, QVariant> m_styleCache;
std::unordered_map<QtNodes::NodeId, float> m_peakLevels; std::unordered_map<QtNodes::NodeId, float> m_peakLevels;
std::unordered_map<QtNodes::ConnectionId, ConnectionChannel> m_connectionChannels;
void recomputeConnectionChannels();
}; };