Compare commits

...

2 commits

Author SHA1 Message Date
621d67ebab Group nodes by app name 2026-02-06 14:19:14 -07:00
07e2fb4c5a Better routing 2026-02-06 13:24:18 -07:00
4 changed files with 785 additions and 145 deletions

View file

@ -802,13 +802,13 @@ void GraphEditorWidget::onContextMenuRequested(const QPoint &pos) {
QPointF scenePos = m_view->mapToScene(pos);
m_lastContextMenuScenePos = scenePos;
bool hitNode = false;
uint32_t hitPwNodeId = 0;
QtNodes::NodeId hitQtNodeId = 0;
for (auto nodeId : m_model->allNodeIds()) {
const WarpNodeData *data = m_model->warpNodeData(nodeId);
if (!data) {
if (!data)
continue;
}
QPointF nodePos =
m_model->nodeData(nodeId, QtNodes::NodeRole::Position).toPointF();
QSize nodeSize =
@ -817,12 +817,13 @@ void GraphEditorWidget::onContextMenuRequested(const QPoint &pos) {
if (nodeRect.contains(scenePos)) {
hitPwNodeId = data->info.id.value;
hitQtNodeId = nodeId;
hitNode = true;
break;
}
}
QPoint screenPos = m_view->mapToGlobal(pos);
if (hitPwNodeId != 0) {
if (hitNode) {
showNodeContextMenu(screenPos, hitPwNodeId, hitQtNodeId);
} else {
showCanvasContextMenu(screenPos, scenePos);
@ -1673,13 +1674,29 @@ void GraphEditorWidget::updateMeters() {
const WarpNodeData *data = m_model->warpNodeData(nodeId);
if (!data || !row.meter)
continue;
auto peak = m_client->NodeMeterPeak(data->info.id);
if (peak.ok()) {
float level = std::max(peak.value.peak_left, peak.value.peak_right);
row.meter->setLevel(level);
m_model->setNodePeakLevel(nodeId, level);
if (level > 0.001f)
const AppGroupData *group = m_model->appGroupData(nodeId);
if (group) {
float maxLevel = 0.0f;
for (uint32_t memberPwId : group->memberPwIds) {
auto peak = m_client->NodeMeterPeak(warppipe::NodeId{memberPwId});
if (peak.ok())
maxLevel = std::max(maxLevel,
std::max(peak.value.peak_left, peak.value.peak_right));
}
row.meter->setLevel(maxLevel);
m_model->setNodePeakLevel(nodeId, maxLevel);
if (maxLevel > 0.001f)
anyActive = true;
} else {
auto peak = m_client->NodeMeterPeak(data->info.id);
if (peak.ok()) {
float level = std::max(peak.value.peak_left, peak.value.peak_right);
row.meter->setLevel(level);
m_model->setNodePeakLevel(nodeId, level);
if (level > 0.001f)
anyActive = true;
}
}
}
@ -1720,8 +1737,16 @@ void GraphEditorWidget::rebuildNodeMeters() {
if (!data)
continue;
new_pw_ids[data->info.id.value] = true;
m_client->EnsureNodeMeter(data->info.id);
const AppGroupData *group = m_model->appGroupData(nodeId);
if (group) {
for (uint32_t memberPwId : group->memberPwIds) {
new_pw_ids[memberPwId] = true;
m_client->EnsureNodeMeter(warppipe::NodeId{memberPwId});
}
} else {
new_pw_ids[data->info.id.value] = true;
m_client->EnsureNodeMeter(data->info.id);
}
auto *row = new QWidget();
auto *rowLayout = new QHBoxLayout(row);
@ -2299,8 +2324,21 @@ void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) {
QString::fromStdString(info.media_role));
}
addField(QStringLiteral("NODE ID"),
QString::number(info.id.value));
const AppGroupData *groupData = m_model->appGroupData(nodeId);
if (groupData) {
addField(QStringLiteral("STREAMS"),
QString::number(groupData->memberPwIds.size()));
QString memberIds;
for (uint32_t pwId : groupData->memberPwIds) {
if (!memberIds.isEmpty())
memberIds += QStringLiteral(", ");
memberIds += QString::number(pwId);
}
addField(QStringLiteral("MEMBER NODE IDS"), memberIds);
} else {
addField(QStringLiteral("NODE ID"),
QString::number(info.id.value));
}
if (!data->inputPorts.empty()) {
layout->addSpacing(8);

View file

@ -66,7 +66,17 @@ QPainterPath SquareConnectionPainter::orthogonalPath(
constexpr double kNodePad = 15.0;
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();
@ -114,51 +124,52 @@ QPainterPath SquareConnectionPainter::orthogonalPath(
// 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;
double railOffset = 0.0;
if (sceneForChannel) {
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();
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);
rightX = std::max(rightX, rightEdge + kNodePad + railOffset);
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 =
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;
double baseMidY = (out.y() + in.y()) / 2.0;
midY = (std::abs(baseMidY - above) < std::abs(baseMidY - below))
? above + spread
: below + spread;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -43,6 +43,15 @@ struct WarpNodeData {
std::vector<warppipe::PortInfo> outputPorts;
};
/// Data for an app-group node (multiple PipeWire streams collapsed into one visual node).
struct AppGroupData {
std::string groupKey;
std::vector<uint32_t> memberPwIds; ///< PipeWire node IDs in this group.
/// canonical port index → list of actual member PortIds for fan-out.
std::unordered_map<unsigned int, std::vector<warppipe::PortId>> outputPortMap;
std::unordered_map<unsigned int, std::vector<warppipe::PortId>> inputPortMap;
};
class WarpGraphModel : public QtNodes::AbstractGraphModel {
Q_OBJECT
@ -87,6 +96,9 @@ public:
uint32_t findPwNodeIdByName(const std::string &name) const;
bool isGroupNode(QtNodes::NodeId nodeId) const;
const AppGroupData *appGroupData(QtNodes::NodeId nodeId) const;
struct NodeVolumeState {
float volume = 1.0f;
bool mute = false;
@ -98,6 +110,12 @@ public:
void setNodePeakLevel(QtNodes::NodeId nodeId, float level);
float nodePeakLevel(QtNodes::NodeId nodeId) const;
struct ConnectionChannel {
int index = 0;
int count = 1;
};
ConnectionChannel connectionChannel(QtNodes::ConnectionId cId) const;
Q_SIGNALS:
void beginBatchUpdate();
void endBatchUpdate();
@ -173,4 +191,20 @@ private:
std::unordered_map<QtNodes::NodeId, QPointer<QWidget>> m_volumeWidgets;
mutable std::unordered_map<QtNodes::NodeId, QVariant> m_styleCache;
std::unordered_map<QtNodes::NodeId, float> m_peakLevels;
std::unordered_map<QtNodes::ConnectionId, ConnectionChannel> m_connectionChannels;
std::unordered_map<QtNodes::NodeId, AppGroupData> m_appGroups;
std::unordered_map<std::string, QtNodes::NodeId> m_groupKeyToQt;
std::unordered_map<uint32_t, QtNodes::NodeId> m_pwToGroupQt;
struct GroupPortRef {
QtNodes::NodeId groupQtId;
QtNodes::PortIndex portIndex;
bool isInput;
};
std::unordered_map<uint32_t, GroupPortRef> m_portToGroupPort;
void rebuildGroupPortMap(QtNodes::NodeId groupQtId);
void recomputeConnectionChannels();
};