Nodes
This commit is contained in:
parent
4addf989cc
commit
87e5aca9d8
11 changed files with 459 additions and 33 deletions
|
|
@ -89,6 +89,7 @@ add_executable(potato-gui
|
||||||
src/main_gui.cpp
|
src/main_gui.cpp
|
||||||
src/gui/GraphEditorWidget.cpp
|
src/gui/GraphEditorWidget.cpp
|
||||||
src/gui/PipeWireGraphModel.cpp
|
src/gui/PipeWireGraphModel.cpp
|
||||||
|
src/meters/AudioLevelMeter.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(potato-gui PRIVATE
|
target_link_libraries(potato-gui PRIVATE
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,84 @@
|
||||||
#include "GraphEditorWidget.h"
|
#include "GraphEditorWidget.h"
|
||||||
|
#include "meters/AudioLevelMeter.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QColor>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QTimer>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <QtNodes/GraphicsViewStyle>
|
||||||
|
#include <QtNodes/NodeStyle>
|
||||||
|
|
||||||
GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent)
|
GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_controller(controller)
|
, m_controller(controller)
|
||||||
{
|
{
|
||||||
|
const QString nodeStyleJson = R"JSON({
|
||||||
|
"NodeStyle": {
|
||||||
|
"NormalBoundaryColor": [62, 68, 80],
|
||||||
|
"SelectedBoundaryColor": [42, 132, 194],
|
||||||
|
"GradientColor0": [214, 220, 230],
|
||||||
|
"GradientColor1": [202, 208, 220],
|
||||||
|
"GradientColor2": [192, 198, 210],
|
||||||
|
"GradientColor3": [182, 188, 200],
|
||||||
|
"ShadowColor": [0, 0, 0],
|
||||||
|
"FontColor": [28, 32, 38],
|
||||||
|
"FontColorFaded": [72, 78, 88],
|
||||||
|
"ConnectionPointColor": [62, 68, 80],
|
||||||
|
"FilledConnectionPointColor": [42, 132, 194],
|
||||||
|
"WarningColor": [196, 140, 45],
|
||||||
|
"ErrorColor": [196, 64, 64],
|
||||||
|
"PenWidth": 1.4,
|
||||||
|
"HoveredPenWidth": 2.2,
|
||||||
|
"ConnectionPointDiameter": 9.0,
|
||||||
|
"Opacity": 1.0
|
||||||
|
}
|
||||||
|
})JSON";
|
||||||
|
QtNodes::NodeStyle::setNodeStyle(nodeStyleJson);
|
||||||
|
|
||||||
|
const QString viewStyleJson = R"JSON({
|
||||||
|
"GraphicsViewStyle": {
|
||||||
|
"BackgroundColor": [26, 28, 34],
|
||||||
|
"FineGridColor": [46, 50, 60],
|
||||||
|
"CoarseGridColor": [66, 72, 86]
|
||||||
|
}
|
||||||
|
})JSON";
|
||||||
|
QtNodes::GraphicsViewStyle::setStyle(viewStyleJson);
|
||||||
|
|
||||||
m_model = new PipeWireGraphModel(controller, this);
|
m_model = new PipeWireGraphModel(controller, this);
|
||||||
m_model->loadLayout();
|
m_model->loadLayout();
|
||||||
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
|
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
|
||||||
m_view = new QtNodes::GraphicsView(m_scene);
|
m_view = new QtNodes::GraphicsView(m_scene);
|
||||||
|
m_scene->setBackgroundBrush(QColor(42, 45, 52));
|
||||||
|
|
||||||
|
auto *splitter = new QSplitter(this);
|
||||||
|
splitter->setOrientation(Qt::Horizontal);
|
||||||
|
splitter->addWidget(m_view);
|
||||||
|
|
||||||
|
auto *meterPanel = new QWidget(splitter);
|
||||||
|
auto *meterLayout = new QVBoxLayout(meterPanel);
|
||||||
|
meterLayout->setContentsMargins(12, 12, 12, 12);
|
||||||
|
meterLayout->setSpacing(10);
|
||||||
|
|
||||||
|
auto *meterLabel = new QLabel(QString("Master Meter"), meterPanel);
|
||||||
|
meterLayout->addWidget(meterLabel);
|
||||||
|
|
||||||
|
m_meter = new AudioLevelMeter(meterPanel);
|
||||||
|
meterLayout->addWidget(m_meter, 1);
|
||||||
|
meterLayout->addStretch();
|
||||||
|
|
||||||
|
meterPanel->setFixedWidth(140);
|
||||||
|
splitter->addWidget(meterPanel);
|
||||||
|
splitter->setStretchFactor(0, 1);
|
||||||
|
splitter->setStretchFactor(1, 0);
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
auto *layout = new QVBoxLayout(this);
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
layout->addWidget(m_view);
|
layout->addWidget(splitter);
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
||||||
connect(m_model, &PipeWireGraphModel::connectionCreated,
|
connect(m_model, &PipeWireGraphModel::connectionCreated,
|
||||||
|
|
@ -26,6 +88,8 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
||||||
|
|
||||||
connect(m_controller, &Potato::PipeWireController::nodeAdded,
|
connect(m_controller, &Potato::PipeWireController::nodeAdded,
|
||||||
this, &GraphEditorWidget::onNodeAdded);
|
this, &GraphEditorWidget::onNodeAdded);
|
||||||
|
connect(m_controller, &Potato::PipeWireController::nodeChanged,
|
||||||
|
this, &GraphEditorWidget::onNodeChanged);
|
||||||
connect(m_controller, &Potato::PipeWireController::nodeRemoved,
|
connect(m_controller, &Potato::PipeWireController::nodeRemoved,
|
||||||
this, &GraphEditorWidget::onNodeRemoved);
|
this, &GraphEditorWidget::onNodeRemoved);
|
||||||
connect(m_controller, &Potato::PipeWireController::linkAdded,
|
connect(m_controller, &Potato::PipeWireController::linkAdded,
|
||||||
|
|
@ -92,13 +156,28 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
||||||
m_model->saveLayout();
|
m_model->saveLayout();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
m_meterTimer = new QTimer(this);
|
||||||
|
m_meterTimer->setInterval(33);
|
||||||
|
m_meterTimer->setTimerType(Qt::PreciseTimer);
|
||||||
|
connect(m_meterTimer, &QTimer::timeout, this, &GraphEditorWidget::updateMeter);
|
||||||
|
m_meterTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isAudioEndpoint(const Potato::NodeInfo &node)
|
||||||
|
{
|
||||||
|
return node.mediaClass == Potato::MediaClass::AudioSink
|
||||||
|
|| node.mediaClass == Potato::MediaClass::AudioSource
|
||||||
|
|| node.mediaClass == Potato::MediaClass::AudioDuplex;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphEditorWidget::syncGraph()
|
void GraphEditorWidget::syncGraph()
|
||||||
{
|
{
|
||||||
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
|
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
|
||||||
for (const auto &node : nodes) {
|
for (const auto &node : nodes) {
|
||||||
m_model->addPipeWireNode(node);
|
if (isAudioEndpoint(node)) {
|
||||||
|
m_model->addPipeWireNode(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QVector<Potato::LinkInfo> links = m_controller->links();
|
const QVector<Potato::LinkInfo> links = m_controller->links();
|
||||||
|
|
@ -124,7 +203,20 @@ void GraphEditorWidget::refreshGraph()
|
||||||
|
|
||||||
void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node)
|
void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node)
|
||||||
{
|
{
|
||||||
m_model->addPipeWireNode(node);
|
if (isAudioEndpoint(node)) {
|
||||||
|
m_model->addPipeWireNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::onNodeChanged(const Potato::NodeInfo &node)
|
||||||
|
{
|
||||||
|
if (!isAudioEndpoint(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_model->updatePipeWireNode(node)) {
|
||||||
|
m_model->addPipeWireNode(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphEditorWidget::onNodeRemoved(uint32_t nodeId)
|
void GraphEditorWidget::onNodeRemoved(uint32_t nodeId)
|
||||||
|
|
@ -234,3 +326,13 @@ QString GraphEditorWidget::connectionKey(const QtNodes::ConnectionId &connection
|
||||||
+ QString(":") + QString::number(connectionId.inNodeId)
|
+ QString(":") + QString::number(connectionId.inNodeId)
|
||||||
+ QString(":") + QString::number(connectionId.inPortIndex);
|
+ QString(":") + QString::number(connectionId.inPortIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::updateMeter()
|
||||||
|
{
|
||||||
|
if (!m_meter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float peak = m_controller->meterPeak();
|
||||||
|
m_meter->setLevel(peak);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
class AudioLevelMeter;
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
class GraphEditorWidget : public QWidget
|
class GraphEditorWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -20,11 +23,13 @@ public:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onNodeAdded(const Potato::NodeInfo &node);
|
void onNodeAdded(const Potato::NodeInfo &node);
|
||||||
|
void onNodeChanged(const Potato::NodeInfo &node);
|
||||||
void onNodeRemoved(uint32_t nodeId);
|
void onNodeRemoved(uint32_t nodeId);
|
||||||
void onLinkAdded(const Potato::LinkInfo &link);
|
void onLinkAdded(const Potato::LinkInfo &link);
|
||||||
void onLinkRemoved(uint32_t linkId);
|
void onLinkRemoved(uint32_t linkId);
|
||||||
void onConnectionCreated(QtNodes::ConnectionId const connectionId);
|
void onConnectionCreated(QtNodes::ConnectionId const connectionId);
|
||||||
void onConnectionDeleted(QtNodes::ConnectionId const connectionId);
|
void onConnectionDeleted(QtNodes::ConnectionId const connectionId);
|
||||||
|
void updateMeter();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void syncGraph();
|
void syncGraph();
|
||||||
|
|
@ -40,4 +45,6 @@ private:
|
||||||
QSet<QString> m_ignoreDelete;
|
QSet<QString> m_ignoreDelete;
|
||||||
QMap<QString, uint32_t> m_connectionToLinkId;
|
QMap<QString, uint32_t> m_connectionToLinkId;
|
||||||
QMap<uint32_t, QString> m_linkIdToConnection;
|
QMap<uint32_t, QString> m_linkIdToConnection;
|
||||||
|
AudioLevelMeter *m_meter = nullptr;
|
||||||
|
QTimer *m_meterTimer = nullptr;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,56 @@
|
||||||
#include "PipeWireGraphModel.h"
|
#include "PipeWireGraphModel.h"
|
||||||
|
#include "PipeWireGraphModel.h"
|
||||||
|
|
||||||
#include <QtCore/QJsonArray>
|
#include <QtCore/QJsonArray>
|
||||||
#include <QtCore/QJsonDocument>
|
#include <QtCore/QJsonDocument>
|
||||||
#include <QtCore/QJsonObject>
|
#include <QtCore/QJsonObject>
|
||||||
#include <QtCore/QObject>
|
#include <QtCore/QObject>
|
||||||
#include <QtCore/QStandardPaths>
|
#include <QtCore/QStandardPaths>
|
||||||
|
#include <QtGui/QFont>
|
||||||
|
#include <QtGui/QFontMetrics>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
#include <QtNodes/StyleCollection>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int nodeWidthFor(const Potato::NodeInfo &info)
|
||||||
|
{
|
||||||
|
QFont captionFont;
|
||||||
|
captionFont.setBold(true);
|
||||||
|
QFontMetrics captionMetrics(captionFont);
|
||||||
|
int maxTextWidth = captionMetrics.horizontalAdvance(info.name);
|
||||||
|
|
||||||
|
QFont portFont;
|
||||||
|
QFontMetrics portMetrics(portFont);
|
||||||
|
for (const auto &port : info.inputPorts) {
|
||||||
|
maxTextWidth = std::max(maxTextWidth, portMetrics.horizontalAdvance(port.name));
|
||||||
|
}
|
||||||
|
for (const auto &port : info.outputPorts) {
|
||||||
|
maxTextWidth = std::max(maxTextWidth, portMetrics.horizontalAdvance(port.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int widthPadding = 140;
|
||||||
|
const int minWidth = 300;
|
||||||
|
const int maxWidth = 520;
|
||||||
|
const int width = maxTextWidth + widthPadding;
|
||||||
|
return std::max(minWidth, std::min(maxWidth, width));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString elideLabel(const QString &text, int width, const QFont &font)
|
||||||
|
{
|
||||||
|
QFontMetrics metrics(font);
|
||||||
|
const int available = std::max(60, width);
|
||||||
|
return metrics.elidedText(text, Qt::ElideRight, available);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PipeWireGraphModel::PipeWireGraphModel(Potato::PipeWireController *controller, QObject *parent)
|
PipeWireGraphModel::PipeWireGraphModel(Potato::PipeWireController *controller, QObject *parent)
|
||||||
: QtNodes::AbstractGraphModel()
|
: QtNodes::AbstractGraphModel()
|
||||||
, m_controller(controller)
|
, m_controller(controller)
|
||||||
|
|
@ -154,7 +191,12 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case QtNodes::NodeRole::Caption:
|
case QtNodes::NodeRole::Caption:
|
||||||
return info.name;
|
{
|
||||||
|
QFont captionFont;
|
||||||
|
captionFont.setBold(true);
|
||||||
|
const int width = nodeWidthFor(info) - 40;
|
||||||
|
return elideLabel(info.name, width, captionFont);
|
||||||
|
}
|
||||||
case QtNodes::NodeRole::CaptionVisible:
|
case QtNodes::NodeRole::CaptionVisible:
|
||||||
return true;
|
return true;
|
||||||
case QtNodes::NodeRole::Position: {
|
case QtNodes::NodeRole::Position: {
|
||||||
|
|
@ -165,13 +207,24 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole
|
||||||
return QPointF(0, 0);
|
return QPointF(0, 0);
|
||||||
}
|
}
|
||||||
case QtNodes::NodeRole::Size:
|
case QtNodes::NodeRole::Size:
|
||||||
return QSize(180, 80);
|
{
|
||||||
|
const int maxPorts = std::max(info.inputPorts.size(), info.outputPorts.size());
|
||||||
|
const int baseHeight = 70;
|
||||||
|
const int perPortHeight = 28;
|
||||||
|
const int height = std::max(110, baseHeight + (maxPorts * perPortHeight));
|
||||||
|
|
||||||
|
const int width = nodeWidthFor(info);
|
||||||
|
|
||||||
|
return QSize(width, height);
|
||||||
|
}
|
||||||
case QtNodes::NodeRole::InPortCount:
|
case QtNodes::NodeRole::InPortCount:
|
||||||
return static_cast<unsigned int>(info.inputPorts.size());
|
return static_cast<unsigned int>(info.inputPorts.size());
|
||||||
case QtNodes::NodeRole::OutPortCount:
|
case QtNodes::NodeRole::OutPortCount:
|
||||||
return static_cast<unsigned int>(info.outputPorts.size());
|
return static_cast<unsigned int>(info.outputPorts.size());
|
||||||
case QtNodes::NodeRole::Type:
|
case QtNodes::NodeRole::Type:
|
||||||
return QString("PipeWire");
|
return QString("PipeWire");
|
||||||
|
case QtNodes::NodeRole::Style:
|
||||||
|
return QtNodes::StyleCollection::nodeStyle().toJson().toVariantMap();
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
@ -217,11 +270,15 @@ QVariant PipeWireGraphModel::portData(QtNodes::NodeId nodeId,
|
||||||
if (role == QtNodes::PortRole::Caption) {
|
if (role == QtNodes::PortRole::Caption) {
|
||||||
if (portType == QtNodes::PortType::In) {
|
if (portType == QtNodes::PortType::In) {
|
||||||
if (portIndex < static_cast<QtNodes::PortIndex>(info.inputPorts.size())) {
|
if (portIndex < static_cast<QtNodes::PortIndex>(info.inputPorts.size())) {
|
||||||
return portLabel(info.inputPorts.at(portIndex));
|
QFont font;
|
||||||
|
const int width = nodeWidthFor(info) - 80;
|
||||||
|
return elideLabel(info.inputPorts.at(portIndex).name, width, font);
|
||||||
}
|
}
|
||||||
} else if (portType == QtNodes::PortType::Out) {
|
} else if (portType == QtNodes::PortType::Out) {
|
||||||
if (portIndex < static_cast<QtNodes::PortIndex>(info.outputPorts.size())) {
|
if (portIndex < static_cast<QtNodes::PortIndex>(info.outputPorts.size())) {
|
||||||
return portLabel(info.outputPorts.at(portIndex));
|
QFont font;
|
||||||
|
const int width = nodeWidthFor(info) - 80;
|
||||||
|
return elideLabel(info.outputPorts.at(portIndex).name, width, font);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -361,6 +418,19 @@ bool PipeWireGraphModel::findConnectionForLink(uint32_t linkId, QtNodes::Connect
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PipeWireGraphModel::updatePipeWireNode(const Potato::NodeInfo &node)
|
||||||
|
{
|
||||||
|
auto it = m_pwToNode.find(node.id);
|
||||||
|
if (it == m_pwToNode.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QtNodes::NodeId nodeId = it->second;
|
||||||
|
m_nodes[nodeId] = node;
|
||||||
|
Q_EMIT nodeUpdated(nodeId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const Potato::NodeInfo *PipeWireGraphModel::nodeInfo(QtNodes::NodeId nodeId) const
|
const Potato::NodeInfo *PipeWireGraphModel::nodeInfo(QtNodes::NodeId nodeId) const
|
||||||
{
|
{
|
||||||
auto it = m_nodes.find(nodeId);
|
auto it = m_nodes.find(nodeId);
|
||||||
|
|
@ -650,3 +720,4 @@ bool PipeWireGraphModel::viewState(double &scale, QPointF ¢er) const
|
||||||
center = m_viewCenter;
|
center = m_viewCenter;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
#include <QtNodes/StyleCollection>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ public:
|
||||||
bool findConnectionForLink(uint32_t linkId, QtNodes::ConnectionId &connectionId) const;
|
bool findConnectionForLink(uint32_t linkId, QtNodes::ConnectionId &connectionId) const;
|
||||||
const Potato::NodeInfo *nodeInfo(QtNodes::NodeId nodeId) const;
|
const Potato::NodeInfo *nodeInfo(QtNodes::NodeId nodeId) const;
|
||||||
bool connectionIdForLink(const Potato::LinkInfo &link, QtNodes::ConnectionId &connectionId) const;
|
bool connectionIdForLink(const Potato::LinkInfo &link, QtNodes::ConnectionId &connectionId) const;
|
||||||
|
bool updatePipeWireNode(const Potato::NodeInfo &node);
|
||||||
void reset();
|
void reset();
|
||||||
void loadLayout();
|
void loadLayout();
|
||||||
void saveLayout() const;
|
void saveLayout() const;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
|
@ -44,11 +45,6 @@ int main(int argc, char *argv[])
|
||||||
auto *screenshotAction = new QAction(&window);
|
auto *screenshotAction = new QAction(&window);
|
||||||
screenshotAction->setShortcut(QKeySequence(Qt::Key_F12));
|
screenshotAction->setShortcut(QKeySequence(Qt::Key_F12));
|
||||||
QObject::connect(screenshotAction, &QAction::triggered, [&window]() {
|
QObject::connect(screenshotAction, &QAction::triggered, [&window]() {
|
||||||
QScreen *screen = window.screen();
|
|
||||||
if (!screen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
|
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
|
||||||
+ QString("/potato");
|
+ QString("/potato");
|
||||||
QDir().mkpath(baseDir);
|
QDir().mkpath(baseDir);
|
||||||
|
|
@ -57,8 +53,17 @@ int main(int argc, char *argv[])
|
||||||
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));
|
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));
|
||||||
const QString filePath = baseDir + QString("/") + fileName;
|
const QString filePath = baseDir + QString("/") + fileName;
|
||||||
|
|
||||||
const QPixmap pixmap = screen->grabWindow(window.winId());
|
QPixmap pixmap = window.grab();
|
||||||
pixmap.save(filePath, "PNG");
|
if (pixmap.isNull()) {
|
||||||
|
QScreen *screen = window.screen();
|
||||||
|
if (screen) {
|
||||||
|
pixmap = screen->grabWindow(window.winId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pixmap.isNull()) {
|
||||||
|
pixmap.save(filePath, "PNG");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.addAction(screenshotAction);
|
window.addAction(screenshotAction);
|
||||||
|
|
||||||
|
|
@ -71,15 +76,21 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
const QString screenshotPath = parser.value(screenshotOption);
|
const QString screenshotPath = parser.value(screenshotOption);
|
||||||
if (!screenshotPath.isEmpty()) {
|
if (!screenshotPath.isEmpty()) {
|
||||||
QTimer::singleShot(500, &window, [&window, &parser, screenshotPath]() {
|
QTimer::singleShot(800, &window, [&window, &parser, screenshotPath]() {
|
||||||
QScreen *screen = window.screen();
|
const QFileInfo info(screenshotPath);
|
||||||
if (!screen) {
|
if (!info.absolutePath().isEmpty()) {
|
||||||
QCoreApplication::exit(2);
|
QDir().mkpath(info.absolutePath());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QPixmap pixmap = screen->grabWindow(window.winId());
|
QPixmap pixmap = window.grab();
|
||||||
if (!pixmap.save(screenshotPath)) {
|
if (pixmap.isNull()) {
|
||||||
|
QScreen *screen = window.screen();
|
||||||
|
if (screen) {
|
||||||
|
pixmap = screen->grabWindow(window.winId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixmap.isNull() || !pixmap.save(screenshotPath)) {
|
||||||
QCoreApplication::exit(3);
|
QCoreApplication::exit(3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/meters/AudioLevelMeter.cpp
Normal file
65
src/meters/AudioLevelMeter.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#include "AudioLevelMeter.h"
|
||||||
|
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
AudioLevelMeter::AudioLevelMeter(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setMinimumWidth(36);
|
||||||
|
setMinimumHeight(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioLevelMeter::setLevel(float level)
|
||||||
|
{
|
||||||
|
const float clamped = qBound(0.0f, level, 1.0f);
|
||||||
|
m_level = clamped;
|
||||||
|
if (clamped >= m_holdLevel) {
|
||||||
|
m_holdLevel = clamped;
|
||||||
|
m_holdFrames = 6;
|
||||||
|
} else if (m_holdFrames > 0) {
|
||||||
|
--m_holdFrames;
|
||||||
|
} else {
|
||||||
|
m_holdLevel = qMax(0.0f, m_holdLevel - m_decayPerFrame);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize AudioLevelMeter::sizeHint() const
|
||||||
|
{
|
||||||
|
return QSize(40, 160);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioLevelMeter::paintEvent(QPaintEvent *event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event)
|
||||||
|
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
|
||||||
|
const QRectF bounds = rect();
|
||||||
|
painter.fillRect(bounds, QColor(24, 24, 28));
|
||||||
|
|
||||||
|
const qreal height = bounds.height();
|
||||||
|
const qreal filled = height * m_level;
|
||||||
|
const QRectF barRect(bounds.left() + 6.0, bounds.bottom() - filled,
|
||||||
|
bounds.width() - 12.0, filled);
|
||||||
|
|
||||||
|
QColor color(50, 255, 120);
|
||||||
|
if (m_level > 0.9f) {
|
||||||
|
color = QColor(255, 70, 70);
|
||||||
|
} else if (m_level > 0.7f) {
|
||||||
|
color = QColor(255, 200, 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.fillRect(barRect, color);
|
||||||
|
|
||||||
|
if (m_holdLevel > 0.0f) {
|
||||||
|
const qreal holdY = bounds.bottom() - (height * m_holdLevel);
|
||||||
|
painter.setPen(QPen(QColor(240, 240, 240), 2));
|
||||||
|
painter.drawLine(QPointF(bounds.left() + 4.0, holdY),
|
||||||
|
QPointF(bounds.right() - 4.0, holdY));
|
||||||
|
}
|
||||||
|
painter.setPen(QColor(45, 45, 50));
|
||||||
|
painter.drawRect(bounds.adjusted(1, 1, -2, -2));
|
||||||
|
}
|
||||||
24
src/meters/AudioLevelMeter.h
Normal file
24
src/meters/AudioLevelMeter.h
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QSize>
|
||||||
|
|
||||||
|
class AudioLevelMeter : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AudioLevelMeter(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setLevel(float level);
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
float m_level = 0.0f;
|
||||||
|
float m_holdLevel = 0.0f;
|
||||||
|
int m_holdFrames = 0;
|
||||||
|
float m_decayPerFrame = 0.02f;
|
||||||
|
};
|
||||||
|
|
@ -25,13 +25,14 @@ enum class MediaClass {
|
||||||
|
|
||||||
struct PortInfo {
|
struct PortInfo {
|
||||||
uint32_t id;
|
uint32_t id;
|
||||||
|
uint32_t nodeId;
|
||||||
QString name;
|
QString name;
|
||||||
uint32_t direction;
|
uint32_t direction;
|
||||||
QString channelName;
|
QString channelName;
|
||||||
|
|
||||||
PortInfo() : id(0), direction(0) {}
|
PortInfo() : id(0), nodeId(0), direction(0) {}
|
||||||
PortInfo(uint32_t portId, const QString &portName, uint32_t portDir, const QString &channel = QString())
|
PortInfo(uint32_t portId, uint32_t owningNodeId, const QString &portName, uint32_t portDir, const QString &channel = QString())
|
||||||
: id(portId), name(portName), direction(portDir), channelName(channel) {}
|
: id(portId), nodeId(owningNodeId), name(portName), direction(portDir), channelName(channel) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NodeInfo {
|
struct NodeInfo {
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,15 @@
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include <pipewire/pipewire.h>
|
#include <pipewire/pipewire.h>
|
||||||
#include <pipewire/keys.h>
|
#include <pipewire/keys.h>
|
||||||
#include <pipewire/properties.h>
|
#include <pipewire/properties.h>
|
||||||
|
#include <pipewire/stream.h>
|
||||||
#include <spa/param/props.h>
|
#include <spa/param/props.h>
|
||||||
|
#include <spa/param/audio/format-utils.h>
|
||||||
|
#include <spa/param/audio/raw.h>
|
||||||
#include <spa/utils/dict.h>
|
#include <spa/utils/dict.h>
|
||||||
#include <spa/utils/type-info.h>
|
#include <spa/utils/type-info.h>
|
||||||
|
|
||||||
|
|
@ -110,6 +114,51 @@ static const struct pw_core_events core_events = []() {
|
||||||
return events;
|
return events;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
|
void meterProcess(void *data)
|
||||||
|
{
|
||||||
|
auto *self = static_cast<PipeWireController*>(data);
|
||||||
|
if (!self || !self->m_meterStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct pw_buffer *buf = pw_stream_dequeue_buffer(self->m_meterStream);
|
||||||
|
if (!buf || !buf->buffer || buf->buffer->n_datas == 0) {
|
||||||
|
if (buf) {
|
||||||
|
pw_stream_queue_buffer(self->m_meterStream, buf);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct spa_buffer *spaBuf = buf->buffer;
|
||||||
|
struct spa_data *data0 = &spaBuf->datas[0];
|
||||||
|
if (!data0->data || !data0->chunk) {
|
||||||
|
pw_stream_queue_buffer(self->m_meterStream, buf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t size = data0->chunk->size;
|
||||||
|
const float *samples = static_cast<const float*>(data0->data);
|
||||||
|
const uint32_t count = size / sizeof(float);
|
||||||
|
|
||||||
|
float peak = 0.0f;
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
const float value = std::fabs(samples[i]);
|
||||||
|
if (value > peak) {
|
||||||
|
peak = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self->m_meterPeak.store(peak, std::memory_order_relaxed);
|
||||||
|
pw_stream_queue_buffer(self->m_meterStream, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct pw_stream_events meter_events = []() {
|
||||||
|
struct pw_stream_events events{};
|
||||||
|
events.version = PW_VERSION_STREAM_EVENTS;
|
||||||
|
events.process = meterProcess;
|
||||||
|
return events;
|
||||||
|
}();
|
||||||
|
|
||||||
PipeWireController::PipeWireController(QObject *parent)
|
PipeWireController::PipeWireController(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
@ -169,6 +218,10 @@ bool PipeWireController::initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
pw_registry_add_listener(m_registry, m_registryListener, ®istry_events, this);
|
pw_registry_add_listener(m_registry, m_registryListener, ®istry_events, this);
|
||||||
|
|
||||||
|
if (!setupMeterStream()) {
|
||||||
|
qWarning() << "Failed to set up meter stream";
|
||||||
|
}
|
||||||
|
|
||||||
unlock();
|
unlock();
|
||||||
|
|
||||||
|
|
@ -201,6 +254,8 @@ void PipeWireController::shutdown()
|
||||||
pw_proxy_destroy(reinterpret_cast<struct pw_proxy*>(m_registry));
|
pw_proxy_destroy(reinterpret_cast<struct pw_proxy*>(m_registry));
|
||||||
m_registry = nullptr;
|
m_registry = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
teardownMeterStream();
|
||||||
|
|
||||||
if (m_core) {
|
if (m_core) {
|
||||||
pw_core_disconnect(m_core);
|
pw_core_disconnect(m_core);
|
||||||
|
|
@ -250,6 +305,11 @@ QVector<LinkInfo> PipeWireController::links() const
|
||||||
return m_links.values().toVector();
|
return m_links.values().toVector();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float PipeWireController::meterPeak() const
|
||||||
|
{
|
||||||
|
return m_meterPeak.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t PipeWireController::createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
uint32_t PipeWireController::createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
||||||
uint32_t inputNodeId, uint32_t inputPortId)
|
uint32_t inputNodeId, uint32_t inputPortId)
|
||||||
{
|
{
|
||||||
|
|
@ -421,6 +481,22 @@ void PipeWireController::handleNodeInfo(uint32_t id, const struct spa_dict *prop
|
||||||
|
|
||||||
node.mediaClass = NodeInfo::mediaClassFromString(mediaClassStr);
|
node.mediaClass = NodeInfo::mediaClassFromString(mediaClassStr);
|
||||||
node.type = NodeInfo::typeFromProperties(mediaClassStr, appNameStr);
|
node.type = NodeInfo::typeFromProperties(mediaClassStr, appNameStr);
|
||||||
|
|
||||||
|
{
|
||||||
|
QMutexLocker lock(&m_nodesMutex);
|
||||||
|
for (auto it = m_ports.cbegin(); it != m_ports.cend(); ++it) {
|
||||||
|
const PortInfo &port = it.value();
|
||||||
|
if (port.nodeId != id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port.direction == PW_DIRECTION_INPUT) {
|
||||||
|
node.inputPorts.append(port);
|
||||||
|
} else if (port.direction == PW_DIRECTION_OUTPUT) {
|
||||||
|
node.outputPorts.append(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
QMutexLocker lock(&m_nodesMutex);
|
QMutexLocker lock(&m_nodesMutex);
|
||||||
|
|
@ -460,22 +536,23 @@ void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *prop
|
||||||
QString portName = name ? toQString(name)
|
QString portName = name ? toQString(name)
|
||||||
: QString("port_") + QString::number(id);
|
: QString("port_") + QString::number(id);
|
||||||
|
|
||||||
PortInfo port(id, portName, direction);
|
uint32_t nodeId = nodeIdStr ? static_cast<uint32_t>(atoi(nodeIdStr)) : 0;
|
||||||
|
PortInfo port(id, nodeId, portName, direction);
|
||||||
|
|
||||||
{
|
{
|
||||||
QMutexLocker lock(&m_nodesMutex);
|
QMutexLocker lock(&m_nodesMutex);
|
||||||
m_ports.insert(id, port);
|
m_ports.insert(id, port);
|
||||||
|
|
||||||
if (nodeIdStr) {
|
if (nodeId != 0 && m_nodes.contains(nodeId)) {
|
||||||
uint32_t nodeId = static_cast<uint32_t>(atoi(nodeIdStr));
|
NodeInfo &node = m_nodes[nodeId];
|
||||||
if (m_nodes.contains(nodeId)) {
|
auto &ports = (direction == PW_DIRECTION_INPUT) ? node.inputPorts : node.outputPorts;
|
||||||
NodeInfo &node = m_nodes[nodeId];
|
for (int i = 0; i < ports.size(); ++i) {
|
||||||
if (direction == PW_DIRECTION_INPUT) {
|
if (ports.at(i).id == id) {
|
||||||
node.inputPorts.append(port);
|
ports.removeAt(i);
|
||||||
} else if (direction == PW_DIRECTION_OUTPUT) {
|
break;
|
||||||
node.outputPorts.append(port);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ports.append(port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -525,4 +602,62 @@ void PipeWireController::unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PipeWireController::setupMeterStream()
|
||||||
|
{
|
||||||
|
if (!m_threadLoop || !m_core) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct pw_properties *props = pw_properties_new(
|
||||||
|
PW_KEY_MEDIA_TYPE, "Audio",
|
||||||
|
PW_KEY_MEDIA_CATEGORY, "Capture",
|
||||||
|
PW_KEY_MEDIA_CLASS, "Audio/Source",
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
m_meterStream = pw_stream_new_simple(
|
||||||
|
pw_thread_loop_get_loop(m_threadLoop),
|
||||||
|
"Potato-Meter",
|
||||||
|
props,
|
||||||
|
&meter_events,
|
||||||
|
this);
|
||||||
|
|
||||||
|
if (!m_meterStream) {
|
||||||
|
pw_properties_free(props);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t buffer[512];
|
||||||
|
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
||||||
|
|
||||||
|
spa_audio_info_raw info{};
|
||||||
|
info.format = SPA_AUDIO_FORMAT_F32;
|
||||||
|
info.rate = 48000;
|
||||||
|
info.channels = 2;
|
||||||
|
info.position[0] = SPA_AUDIO_CHANNEL_FL;
|
||||||
|
info.position[1] = SPA_AUDIO_CHANNEL_FR;
|
||||||
|
|
||||||
|
const struct spa_pod *params[1];
|
||||||
|
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &info);
|
||||||
|
|
||||||
|
const int res = pw_stream_connect(
|
||||||
|
m_meterStream,
|
||||||
|
PW_DIRECTION_INPUT,
|
||||||
|
PW_ID_ANY,
|
||||||
|
static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
|
||||||
|
params,
|
||||||
|
1);
|
||||||
|
|
||||||
|
return res == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PipeWireController::teardownMeterStream()
|
||||||
|
{
|
||||||
|
if (!m_meterStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_stream_destroy(m_meterStream);
|
||||||
|
m_meterStream = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Potato
|
} // namespace Potato
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QAtomicInteger>
|
#include <QAtomicInteger>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
struct pw_thread_loop;
|
struct pw_thread_loop;
|
||||||
struct pw_context;
|
struct pw_context;
|
||||||
struct pw_core;
|
struct pw_core;
|
||||||
struct pw_registry;
|
struct pw_registry;
|
||||||
|
struct pw_stream;
|
||||||
struct spa_hook;
|
struct spa_hook;
|
||||||
struct spa_dict;
|
struct spa_dict;
|
||||||
|
|
||||||
|
|
@ -31,6 +33,7 @@ public:
|
||||||
QVector<NodeInfo> nodes() const;
|
QVector<NodeInfo> nodes() const;
|
||||||
NodeInfo nodeById(uint32_t id) const;
|
NodeInfo nodeById(uint32_t id) const;
|
||||||
QVector<LinkInfo> links() const;
|
QVector<LinkInfo> links() const;
|
||||||
|
float meterPeak() const;
|
||||||
|
|
||||||
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
||||||
uint32_t inputNodeId, uint32_t inputPortId);
|
uint32_t inputNodeId, uint32_t inputPortId);
|
||||||
|
|
@ -58,10 +61,13 @@ private:
|
||||||
friend void registryEventGlobalRemove(void *data, uint32_t id);
|
friend void registryEventGlobalRemove(void *data, uint32_t id);
|
||||||
friend void coreEventDone(void *data, uint32_t id, int seq);
|
friend void coreEventDone(void *data, uint32_t id, int seq);
|
||||||
friend void coreEventError(void *data, uint32_t id, int seq, int res, const char *message);
|
friend void coreEventError(void *data, uint32_t id, int seq, int res, const char *message);
|
||||||
|
friend void meterProcess(void *data);
|
||||||
|
|
||||||
void handleNodeInfo(uint32_t id, const struct ::spa_dict *props);
|
void handleNodeInfo(uint32_t id, const struct ::spa_dict *props);
|
||||||
void handlePortInfo(uint32_t id, const struct ::spa_dict *props);
|
void handlePortInfo(uint32_t id, const struct ::spa_dict *props);
|
||||||
void handleLinkInfo(uint32_t id, const struct ::spa_dict *props);
|
void handleLinkInfo(uint32_t id, const struct ::spa_dict *props);
|
||||||
|
bool setupMeterStream();
|
||||||
|
void teardownMeterStream();
|
||||||
|
|
||||||
void lock();
|
void lock();
|
||||||
void unlock();
|
void unlock();
|
||||||
|
|
@ -70,6 +76,7 @@ private:
|
||||||
pw_context *m_context = nullptr;
|
pw_context *m_context = nullptr;
|
||||||
pw_core *m_core = nullptr;
|
pw_core *m_core = nullptr;
|
||||||
pw_registry *m_registry = nullptr;
|
pw_registry *m_registry = nullptr;
|
||||||
|
pw_stream *m_meterStream = nullptr;
|
||||||
|
|
||||||
spa_hook *m_registryListener = nullptr;
|
spa_hook *m_registryListener = nullptr;
|
||||||
spa_hook *m_coreListener = nullptr;
|
spa_hook *m_coreListener = nullptr;
|
||||||
|
|
@ -81,6 +88,7 @@ private:
|
||||||
|
|
||||||
QAtomicInteger<bool> m_connected{false};
|
QAtomicInteger<bool> m_connected{false};
|
||||||
QAtomicInteger<bool> m_initialized{false};
|
QAtomicInteger<bool> m_initialized{false};
|
||||||
|
std::atomic<float> m_meterPeak{0.0f};
|
||||||
|
|
||||||
uint32_t m_nodeIdCounter = 0;
|
uint32_t m_nodeIdCounter = 0;
|
||||||
uint32_t m_linkIdCounter = 0;
|
uint32_t m_linkIdCounter = 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue