diff --git a/.sisyphus/notepads/warppipe-node-editor/learnings.md b/.sisyphus/notepads/warppipe-node-editor/learnings.md new file mode 100644 index 0000000..5d041e7 --- /dev/null +++ b/.sisyphus/notepads/warppipe-node-editor/learnings.md @@ -0,0 +1,345 @@ +## QtNodes (nodeeditor) Library Research - 2026-01-29 + +### Library Overview +- **Repository**: https://github.com/paceholder/nodeeditor +- **License**: BSD-3-Clause +- **Latest Version**: 3.0.x (major rewrite from v2.x) +- **Qt Support**: Qt 5.15+ and Qt 6.x +- **Documentation**: https://qtnodes.readthedocs.io/ + +### Key Architecture Concepts +1. **Model-View Approach**: Graph structure defined by classes derived from `AbstractGraphModel` +2. **Headless Mode**: Can create/modify graphs without GUI (useful for testing/processing) +3. **Two Main Workflows**: + - Pure graph visualization (AbstractGraphModel) + - Data propagation (DataFlowGraphModel) + +### CMake Integration +```cmake +# Build options +cmake .. -DUSE_QT6=on # or off for Qt5 +cmake .. -DBUILD_SHARED_LIBS=off # for static lib + +# Dependencies +- Qt >5.15 +- CMake 3.8+ +- Catch2 (for tests, optional) +``` + +### Creating Custom Node Types + +#### 1. Derive from NodeDelegateModel +```cpp +class MyCustomNode : public QtNodes::NodeDelegateModel { + Q_OBJECT +public: + QString name() const override { return "MyNodeType"; } + QString caption() const override { return "My Node"; } + + // Define ports + unsigned int nPorts(PortType portType) const override; + NodeDataType dataType(PortType portType, PortIndex index) const override; + + // Data handling + void setInData(std::shared_ptr data, PortIndex port) override; + std::shared_ptr outData(PortIndex port) override; + + // Optional: Embed Qt widget + QWidget* embeddedWidget() override { return _myWidget; } + + // Optional: Make resizable + bool resizable() const override { return true; } +}; +``` + +#### 2. Register with NodeDelegateModelRegistry +```cpp +auto registry = std::make_shared(); +registry->registerModel("Category"); +``` + +#### 3. Create DataFlowGraphModel +```cpp +auto model = std::make_shared(registry); +``` + +### Port Management + +#### Port Types +```cpp +enum class PortType { In, Out }; +``` + +#### Defining Ports +```cpp +unsigned int MyNode::nPorts(PortType portType) const { + if (portType == PortType::In) return 2; // 2 input ports + return 1; // 1 output port +} + +NodeDataType MyNode::dataType(PortType portType, PortIndex index) const { + return {"audio", "Audio Stream"}; // {id, name} +} +``` + +#### Dynamic Ports +Use these signals to add/remove ports at runtime: +```cpp +portsAboutToBeInserted(nodeId, PortType::Out, startIndex, endIndex); +// Modify underlying data +portsInserted(); + +portsAboutToBeDeleted(nodeId, PortType::In, startIndex, endIndex); +// Modify underlying data +portsDeleted(); +``` + +### Connection Management + +#### Connection Structure +```cpp +struct ConnectionId { + NodeId outNodeId; + PortIndex outPortIndex; + NodeId inNodeId; + PortIndex inPortIndex; +}; +``` + +#### Creating Connections +```cpp +// Programmatically +ConnectionId connId{sourceNode, 0, targetNode, 0}; +model->addConnection(connId); + +// Check if possible +if (model->connectionPossible(connId)) { + model->addConnection(connId); +} +``` + +#### Connection Policies +```cpp +enum class ConnectionPolicy { + One, // Only one connection per port + Many // Multiple connections allowed +}; + +// Return from nodeData with PortRole::ConnectionPolicyRole +``` + +### Data Propagation + +#### Data Flow Chain +1. Source node emits `dataUpdated(PortIndex)` +2. `DataFlowGraphModel::onOutPortDataUpdated()` triggered +3. Data fetched via `outData(portIndex)` +4. Data set to connected nodes via `setInData(data, portIndex)` +5. Signal `inPortDataWasSet()` emitted + +#### Custom Data Types +```cpp +class MyAudioData : public NodeData { +public: + NodeDataType type() const override { + return {"audio", "Audio Stream"}; + } + + // Your data members + std::vector samples; + int sampleRate; +}; +``` + +### Styling and Customization + +#### Style Classes +- `NodeStyle`: Node appearance (colors, borders, shadows) +- `ConnectionStyle`: Connection lines (colors, width) +- `GraphicsViewStyle`: Background grid colors + +#### Accessing Styles +```cpp +#include + +auto& nodeStyle = StyleCollection::nodeStyle(); +auto& connStyle = StyleCollection::connectionStyle(); +``` + +#### JSON Style Format +```json +{ + "NodeStyle": { + "NormalBoundaryColor": [255, 255, 255], + "SelectedBoundaryColor": [255, 165, 0], + "GradientColor0": "gray", + "ShadowColor": [20, 20, 20], + "FontColor": "white", + "ConnectionPointColor": [169, 169, 169], + "PenWidth": 1.0, + "Opacity": 0.8 + } +} +``` + +### Event Handling + +#### Context Menus +```cpp +// GraphicsView level +void MyGraphicsView::contextMenuEvent(QContextMenuEvent* event) override; + +// Node level - connect to signal +connect(scene, &BasicGraphicsScene::nodeContextMenu, + [](NodeId nodeId, QPointF scenePos) { + // Show custom menu + }); +``` + +#### Node Events +Available signals from BasicGraphicsScene: +- `nodeClicked(NodeId)` +- `nodeDoubleClicked(NodeId)` +- `nodeContextMenu(NodeId, QPointF)` +- `nodeHovered(NodeId, QPoint)` +- `nodeHoverLeft(NodeId)` + +### Scene Setup + +#### Basic Setup +```cpp +#include +#include +#include + +auto registry = registerMyNodes(); +auto model = std::make_shared(registry); +auto scene = new DataFlowGraphicsScene(*model); +auto view = new GraphicsView(scene); + +view->show(); +``` + +#### Undo/Redo Support +```cpp +// Scene has built-in QUndoStack +scene->undoStack().undo(); +scene->undoStack().redo(); + +// Keyboard shortcuts (built-in) +// Ctrl+Z: Undo +// Ctrl+Shift+Z: Redo +// Ctrl+D: Duplicate +``` + +### Serialization + +#### Save/Load Graph +```cpp +// Save +QJsonObject json = model->save(); +QJsonDocument doc(json); +QFile file("graph.json"); +file.open(QIODevice::WriteOnly); +file.write(doc.toJson()); + +// Load +QFile file("graph.json"); +file.open(QIODevice::ReadOnly); +QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); +model->load(doc.object()); +``` + +#### Custom Node Serialization +```cpp +QJsonObject MyNode::save() const override { + QJsonObject obj = NodeDelegateModel::save(); + obj["custom-data"] = _myCustomValue; + return obj; +} + +void MyNode::load(QJsonObject const& obj) override { + _myCustomValue = obj["custom-data"].toInt(); +} +``` + +### Layout Options + +#### Horizontal (Default) +``` + ------- +o | Caption +o | + ------- +``` + +#### Vertical +``` + -------o-------------o------- +| PortCaption PortCaption | +| Node Caption | +| PortCaption | + --------------o-------------- +``` + +Use `DefaultVerticalNodeGeometry` instead of `DefaultHorizontalNodeGeometry`. + +### Advanced Features + +#### Locked Nodes +```cpp +NodeFlags MyModel::nodeFlags(NodeId nodeId) const override { + auto flags = DataFlowGraphModel::nodeFlags(nodeId); + if (shouldLock) flags |= NodeFlag::Locked; + return flags; +} +``` + +#### Disable Connection Detaching +```cpp +bool MyModel::detachPossible(ConnectionId const& id) const override { + return false; // Prevent detaching +} +``` + +#### Embedded Widgets +```cpp +QWidget* MyNode::embeddedWidget() override { + if (!_widget) { + _widget = new QSlider(Qt::Horizontal); + connect(_widget, &QSlider::valueChanged, [this](int val) { + // Update and propagate data + Q_EMIT dataUpdated(0); + }); + } + return _widget; +} +``` + +### Example References +- `examples/calculator/`: Basic data flow with math operations +- `examples/dynamic_ports/`: Adding/removing ports at runtime +- `examples/styles/`: Custom styling +- `examples/vertical_layout/`: Vertical node layout +- `examples/lock_nodes_and_connections/`: Node locking +- `examples/headless_main.cpp`: Using without GUI + +### Key Classes Summary +- `AbstractGraphModel`: Base graph model +- `DataFlowGraphModel`: Graph with data propagation +- `NodeDelegateModel`: Custom node implementation +- `NodeDelegateModelRegistry`: Node type registration +- `BasicGraphicsScene`: Scene management +- `DataFlowGraphicsScene`: Scene with data flow +- `GraphicsView`: View widget +- `NodeData`: Base class for data types +- `StyleCollection`: Centralized styling + +### Important Notes for Audio Routing +1. Use `ConnectionPolicy::Many` for audio ports (multiple connections) +2. Consider using `std::shared_ptr` for data sharing +3. Implement proper sample rate/buffer size handling in data types +4. Use embedded widgets for gain controls, meters, etc. +5. Consider headless mode for audio processing thread +6. Implement proper serialization for saving/loading patches + diff --git a/CMakeLists.txt b/CMakeLists.txt index fd3eb05..d8bc2d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,3 +59,31 @@ if(WARPPIPE_BUILD_TESTS) target_compile_definitions(warppipe_tests PRIVATE WARPPIPE_TESTING) add_test(NAME warppipe_tests COMMAND warppipe_tests) endif() + +option(WARPPIPE_BUILD_GUI "Build warppipe Qt6 GUI application" ON) + +if(WARPPIPE_BUILD_GUI) + set(CMAKE_AUTOMOC ON) + set(CMAKE_AUTORCC ON) + set(CMAKE_AUTOUIC ON) + + find_package(Qt6 6.2 REQUIRED COMPONENTS Core Widgets) + + FetchContent_Declare( + QtNodes + GIT_REPOSITORY https://github.com/paceholder/nodeeditor + GIT_TAG master + ) + FetchContent_MakeAvailable(QtNodes) + + add_executable(warppipe-gui + gui/main.cpp + ) + + target_link_libraries(warppipe-gui PRIVATE + warppipe + Qt6::Core + Qt6::Widgets + QtNodes + ) +endif() diff --git a/GUI_PLAN.md b/GUI_PLAN.md index 1f98413..9723843 100644 --- a/GUI_PLAN.md +++ b/GUI_PLAN.md @@ -7,13 +7,13 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library. ## Milestones -- [ ] Milestone 0 - Qt6 Project Setup - - [ ] Create `gui/` subdirectory in warppipe project - - [ ] Add Qt6 + QtNodes to CMakeLists.txt (FetchContent for nodeeditor from github.com/paceholder/nodeeditor) - - [ ] Create `warppipe-gui` target with Qt6::Widgets and QtNodes dependencies - - [ ] Enable CMAKE_AUTOMOC, CMAKE_AUTORCC, CMAKE_AUTOUIC - - [ ] Create minimal main.cpp with QApplication + QMainWindow - - [ ] Verify GUI launches and shows empty window +- [x] Milestone 0 - Qt6 Project Setup + - [x] Create `gui/` subdirectory in warppipe project + - [x] Add Qt6 + QtNodes to CMakeLists.txt (FetchContent for nodeeditor from github.com/paceholder/nodeeditor) + - [x] Create `warppipe-gui` target with Qt6::Widgets and QtNodes dependencies + - [x] Enable CMAKE_AUTOMOC, CMAKE_AUTORCC, CMAKE_AUTOUIC + - [x] Create minimal main.cpp with QApplication + QMainWindow + - [x] Verify GUI launches and shows empty window - [ ] Milestone 1 - Core Model Integration - [ ] Create `WarpGraphModel : public QtNodes::AbstractGraphModel` diff --git a/gui/main.cpp b/gui/main.cpp new file mode 100644 index 0000000..d91f340 --- /dev/null +++ b/gui/main.cpp @@ -0,0 +1,15 @@ +#include +#include + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + QCoreApplication::setApplicationName("Warppipe"); + QCoreApplication::setApplicationVersion("0.1.0"); + + QMainWindow window; + window.setWindowTitle("Warppipe — Audio Router"); + window.resize(1280, 720); + window.show(); + + return app.exec(); +}