Skip to content

mecondev/node_editor

Repository files navigation

PyQt Node Editor

A portable, extensible framework for building node-based visual editors with PyQt5.

Python Version PyQt5 Tests License

What is this?

PyQt Node Editor is a complete Python framework for creating visual node-based programming interfaces. Think Blender's node editor, Unreal Engine Blueprints, or audio software like Max/MSP – but for your own applications.

The framework is designed to be portable: simply copy the node_editor/ folder into your project and start building custom node-based tools.

Key Features

  • Portable Package: Self-contained, copy-paste ready
  • Qt-Free Core: Domain logic independent of Qt runtime (graphics/widgets layers only)
  • Stable IDs (ULID): Sortable unique identifiers for reliable persistence
  • Theme Engine: Built-in dark/light themes with QSS stylesheet support
  • 52 Built-in Nodes: Math, logic, string, list, time, file I/O operations
  • Node Registry: Decorator-based registration with unique operation codes
  • Full Serialization: JSON save/load with IO-free snapshot format and undo/redo history
  • Edge Validators: Customizable connection rules
  • Interactive Tools: Edge dragging, rerouting, snapping, cut-line

Documentation

For detailed technical documentation see docs/architecture.md, which covers:

  • Layer architecture (Core, Graphics, Widgets)
  • Module dependencies and import hierarchy
  • Data flow and evaluation system
  • Theme system integration
  • Serialization format
  • Extension points for custom nodes, graphics, and validators

API Design

The framework follows these conventions:

  • Graphics attributes: All graphics item references use snake_case naming (e.g., graphics_view, graphics_socket, graphics_node).
  • Class factories: Graphics classes are injected via node_editor.core._init_graphics_classes() to avoid circular imports.
  • API methods: All public methods follow snake_case convention (e.g., mark_dirty(), get_input(), save_to_file()).
  • Scene state: Use the has_been_modified property to check unsaved changes.
  • Serialization: Socket objects must explicitly include the multi_edges boolean field.

Installation

Requirements

  • Python 3.8+
  • PyQt5 >= 5.15.0

Option 1: Copy into your project (Recommended)

# Clone the repo
git clone https://github.com/mecondev/node_editor.git

# Copy the portable package into your project
cp -r node_editor/node_editor /path/to/your/project/

Option 2: Install as package

pip install -r requirements.txt
pip install -e ".[dev]"  # For development dependencies

Quickstart

Minimal Example

import sys
from PyQt5.QtWidgets import QApplication
from node_editor import NodeEditorWindow

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = NodeEditorWindow()
    window.show()
    sys.exit(app.exec_())

Embedding in Your Application

from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QWidget
from node_editor import NodeEditorWidget

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        container = QWidget()
        layout = QVBoxLayout(container)
        
        self.editor = NodeEditorWidget()
        layout.addWidget(self.editor)
        
        self.setCentralWidget(container)

Creating Custom Nodes

from node_editor.core.node import Node
from node_editor.nodes import NodeRegistry

@NodeRegistry.register(200)  # Use op_codes >= 200 for custom nodes
class MyCustomNode(Node):
    """Custom node with one input and one output."""
    
    def __init__(self, scene):
        super().__init__(scene, "My Node", inputs=[1], outputs=[1])
        self.mark_dirty()
    
    def eval(self):
        """Evaluate the node."""
        input_val = self.get_input(0)
        if input_val is None:
            self.mark_invalid()
            return None
        
        # Your custom logic here
        result = input_val.eval() * 2
        
        self.value = result
        self.mark_dirty(False)
        self.mark_invalid(False)
        return result

Themes

Switching Themes

from node_editor.themes import ThemeEngine, DarkTheme, LightTheme

# Register themes (done automatically on first use)
ThemeEngine.register_theme(DarkTheme)
ThemeEngine.register_theme(LightTheme)

# List available themes
print(ThemeEngine.available_themes())  # ['dark', 'light']

# Switch theme
ThemeEngine.set_theme("light")

# Refresh existing graphics items after theme change
ThemeEngine.refresh_graphics_items(editor.scene)

# Get current theme for accessing colors
theme = ThemeEngine.current_theme()
print(theme.node_background)  # QColor object

Creating Custom Themes

Create a new theme by subclassing BaseTheme:

from node_editor.themes import BaseTheme, ThemeEngine
from PyQt5.QtGui import QColor

class MyTheme(BaseTheme):
    name = "mytheme"
    display_name = "My Custom Theme"
    
    scene_background = QColor("#1a1a2e")
    node_background = QColor("#16213e")
    node_title_background = QColor("#0f3460")
    # ... override other colors as needed

# Register and use
ThemeEngine.register_theme(MyTheme)
ThemeEngine.set_theme("mytheme")

Node System

Built-in Nodes (52 total)

Category Nodes Op Codes Module
Input NumberInput, TextInput 1-2 input_node.py
Output Output 3 output_node.py
Math Add, Subtract, Multiply, Divide 10-13 math_nodes.py
Math Extended Power, Sqrt, Abs, Min, Max, Round, Modulo 50-56 math_nodes.py
Comparison Equal, NotEqual, LessThan, LessEqual, GreaterThan, GreaterEqual 20-25 logic_nodes.py
Logic If, And, Or, Not, Xor 30, 60-63 logic_nodes.py
String Concatenate, Format, Length, Substring, Split 40-44 string_nodes.py
Conversion ToString, ToNumber, ToBool, ToInt 70-73 conversion_nodes.py
Utility Constant, Print, Comment, Clamp, Random 80-84 utility_nodes.py
List CreateList, GetItem, ListLength, Append, Join 90-94 list_nodes.py
Time CurrentTime, FormatDate, ParseDate, TimeDelta, CompareTime 100-104 time_nodes.py
Advanced RegexMatch, FileRead, FileWrite, HttpRequest 110-113 advanced_nodes.py

Node Registration Convention

  • 1-113: Used by built-in framework nodes
  • 200+: Recommended for custom application nodes
@NodeRegistry.register(200)  # Your custom op_code
class MyApplicationNode(Node):
    pass

Serialization

Save/Load Graphs

# Save to file
editor.scene.save_to_file("my_graph.json")

# Load from file
editor.scene.load_from_file("my_graph.json")

# Manual serialization
data = editor.scene.serialize()  # Returns OrderedDict with version field
editor.scene.deserialize(data)   # Restores state, handles version migrations

JSON Format

{
    "version": "2.0.0",
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "scene_width": 64000,
    "scene_height": 64000,
    "nodes": [
        {
            "sid": "01BX5ZZKBK6S0URNNZZ0BCZ7X0",
            "title": "Add",
            "pos_x": 100,
            "pos_y": 200,
            "inputs": [...],
            "outputs": [...],
            "content": {}
        }
    ],
    "edges": [
        {
            "sid": "01BX5ZZKBK6S0URNNZZ0C5Y9K1",
            "edge_type": 2,
            "start_sid": "01BX5ZZKBK6S0URNNZZ0BCZ7X0",
            "end_sid": "01BX5ZZKBK6S0URNNZZ0BCZZZ0"
        }
    ]
}

Format v2 Features:

  • sid (stable ID): ULID for reliable cross-session identification
  • IO-free snapshot: Serialization independent of file I/O operations
  • version field enables format migrations when needed

Running Examples

Calculator Example

A full-featured calculator application demonstrating MDI, custom nodes, and drag-and-drop:

cd examples/calculator
python main.py

Minimal Example

Basic NodeEditorWindow showcase:

cd examples/minimal
python main.py

Testing

# Run all tests
pytest

# Run with coverage
pytest --cov=node_editor

# Run specific test file
pytest tests/test_nodes_math.py

Test Status: 368 tests passing

Project Structure

node_editor/                    # Root repository
├── main.py                     # Demo entry point
├── config.py                   # App configuration
├── requirements.txt
├── pyproject.toml
│
├── node_editor/                # THE PORTABLE PACKAGE
│   ├── __init__.py             # Public API exports
│   │
│   ├── core/                   # Framework core
│   │   ├── node.py             # Node class
│   │   ├── edge.py             # Edge class
│   │   ├── socket.py           # Socket class
│   │   ├── scene.py            # Scene manager
│   │   ├── history.py          # Undo/redo
│   │   └── clipboard.py        # Copy/paste
│   │
│   ├── graphics/               # Qt graphics items
│   │   ├── view.py             # QGraphicsView
│   │   ├── scene.py            # QGraphicsScene
│   │   ├── node.py             # Node graphics
│   │   ├── edge.py             # Edge graphics
│   │   └── socket.py           # Socket graphics
│   │
│   ├── widgets/                # Embeddable widgets
│   │   ├── editor_widget.py    # NodeEditorWidget
│   │   ├── editor_window.py    # NodeEditorWindow
│   │   └── content_widget.py   # Node content base
│   │
│   ├── nodes/                  # Built-in nodes
│   │   ├── registry.py         # Node registration
│   │   ├── math_nodes.py       # Arithmetic
│   │   ├── logic_nodes.py      # Comparisons, boolean
│   │   ├── string_nodes.py     # String operations
│   │   └── ...                 # More node types
│   │
│   ├── themes/                 # Theme engine
│   │   ├── theme_engine.py     # Theme manager
│   │   ├── base_theme.py       # Base theme class
│   │   ├── dark/               # Dark theme
│   │   └── light/              # Light theme
│   │
│   ├── tools/                  # Interactive tools
│   │   ├── edge_dragging.py
│   │   ├── edge_validators.py
│   │   └── ...
│   │
│   └── utils/                  # Helpers
│       ├── qt_helpers.py
│       └── helpers.py
│
├── examples/                   # Example applications
│   ├── calculator/             # Full calculator app
│   └── minimal/                # Basic example
│
├── tests/                      # Unit tests
│
└── docs/                       # Documentation

Public API Summary

Core Classes

from node_editor import (
    Node,               # Base node class
    Edge,               # Connection class
    Socket,             # Connection point class
)

Widgets

from node_editor import (
    NodeEditorWidget,   # Embeddable canvas
    NodeEditorWindow,   # Full application window
)

Node System

from node_editor.nodes import (
    NodeRegistry,       # Registration system
    
    # Built-in nodes
    AddNode, SubtractNode, MultiplyNode, DivideNode,
    NumberInputNode, TextInputNode, OutputNode,
    # ... and 40+ more
)

Themes

from node_editor.themes import (
    ThemeEngine,        # Theme manager
    BaseTheme,          # For custom themes
    DarkTheme,          # Built-in dark
    LightTheme,         # Built-in light
)

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing)
  3. Write tests for your changes
  4. Ensure pytest and ruff check . pass
  5. Submit a pull request

License

MIT License - see LICENSE file for details.


Author: Michael Economou
Version: 1.0.0
Date: 2025-12-14

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages