✨ NodeLink Documentation

NodeLink is a Qt/QML node editor library for building node-based and dataflow-oriented applications. It provides a ready-made scene engine with customizable nodes, ports, and connections, while staying flexible enough to be integrated into complex systems and domain-specific tools.

This documentation gives you a high-level overview, a guided setup, and reference material for working with NodeLink from both QML and C++.

Who is this for?

NodeLink targets Qt developers who want to build:

  • Visual scripting tools
  • Dataflow editors (e.g., effects chains, ETL, processing graphs)
  • Node-based UIs (logic, shader graphs, workflows, automation, etc.)

What is NodeLink?

NodeLink is a Qt/QML-based framework for building node-based applications. It follows a Model-View-Controller (MVC) design, separating rendering and interaction (View) from the logical graph structure (Model).

The same model can be used in a headless mode (no visible node editor) or attached to one or more NodeLink views.

Key Features

NodeLink ships with a rich set of features for building production-grade node editors:

Production-ready architecture

NodeLink is designed for integration into real applications: it is modular, tested with Qt 6, and built to work with CMake-based projects across platforms (Linux, Windows, macOS).

πŸ“‹ Requirements

Supported Platforms

C++ / Build Toolchain

Qt / QML

πŸ“¦ Installation

Clone the repository (including submodules) and add it to your CMake project.

Clone

# Recommended (single step)
git clone --recursive https://github.com/Roniasoft/NodeLink.git
cd NodeLink

# If you cloned without --recursive
git submodule update --init --recursive

Integrate with CMake

In your existing CMake project, add NodeLink as a subdirectory and link it to your target.

# CMakeLists.txt (top-level or within your app)
add_subdirectory(NodeLink)

target_link_libraries(MyApp
    PRIVATE
        NodeLinkQml    # or the library target exported by NodeLink
)

Install vs. Subdirectory

You can either:

  • Install NodeLink system-wide (make install) and then find it via find_package, or
  • Include it as a subdirectory in your project for tighter version control.

πŸ› οΈ Building & Running

Command-line build (Linux / macOS)

mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install   # optional, if you want to install

Build with Qt Creator

  1. Open CMakeLists.txt as a project.
  2. Run CMake configuration.
  3. Build all targets.
  4. Run one of the example applications or your own.

Submodules

If you see missing headers or empty submodule folders, ensure you have run:

git submodule update --init --recursive

πŸš€ Quick Start: Your First Custom Node

In this section, we'll create a simple "HelloWorld" node that displays a message. This will take approximately 10 minutes.

Step 1: Create Project Directory (1 minute)

Create a new directory for your project:

mkdir HelloNodeLink
cd HelloNodeLink

Step 2: Create main.cpp (1 minute)

Create main.cpp:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQuickControls2/QQuickStyle>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    // Set Material style
    QQuickStyle::setStyle("Material");

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/HelloNodeLink/main.qml"));

    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);

    engine.load(url);

    return app.exec();
}

Step 3: Create HelloWorldNode.qml (2 minutes)

Create HelloWorldNode.qml:

import QtQuick
import NodeLink

Node {
    // Set unique type identifier
    type: 0

    // Configure node data
    nodeData: I_NodeData {}

    // Configure GUI
    guiConfig.width: 200
    guiConfig.height: 120
    guiConfig.color: "#4A90E2"

    // Custom property for input text
    property string inputText: ""

    // Add ports when node is created
    Component.onCompleted: addPorts();

    // Update output when input changes
    onInputTextChanged: {
        if (inputText.length > 0) {
            nodeData.data = "Hello, " + inputText;
        } else {
            nodeData.data = "";
        }
    }

    // Function to add ports
    function addPorts() {
        // Input port (left side)
        let inputPort = NLCore.createPort();
        inputPort.portType = NLSpec.PortType.Input;
        inputPort.portSide = NLSpec.PortPositionSide.Left;
        inputPort.title = "Input";
        inputPort.color = "#4A90E2";
        addPort(inputPort);

        // Output port (right side)
        let outputPort = NLCore.createPort();
        outputPort.portType = NLSpec.PortType.Output;
        outputPort.portSide = NLSpec.PortPositionSide.Right;
        outputPort.title = "Output";
        outputPort.color = "#7ED321";
        addPort(outputPort);
    }

    // Handle data from connected nodes
    function processInput() {
        // This will be called by the scene when data flows
        var inputPort = findPortByPortSide(NLSpec.PortPositionSide.Left);
        if (inputPort) {
            // Get data from connected node (simplified - actual implementation depends on your scene)
            // For now, we'll use the inputText property
            if (nodeData.input !== undefined && nodeData.input !== null) {
                inputText = String(nodeData.input);
            }
        }
    }
}

Step 4: Create main.qml (3 minutes)

Create main.qml:

import QtQuick
import QtQuickStream
import QtQuick.Controls
import NodeLink

Window {
    id: window

    // Scene property (will be overridden by load)
    property Scene scene: Scene { }

    // Node registry setup
    property NLNodeRegistry nodeRegistry: NLNodeRegistry {
        _qsRepo: NLCore.defaultRepo
        imports: ["HelloNodeLink", "NodeLink"]
        defaultNode: 0
    }

    width: 1280
    height: 960
    visible: true
    title: qsTr("Hello NodeLink - Your First Custom Node")
    color: "#1e1e1e"

    Material.theme: Material.Dark
    Material.accent: "#4890e2"

    Component.onCompleted: {
        // Register HelloWorldNode
        var nodeType = 0;
        nodeRegistry.nodeTypes[nodeType] = "HelloWorldNode";
        nodeRegistry.nodeNames[nodeType] = "Hello World";
        nodeRegistry.nodeIcons[nodeType] = "\uf075";  // Font Awesome comment icon
        nodeRegistry.nodeColors[nodeType] = "#4A90E2";

        // Initialize QtQuickStream repository
        NLCore.defaultRepo = NLCore.createDefaultRepo(["QtQuickStream", "NodeLink", "HelloNodeLink"])
        NLCore.defaultRepo.initRootObject("Scene");

        // Set registry to scene
        window.scene = Qt.binding(function() {
            return NLCore.defaultRepo.qsRootObject;
        });
        window.scene.nodeRegistry = Qt.binding(function() {
            return window.nodeRegistry;
        });
    }

    // Main view
    NLView {
        id: view
        scene: window.scene
        anchors.fill: parent
    }

    // Instructions label
    Rectangle {
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.margins: 20
        width: 300
        height: 150
        color: "#2d2d2d"
        radius: 5
        border.color: "#4890e2"
        border.width: 1

        Column {
            anchors.fill: parent
            anchors.margins: 10
            spacing: 5

            Text {
                text: "Instructions:"
                color: "#ffffff"
                font.bold: true
                font.pixelSize: 14
            }

            Text {
                text: "1. Right-click to create nodes"
                color: "#cccccc"
                font.pixelSize: 12
            }

            Text {
                text: "2. Drag from ports to connect"
                color: "#cccccc"
                font.pixelSize: 12
            }

            Text {
                text: "3. Your HelloWorld node is ready!"
                color: "#7ED321"
                font.pixelSize: 12
            }
        }
    }
}

Step 5: Create CMakeLists.txt (2 minutes)

Create CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)

# Require C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

# Configure Qt
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui QuickControls2 REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui QuickControls2 REQUIRED)

# Set QML import path
set(QML_IMPORT_PATH ${CMAKE_BINARY_DIR}/qml/NodeLink/resources/View)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)

# Add NodeLink as subdirectory (adjust path to your NodeLink installation)
# Option 1: If NodeLink is in parent directory
add_subdirectory(../NodeLink NodeLink)

# Option 2: If NodeLink is installed system-wide, use find_package instead
# find_package(NodeLink REQUIRED)

# Create executable
qt_add_executable(HelloNodeLink main.cpp)

# Define QML module
qt_add_qml_module(HelloNodeLink
    URI "HelloNodeLink"
    VERSION 1.0

    QML_FILES
        main.qml
        HelloWorldNode.qml

    SOURCES
)

# Include directories
target_include_directories(HelloNodeLink PUBLIC
    Qt${QT_VERSION_MAJOR}::QuickControls2)

# Link libraries
target_link_libraries(HelloNodeLink PRIVATE
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::QuickControls2
    NodeLinkplugin
    QtQuickStreamplugin
)

# Debug definitions
target_compile_definitions(HelloNodeLink
    PRIVATE $<$,$>:QT_QML_DEBUG>)

Important: Adjust the add_subdirectory path to point to your NodeLink installation directory.

Step 6: Build and Run (1 minute)

Using Command Line:

# Create build directory
mkdir build
cd build

# Configure
cmake ..

# Build
cmake --build . --config Release

# Run
./HelloNodeLink  # Linux/macOS
# or
HelloNodeLink.exe  # Windows

Using Qt Creator:

  1. CMakeLists.txt in Qt Creator
  2. Configure the project
  3. Build (Ctrl+B / Cmd+B)
  4. Run (Ctrl+R / Cmd+R)

Expected Result:

result

Calculator Example

Overview

The Calculator Example is a visual node-based calculator application built with NodeLink and Qt Quick. It demonstrates how to create a functional node graph system where users can connect different types of nodes to perform mathematical calculations. This example serves as an excellent introduction to building custom node-based applications using the NodeLink framework.

Calculator Example Overview

Calculator Example in Action

a. Purpose and Use Cases

Purpose

The Calculator Example demonstrates:

  1. Node-Based Data Flow: Shows how data flows through a network of connected nodes, where each node performs a specific operation or holds a value.

  2. Custom Node Types: Illustrates how to define and implement custom node types with specific behaviors, ports, and data handling.

  3. Real-Time Updates: Demonstrates automatic data propagation through the node graph when connections are made, modified, or removed.

  4. Visual Programming: Provides a visual interface for building mathematical expressions without writing code.

  5. Scene Management: Shows how to manage a scene with multiple nodes, links, and their relationships.

Use Cases

Example Scenarios

b. Node Types Explained

The Calculator Example implements six distinct node types, each serving a specific role in the calculation pipeline.

1. Source Node (SourceNode)

Purpose: Provides input values for calculations.

Type ID: CSpecs.NodeType.Source (0)

Properties:
- Contains a single output port named "value"
- Users can directly edit the numeric value in the node
- Acts as the starting point for data flow

Ports:
- Output Port: "value" (Right side) - Emits the numeric value entered by the user

Usage:
- Place Source nodes to input numbers into your calculation
- Connect the output port to operation nodes or Result nodes
- Edit the value by clicking on the node and typing a number

Example: A Source node with value 5 outputs 5 through its output port.


2. Additive Node (AdditiveNode)

Purpose: Performs addition of two input values.

Type ID: CSpecs.NodeType.Additive (1)

Properties:
- Inherits from OperationNode
- Has two input ports and one output port
- Performs: output = input1 + input2

Ports:
- Input Port 1: "input 1" (Left side)
- Input Port 2: "input 2" (Left side)
- Output Port: "value" (Right side) - Emits the sum of the two inputs

Behavior:
- Waits for both inputs to be connected and have valid data
- Automatically calculates the result when both inputs are available
- Outputs null if either input is missing

Example:
- Input 1: 3, Input 2: 7 β†’ Output: 10


3. Multiplier Node (MultiplierNode)

Purpose: Performs multiplication of two input values.

Type ID: CSpecs.NodeType.Multiplier (2)

Properties:
- Inherits from OperationNode
- Has two input ports and one output port
- Performs: output = input1 * input2

Ports:
- Input Port 1: "input 1" (Left side)
- Input Port 2: "input 2" (Left side)
- Output Port: "value" (Right side) - Emits the product of the two inputs

Behavior:
- Requires both inputs to be connected and have valid data
- Calculates result automatically when both inputs are available

Example:
- Input 1: 4, Input 2: 6 β†’ Output: 24


4. Subtraction Node (SubtractionNode)

Purpose: Performs subtraction of two input values.

Type ID: CSpecs.NodeType.Subtraction (3)

Properties:
- Inherits from OperationNode
- Has two input ports and one output port
- Performs: output = input1 - input2

Ports:
- Input Port 1: "input 1" (Left side)
- Input Port 2: "input 2" (Left side)
- Output Port: "value" (Right side) - Emits the difference of the two inputs

Behavior:
- Subtracts the second input from the first input
- Requires both inputs to be connected

Example:
- Input 1: 10, Input 2: 3 β†’ Output: 7


5. Division Node (DivisionNode)

Purpose: Performs division of two input values.

Type ID: CSpecs.NodeType.Division (4)

Properties:
- Inherits from OperationNode
- Has two input ports and one output port
- Performs: output = input1 / input2

Ports:
- Input Port 1: "input 1" (Left side)
- Input Port 2: "input 2" (Left side)
- Output Port: "value" (Right side) - Emits the quotient of the two inputs

Behavior:
- Divides the first input by the second input
- Handles division by zero: outputs "undefined (Divide by zero)" if the second input is 0
- Requires both inputs to be connected

Example:
- Input 1: 20, Input 2: 4 β†’ Output: 5
- Input 1: 10, Input 2: 0 β†’ Output: "undefined (Divide by zero)"


6. Result Node (ResultNode)

Purpose: Displays the final result of a calculation.

Type ID: CSpecs.NodeType.Result (5)

Properties:
- Contains a single input port
- Displays the value received from connected nodes
- Read-only (cannot be edited directly)

Ports:
- Input Port: "value" (Left side) - Receives the calculated result

Behavior:
- Displays whatever value is connected to its input port
- Updates automatically when the input value changes
- Resets to null when cloned or when the input connection is removed

Usage:
- Connect the output of any node (Source or Operation) to a Result node to view the final value
- Useful for displaying intermediate or final results in complex calculations

Example:
- Connect a Source node with value 42 β†’ Result displays 42
- Connect an Additive node output β†’ Result displays the sum


Node Type Summary Table

Node Type Type ID Input Ports Output Ports Operation
Source 0 0 1 Provides input value
Additive 1 2 1 Addition (+)
Multiplier 2 2 1 Multiplication (Γ—)
Subtraction 3 2 1 Subtraction (-)
Division 4 2 1 Division (/)
Result 5 1 0 Displays result

c. Step-by-Step Building Guide

This guide will walk you through building the Calculator Example from scratch, explaining each component and how they work together.

Prerequisites

Step 1: Project Setup

1.1 Create Project Structure

Create the following directory structure:

calculator/
β”œβ”€β”€ CMakeLists.txt
β”œβ”€β”€ main.cpp
β”œβ”€β”€ main.qml
└── resources/
    β”œβ”€β”€ Core/
    └── View/
    └── fonts/

1.2 Configure CMakeLists.txt

Create CMakeLists.txt with the following configuration:

cmake_minimum_required(VERSION 3.1.0)

set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Configure Qt
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui QuickControls2 REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui QuickControls2 REQUIRED)

list(APPEND QML_IMPORT_PATH ${CMAKE_BINARY_DIR}/qml)

# Create executable
qt_add_executable(Calculator main.cpp)

# Set CSpecs as singleton
set_source_files_properties(
    resources/Core/CSpecs.qml
    PROPERTIES
        QT_QML_SINGLETON_TYPE True
)

# Define QML module
qt_add_qml_module(Calculator
    URI "Calculator"
    VERSION 1.0
    QML_FILES
        main.qml
        resources/Core/CSpecs.qml
        resources/Core/SourceNode.qml
        resources/Core/OperationNode.qml
        resources/Core/ResultNode.qml
        resources/Core/AdditiveNode.qml
        resources/Core/MultiplierNode.qml
        resources/Core/SubtractionNode.qml
        resources/Core/DivisionNode.qml
        resources/Core/CalculatorScene.qml
        resources/Core/OperationNodeData.qml
        resources/View/CalculatorView.qml
        resources/View/CalculatorNodeView.qml
    RESOURCES
        resources/fonts/Font\ Awesome\ 6\ Pro-Thin-100.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Solid-900.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Regular-400.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Light-300.otf
)

target_include_directories(Calculator PUBLIC
    Qt${QT_VERSION_MAJOR}::QuickControls2)

target_link_libraries(Calculator PRIVATE
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::QuickControls2
    NodeLinkplugin
    QtQuickStreamplugin
)

Key Points:
- Links to NodeLinkplugin and QtQuickStreamplugin
- Sets CSpecs.qml as a singleton for global access
- Includes Font Awesome fonts for icons


Step 2: Create Specifications (CSpecs.qml)

Create resources/Core/CSpecs.qml - a singleton that defines node type constants:

pragma Singleton

import QtQuick

QtObject {
    enum NodeType {
        Source      = 0,
        Additive    = 1,
        Multiplier  = 2,
        Subtraction = 3,
        Division    = 4,
        Result      = 5,
        Operation   = 6,
        Unknown     = 99
    }

    enum OperationType {
        Additive     = 0,
        Multiplier   = 1,
        Subtraction  = 2,
        Division     = 3,
        Unknown = 99
    }
}

Purpose: Provides type-safe constants for node types used throughout the application.


Step 3: Create Node Data Models

3.1 OperationNodeData.qml

Create resources/Core/OperationNodeData.qml - data model for operation nodes:

import QtQuick
import NodeLink

I_NodeData {
    property var inputFirst:  null
    property var inputSecond: null
}

Purpose: Extends I_NodeData to store two input values for binary operations.


Step 4: Create Base Node Types

4.1 SourceNode.qml

Create resources/Core/SourceNode.qml:

import QtQuick
import NodeLink

Node {
    type: CSpecs.NodeType.Source
    nodeData: I_NodeData {}

    guiConfig.autoSize: true
    guiConfig.width: 150
    guiConfig.height: 100

    Component.onCompleted: addPorts();

    function addPorts() {
        let _port1 = NLCore.createPort();
        _port1.portType = NLSpec.PortType.Output
        _port1.portSide = NLSpec.PortPositionSide.Right
        _port1.title    = "value";
        addPort(_port1);
    }
}

Key Features:
- Single output port on the right side
- Auto-sized node with fixed dimensions
- Users can edit the value directly in the node view


4.2 OperationNode.qml (Base Class)

Create resources/Core/OperationNode.qml - base class for all operation nodes:

import QtQuick
import NodeLink

Node {
    property int operationType: CSpecs.OperationType.Additive

    type: CSpecs.NodeType.Operation
    nodeData: OperationNodeData {}

    guiConfig.autoSize: false
    guiConfig.minWidth: 150
    guiConfig.minHeight: 80
    guiConfig.baseContentWidth: 120

    Component.onCompleted: addPorts();

    function addPorts() {
        let _port1 = NLCore.createPort();
        let _port2 = NLCore.createPort();
        let _port3 = NLCore.createPort();

        _port1.portType = NLSpec.PortType.Input
        _port1.portSide = NLSpec.PortPositionSide.Left
        _port1.enable   = false;
        _port1.title    = "input 1";

        _port2.portType = NLSpec.PortType.Input
        _port2.portSide = NLSpec.PortPositionSide.Left
        _port2.enable   = false;
        _port2.title    = "input 2";

        _port3.portType = NLSpec.PortType.Output
        _port3.portSide = NLSpec.PortPositionSide.Right
        _port3.title    = "value";

        addPort(_port1);
        addPort(_port2);
        addPort(_port3);
    }
}

Key Features:
- Two input ports (left side) and one output port (right side)
- Uses OperationNodeData to store input values
- Base class for all arithmetic operations


4.3 ResultNode.qml

Create resources/Core/ResultNode.qml:

import QtQuick
import NodeLink

Node {
    type: CSpecs.NodeType.Result
    nodeData: I_NodeData {}

    guiConfig.autoSize: true
    guiConfig.width: 150
    guiConfig.height: 100

    Component.onCompleted: addPorts();

    onCloneFrom: function (baseNode) {
        nodeData.data = null;
    }

    function addPorts() {
        let _port1 = NLCore.createPort();
        _port1.portType = NLSpec.PortType.Input
        _port1.portSide = NLSpec.PortPositionSide.Left
        _port1.enable   = false;
        _port1.title    = "value";
        addPort(_port1);
    }
}

Key Features:
- Single input port (left side)
- Resets data when cloned
- Read-only display of results


Step 5: Create Specific Operation Nodes

5.1 AdditiveNode.qml

Create resources/Core/AdditiveNode.qml:

import QtQuick
import Calculator

OperationNode {
    operationType: CSpecs.OperationType.Additive

    function updataData() {
        if (!nodeData.inputFirst || !nodeData.inputSecond) {
            nodeData.data = null;
            return;
        }
        var input1 = parseFloat(nodeData.inputFirst);
        var input2 = parseFloat(nodeData.inputSecond)
        nodeData.data = input1 + input2;
    }
}

Key Features:
- Inherits from OperationNode
- Implements updataData() to perform addition
- Validates inputs before calculation


5.2 MultiplierNode.qml

Create resources/Core/MultiplierNode.qml:

import QtQuick
import Calculator

OperationNode {
    operationType: CSpecs.OperationType.Multiplier

    function updataData() {
        if (!nodeData.inputFirst || !nodeData.inputSecond) {
            nodeData.data = null;
            return;
        }
        var input1 = parseFloat(nodeData.inputFirst);
        var input2 = parseFloat(nodeData.inputSecond)
        nodeData.data = input1 * input2;
    }
}

5.3 SubtractionNode.qml

Create resources/Core/SubtractionNode.qml:

import QtQuick
import Calculator

OperationNode {
    operationType: CSpecs.OperationType.Subtraction

    function updataData() {
        if (!nodeData.inputFirst || !nodeData.inputSecond) {
            nodeData.data = null;
            return;
        }
        var input1 = parseFloat(nodeData.inputFirst);
        var input2 = parseFloat(nodeData.inputSecond)
        nodeData.data = input1 - input2;
    }
}

5.4 DivisionNode.qml

Create resources/Core/DivisionNode.qml:

import QtQuick
import Calculator

OperationNode {
    operationType: CSpecs.OperationType.Division

    function updataData() {
        if (!nodeData.inputFirst || !nodeData.inputSecond) {
            nodeData.data = null;
            return;
        }
        var input1 = parseFloat(nodeData.inputFirst);
        var input2 = parseFloat(nodeData.inputSecond)
        if (input2 !== 0)
            nodeData.data = input1 / input2;
        else
            nodeData.data = "undefined (Divide by zero)"
    }
}

Key Features:
- Includes division-by-zero handling
- Returns error message for invalid operations


Step 6: Create the Scene

6.1 CalculatorScene.qml

Create resources/Core/CalculatorScene.qml - the main scene that manages nodes and links:

import QtQuick
import QtQuick.Controls
import NodeLink
import Calculator

I_Scene {
    id: scene

    nodeRegistry: NLNodeRegistry {
        _qsRepo: scene._qsRepo
        imports: ["Calculator"]
        defaultNode: CSpecs.NodeType.Source

        nodeTypes: [
            CSpecs.NodeType.Source      = "SourceNode",
            CSpecs.NodeType.Additive    = "AdditiveNode",
            CSpecs.NodeType.Multiplier  = "MultiplierNode",
            CSpecs.NodeType.Subtraction = "SubtractionNode",
            CSpecs.NodeType.Division    = "DivisionNode",
            CSpecs.NodeType.Result      = "ResultNode"
        ];

        nodeNames: [
            CSpecs.NodeType.Source      = "Source",
            CSpecs.NodeType.Additive    = "Additive",
            CSpecs.NodeType.Multiplier  = "Multiplier",
            CSpecs.NodeType.Subtraction = "Subtraction",
            CSpecs.NodeType.Division    = "Division",
            CSpecs.NodeType.Result      = "Result"
        ];

        nodeIcons: [
            CSpecs.NodeType.Source      = "\ue4e2",
            CSpecs.NodeType.Additive    = "+",
            CSpecs.NodeType.Multiplier  = "\uf00d",
            CSpecs.NodeType.Subtraction = "-",
            CSpecs.NodeType.Division    = "/",
            CSpecs.NodeType.Result      = "\uf11b",
        ];

        nodeColors: [
            CSpecs.NodeType.Source     = "#444",
            CSpecs.NodeType.Additive    = "#444",
            CSpecs.NodeType.Multiplier  = "#444",
            CSpecs.NodeType.Subtraction = "#444",
            CSpecs.NodeType.Division    = "#444",
            CSpecs.NodeType.Result      = "#444",
        ];
    }

    selectionModel: SelectionModel {
        existObjects: [...Object.keys(nodes), ...Object.keys(links)]
    }

    property UndoCore _undoCore: UndoCore {
        scene: scene
    }

    // Update node data when links/nodes change
    onLinkRemoved: _upateDataTimer.start();
    onNodeRemoved: _upateDataTimer.start();
    onLinkAdded:   updateData();

    property Timer _upateDataTimer: Timer {
        repeat: false
        running: false
        interval: 1
        onTriggered: scene.updateData();
    }

    // Create a node with specific type and position
    function createCustomizeNode(nodeType: int, xPos: real, yPos: real): string {
        var title = nodeRegistry.nodeNames[nodeType] + "_" +
                   (Object.values(scene.nodes).filter(node => node.type === nodeType).length + 1);
        return createSpecificNode(nodeRegistry.imports, nodeType,
                                 nodeRegistry.nodeTypes[nodeType],
                                 nodeRegistry.nodeColors[nodeType],
                                 title, xPos, yPos);
    }

    // Link validation and creation
    function linkNodes(portA: string, portB: string) {
        if (!canLinkNodes(portA, portB)) {
            console.error("[Scene] Cannot link Nodes");
            return;
        }
        let link = Object.values(links).find(conObj =>
            conObj.inputPort._qsUuid === portA &&
            conObj.outputPort._qsUuid === portB);
        if (link === undefined)
            createLink(portA, portB);
    }

    // Validation rules for linking
    function canLinkNodes(portA: string, portB: string): bool {
        // ... (validation logic - see source file)
        return true;
    }

    // Update all node data based on connections
    function updateData() {
        var notReadyLinks = [];

        // Initialize node data
        Object.values(nodes).forEach(node => {
            switch (node.type) {
                case CSpecs.NodeType.Additive:
                case CSpecs.NodeType.Multiplier:
                case CSpecs.NodeType.Subtraction:
                case CSpecs.NodeType.Division: {
                    node.nodeData.data = null;
                    node.nodeData.inputFirst = null;
                    node.nodeData.inputSecond = null
                } break;
                case CSpecs.NodeType.Result: {
                    node.nodeData.data = null;
                } break;
            }
        });

        // Process links and update data
        Object.values(links).forEach(link => {
            // ... (data propagation logic)
        });

        // Handle nodes waiting for multiple inputs
        while (notReadyLinks.length > 0) {
            // ... (process remaining links)
        }
    }

    // Update specific node data
    function upadateNodeData(upstreamNode: Node, downStreamNode: Node) {
        switch (downStreamNode.type) {
            case CSpecs.NodeType.Additive:
            case CSpecs.NodeType.Multiplier:
            case CSpecs.NodeType.Subtraction:
            case CSpecs.NodeType.Division: {
                if (!downStreamNode.nodeData.inputFirst)
                    downStreamNode.nodeData.inputFirst = upstreamNode.nodeData.data;
                else if (!downStreamNode.nodeData.inputSecond)
                    downStreamNode.nodeData.inputSecond = upstreamNode.nodeData.data;
                downStreamNode.updataData();
            } break;
            case CSpecs.NodeType.Result: {
                downStreamNode.nodeData.data = upstreamNode.nodeData.data;
            } break;
        }
    }
}

Key Features:
- Node Registry: Defines all available node types, names, icons, and colors
- Link Validation: Ensures valid connections (no cycles, single input per port, etc.)
- Data Propagation: Automatically updates node values when connections change
- Undo Support: Integrated undo/redo functionality

Data Flow Logic:
1. When a link is added/removed, updateData() is called
2. All operation nodes are reset
3. Data flows from Source nodes through operation nodes
4. Operation nodes wait for both inputs before calculating
5. Results propagate to connected nodes


Step 7: Create Views

7.1 CalculatorNodeView.qml

Create resources/View/CalculatorNodeView.qml - custom view for displaying nodes:

import QtQuick
import QtQuick.Controls
import NodeLink
import Calculator

NodeView {
    id: nodeView

    contentItem: Item {
        id: mainContentItem
        property bool iconOnly: ((node?.operationType ?? -1) > -1) ||
                                nodeView.isNodeMinimal

        // Header with icon and title
        Item {
            id: titleItem
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.top: parent.top
            anchors.margins: 12
            visible: !mainContentItem.iconOnly
            height: 20

            Text {
                id: iconText
                font.family: NLStyle.fontType.font6Pro
                font.pixelSize: 20
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                text: scene.nodeRegistry.nodeIcons[node.type]
                color: node.guiConfig.color
            }

            NLTextArea {
                id: titleTextArea
                anchors.right: parent.right
                anchors.left: iconText.right
                anchors.verticalCenter: parent.verticalCenter
                anchors.leftMargin: 5
                height: 40
                readOnly: !nodeView.edit
                placeholderText: qsTr("Enter title")
                color: NLStyle.primaryTextColor
                text: node.title
                onTextChanged: {
                    if (node && node.title !== text)
                        node.title = text;
                }
            }
        }

        // Value display/input field
        NLTextField {
            id: textArea
            anchors.top: titleItem.bottom
            anchors.right: parent.right
            anchors.bottom: parent.bottom
            anchors.left: parent.left
            anchors.margins: 12
            anchors.topMargin: 5
            visible: !mainContentItem.iconOnly
            placeholderText: qsTr("Number")
            color: NLStyle.primaryTextColor
            text: node?.nodeData?.data
            readOnly: !nodeView.edit || (node.type === CSpecs.NodeType.Result)
            validator: DoubleValidator {}
            onTextChanged: {
                if (node && (node.nodeData?.data ?? "") !== text) {
                    if (node.type === CSpecs.NodeType.Source) {
                        node.nodeData.data = text;
                        scene.updateData();
                    }
                }
            }
        }

        // Minimal view (icon only at low zoom)
        Rectangle {
            id: minimalRectangle
            anchors.fill: parent
            anchors.margins: 10
            color: mainContentItem.iconOnly ? "#282828" : "transparent"
            radius: NLStyle.radiusAmount.nodeView

            Text {
                font.family: NLStyle.fontType.font6Pro
                font.pixelSize: 60
                anchors.centerIn: parent
                text: scene.nodeRegistry.nodeIcons[node.type]
                color: node.guiConfig.color
                visible: mainContentItem.iconOnly
            }
        }
    }
}

Key Features:
- Editable Title: Users can rename nodes
- Value Display: Shows node data (read-only for Result, editable for Source)
- Minimal Mode: Shows icon only when zoomed out
- Validation: Uses DoubleValidator for numeric input


7.2 CalculatorView.qml

Create resources/View/CalculatorView.qml - main view container:

import QtQuick
import QtQuick.Controls
import NodeLink
import QtQuickStream
import Calculator

Item {
    id: view
    property CalculatorScene scene

    property SceneSession sceneSession: SceneSession {
        enabledOverview: false;
        doNodesNeedImage: false
    }

    // Nodes Scene (flickable canvas)
    NodesScene {
        id: nodesScene
        anchors.fill: parent
        scene: view.scene
        sceneSession: view.sceneSession
        sceneContent: NodesRect {
            scene: view.scene
            sceneSession: view.sceneSession
            nodeViewComponent: Qt.createComponent("CalculatorNodeView.qml")
        }
    }

    // Side menu for adding nodes
    SideMenu {
        scene: view.scene
        sceneSession: view.sceneSession
        anchors.right: parent.right
        anchors.rightMargin: 45
        anchors.top: parent.top
        anchors.topMargin: 50
    }
}

Key Features:
- NodesScene: Provides the scrollable canvas for nodes
- SideMenu: Allows users to add new nodes to the scene
- SceneSession: Manages view state and interactions


Step 8: Create Main Application

8.1 main.cpp

Create main.cpp:

#include <QtGui/QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>

int main(int argc, char* argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    // Set Material style
    QQuickStyle::setStyle("Material");

    // Import all items into QML engine
    engine.addImportPath(":/");

    const QUrl url(u"qrc:/Calculator/main.qml"_qs);
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

Key Features:
- Uses Material design style
- Loads QML resources from the Calculator module
- Standard Qt Quick application setup


8.2 main.qml

Create main.qml:

import QtQuick
import QtQuick.Dialogs
import QtQuick.Controls

import QtQuickStream
import NodeLink
import Calculator

Window {
    id: window
    property CalculatorScene scene: null

    width: 1280
    height: 960
    visible: true
    title: qsTr("Calculator Example")
    color: "#1e1e1e"

    Material.theme: Material.Dark
    Material.accent: "#4890e2"

    Component.onCompleted: {
        // Create root object
        NLCore.defaultRepo = NLCore.createDefaultRepo(["QtQuickStream", "Calculator"])
        NLCore.defaultRepo.initRootObject("CalculatorScene");
        window.scene = Qt.binding(function() {
            return NLCore.defaultRepo.qsRootObject;
        });
    }

    // Load Font Awesome fonts
    FontLoader { source: "qrc:/Calculator/resources/fonts/Font Awesome 6 Pro-Thin-100.otf" }
    FontLoader { source: "qrc:/Calculator/resources/fonts/Font Awesome 6 Pro-Solid-900.otf" }
    FontLoader { source: "qrc:/Calculator/resources/fonts/Font Awesome 6 Pro-Regular-400.otf" }
    FontLoader { source: "qrc:/Calculator/resources/fonts/Font Awesome 6 Pro-Light-300.otf" }

    // Main view
    CalculatorView {
        id: view
        scene: window.scene
        anchors.fill: parent
    }
}

Key Features:
- Dark Theme: Material Dark theme with custom accent color
- Scene Initialization: Creates the CalculatorScene using QtQuickStream
- Font Loading: Loads Font Awesome icons
- Full-Screen View: CalculatorView fills the window


Step 9: Build and Run

9.1 Configure Build

  1. Create a build directory:
    bash mkdir build cd build

  2. Configure with CMake:
    bash cmake .. -DCMAKE_PREFIX_PATH=<Qt_Install_Path>

  3. Build the project:
    bash cmake --build .

9.2 Run the Application

Run the executable:

./Calculator  # Linux/Mac
Calculator.exe  # Windows

Step 10: Using the Calculator

Basic Usage

  1. Add Nodes: Click the side menu to add nodes (Source, Additive, Multiplier, etc.)

  2. Set Values:

    • Click on a Source node
    • Type a number in the value field
    • Press Enter or click outside
  3. Connect Nodes:

    • Click and drag from an output port (right side)
    • Release on an input port (left side)
    • A link will be created if the connection is valid
  4. View Results:

    • Connect operation nodes to a Result node
    • The Result node displays the calculated value
    • Values update automatically when inputs change

Example Calculation: (5 + 3) Γ— 2

  1. Add two Source nodes, set values to 5 and 3
  2. Add an Additive node
  3. Connect Source (5) β†’ Additive input 1
  4. Connect Source (3) β†’ Additive input 2
  5. Add a Source node with value 2
  6. Add a Multiplier node
  7. Connect Additive output β†’ Multiplier input 1
  8. Connect Source (2) β†’ Multiplier input 2
  9. Add a Result node
  10. Connect Multiplier output β†’ Result input
  11. Result displays 16

Architecture Overview

Data Flow


Source Node β†’ Operation Node β†’ Result Node
     ↓                              ↓                       ↓
  (value)              (input1, input2)      (result)

            

Component Hierarchy

CalculatorScene (I_Scene)
β”œβ”€β”€ NodeRegistry (defines node types)
β”œβ”€β”€ SelectionModel (manages selection)
β”œβ”€β”€ UndoCore (undo/redo support)
└── Nodes & Links
    β”œβ”€β”€ SourceNode
    β”œβ”€β”€ OperationNodes (Additive, Multiplier, etc.)
    └── ResultNode

CalculatorView
β”œβ”€β”€ NodesScene (canvas)
    └── NodesRect (renders nodes)
        └── CalculatorNodeView (custom node UI)
└── SideMenu (add nodes)

Key Concepts

Ports

Data Propagation

  1. Source nodes provide initial values
  2. Operation nodes wait for both inputs before calculating
  3. Results propagate downstream automatically
  4. Updates trigger when:
    • Links are added/removed
    • Source node values change
    • Nodes are deleted

Node Lifecycle

  1. Creation: Node created via createCustomizeNode()
  2. Initialization: Ports added in Component.onCompleted
  3. Connection: Links established via linkNodes()
  4. Update: Data calculated in updataData()
  5. Deletion: Cleanup handled by scene

Extending the Calculator

Adding New Operations

To add a new operation (e.g., Power/Exponentiation):

  1. Add to CSpecs.qml:
    qml enum NodeType { // ... existing types Power = 6 }

  2. Create PowerNode.qml:
    ```qml
    import QtQuick
    import Calculator

    OperationNode {
    operationType: CSpecs.OperationType.Power
    function updataData() {
    if (!nodeData.inputFirst || !nodeData.inputSecond) {
    nodeData.data = null;
    return;
    }
    var base = parseFloat(nodeData.inputFirst);
    var exponent = parseFloat(nodeData.inputSecond);
    nodeData.data = Math.pow(base, exponent);
    }
    }
    ```

  3. Register in CalculatorScene.qml:
    qml nodeTypes: [ // ... existing CSpecs.NodeType.Power = "PowerNode" ]

Customizing Appearance

Troubleshooting

Common Issues

  1. Nodes not updating: Check that updateData() is called after link changes
  2. Invalid connections: Verify canLinkNodes() validation logic
  3. Division by zero: Handled in DivisionNode.qml - displays error message
  4. Missing fonts: Ensure Font Awesome fonts are in resources/fonts/

Debug Tips

Conclusion

The Calculator Example demonstrates the core concepts of building node-based applications with NodeLink:

Use this example as a foundation for building more complex node-based applications such as:
- Visual programming languages
- Data processing pipelines
- Workflow builders
- Shader editors
- Logic circuit simulators

For more examples, see the other examples in the NodeLink repository.

Logic Circuit

Overview

The Logic Circuit Example demonstrates how to build a visual digital logic circuit simulator using NodeLink. This example allows users to create, connect, and test digital logic gates (AND, OR, NOT) in a visual node-based interface. Users can toggle input switches and observe how signals propagate through the circuit in real-time, making it an excellent tool for learning digital logic, designing circuits, and understanding boolean algebra.

Logic Circuit Overview

Logic Circuit Example

a. Purpose and Use Cases

Purpose

The Logic Circuit Example demonstrates:

  1. Visual Circuit Design: Create digital logic circuits using a visual node-based interface without writing code.

  2. Real-Time Signal Propagation: Observe how boolean signals (true/false) flow through gates and update outputs instantly.

  3. Standard Logic Gates: Implement fundamental logic gates (AND, OR, NOT) with their standard symbols and truth tables.

  4. Interactive Input/Output: Toggle input switches and see immediate results on output indicators.

  5. Educational Tool: Learn digital logic concepts, boolean algebra, and circuit design principles visually.

  6. Gate Symbol Rendering: Display logic gates using standard digital circuit symbols drawn with Canvas.

Use Cases

Example Scenarios

Real-World Applications

Use Case Diagram

b. Node Types Explained

The Logic Circuit Example implements five distinct node types, each representing a fundamental component of digital logic circuits.

1. Input Node (InputNode)

Purpose: Provides boolean input values (ON/OFF) to the circuit through an interactive toggle switch.

Type ID: LSpecs.NodeType.Input (0)

Properties:
- Contains a single output port
- Has an interactive toggle switch that can be clicked
- Outputs true (ON) or false (OFF) boolean values
- Acts as the starting point for all signals in the circuit

Ports:
- Output Port: (Right side) - Emits the current state (true/false)

Properties:
- nodeData.currentState: Boolean value representing ON (true) or OFF (false)
- nodeData.output: Output value (same as currentState)
- nodeData.displayValue: String representation ("ON" or "OFF")

Behavior:
- Initializes to OFF (false) state
- Clicking the switch toggles between ON and OFF
- When toggled, updates the circuit by calling scene.updateLogic()
- Visual indicator shows green for ON, gray for OFF

Visual Appearance:
- Icon: Lightning bolt (⚑)
- Toggle Switch: Rounded rectangle with ON/OFF text
- Color: Green (#A6E3A1) when ON, Gray (#585B70) when OFF
- Interactive: Clickable to toggle state

Usage Example:
- Click Input node β†’ Toggles from OFF to ON
- Output port emits true
- Connected gates receive the signal

Input Node

Truth Table:
| Input State | Output |
|-------------|--------|
| OFF | false |
| ON | true |


2. AND Gate (AndNode)

Purpose: Performs logical AND operation - output is true only when both inputs are true.

Type ID: LSpecs.NodeType.AND (1)

Properties:
- Has two input ports and one output port
- Implements boolean AND logic: output = inputA && inputB
- Uses standard AND gate symbol (flat left side, curved right side)

Ports:
- Input Port A: (Left side, top)
- Input Port B: (Left side, bottom)
- Output Port: (Right side)

Properties:
- nodeData.inputA: First input value (boolean or null)
- nodeData.inputB: Second input value (boolean or null)
- nodeData.output: Result of AND operation (boolean or null)

Behavior:
- Waits for both inputs to be connected and have valid values
- Calculates: output = inputA && inputB
- Output is null if either input is missing
- Updates automatically when inputs change

Visual Appearance:
- Icon: AND symbol (∧)
- Gate Symbol: Standard AND gate shape (flat left, curved right)
- Drawn using Canvas with white fill and black border
- Color: Black border (#000000)

Usage Example:
- Input A: true, Input B: true β†’ Output: true
- Input A: true, Input B: false β†’ Output: false
- Input A: false, Input B: true β†’ Output: false
- Input A: false, Input B: false β†’ Output: false

AND Gate

Truth Table:
| Input A | Input B | Output |
|---------|---------|--------|
| false | false | false |
| false | true | false |
| true | false | false |
| true | true | true |

Code Implementation:

function updateData() {
    if (nodeData.inputA !== null && nodeData.inputB !== null) {
        nodeData.output = nodeData.inputA && nodeData.inputB;
    } else {
        nodeData.output = null;
    }
}

3. OR Gate (OrNode)

Purpose: Performs logical OR operation - output is true when at least one input is true.

Type ID: LSpecs.NodeType.OR (2)

Properties:
- Has two input ports and one output port
- Implements boolean OR logic: output = inputA || inputB
- Uses standard OR gate symbol (curved shape on both sides)

Ports:
- Input Port A: (Left side, top)
- Input Port B: (Left side, bottom)
- Output Port: (Right side)

Properties:
- nodeData.inputA: First input value (boolean or null)
- nodeData.inputB: Second input value (boolean or null)
- nodeData.output: Result of OR operation (boolean or null)

Behavior:
- Waits for both inputs to be connected and have valid values
- Calculates: output = inputA || inputB
- Output is null if either input is missing
- Updates automatically when inputs change

Visual Appearance:
- Icon: OR symbol (∨)
- Gate Symbol: Standard OR gate shape (curved on both sides)
- Drawn using Canvas with white fill and black border
- Color: Black border (#000000)

Usage Example:
- Input A: true, Input B: true β†’ Output: true
- Input A: true, Input B: false β†’ Output: true
- Input A: false, Input B: true β†’ Output: true
- Input A: false, Input B: false β†’ Output: false

OR Gate

Truth Table:
| Input A | Input B | Output |
|---------|---------|--------|
| false | false | false |
| false | true | true |
| true | false | true |
| true | true | true |

Code Implementation:

function updateData() {
    if (nodeData.inputA !== null && nodeData.inputB !== null) {
        nodeData.output = nodeData.inputA || nodeData.inputB;
    } else {
        nodeData.output = null;
    }
}

4. NOT Gate (NotNode)

Purpose: Performs logical NOT operation (inversion) - output is the opposite of the input.

Type ID: LSpecs.NodeType.NOT (3)

Properties:
- Has one input port and one output port
- Implements boolean NOT logic: output = !inputA
- Uses standard NOT gate symbol (triangle with bubble)

Ports:
- Input Port: (Left side)
- Output Port: (Right side)

Properties:
- nodeData.inputA: Input value (boolean or null)
- nodeData.output: Result of NOT operation (boolean or null)

Behavior:
- Waits for input to be connected and have a valid value
- Calculates: output = !inputA
- Output is null if input is missing
- Updates automatically when input changes

Visual Appearance:
- Icon: NOT symbol (~)
- Gate Symbol: Triangle pointing right with a bubble (circle) on the output
- Drawn using Canvas with white fill and black border
- Color: Black border (#000000)

Usage Example:
- Input: true β†’ Output: false
- Input: false β†’ Output: true

NOT Gate

Truth Table:
| Input | Output |
|-------|--------|
| false | true |
| true | false |

Code Implementation:

function updateData() {
    if (nodeData.inputA !== null) {
        nodeData.output = !nodeData.inputA;
    } else {
        nodeData.output = null;
    }
}

5. Output Node (OutputNode)

Purpose: Displays the final result of the logic circuit as a visual indicator (lamp).

Type ID: LSpecs.NodeType.Output (4)

Properties:
- Contains a single input port
- Displays the boolean value received from connected gates
- Shows visual indicator: Green (ON), Red (OFF), or Gray (UNDEFINED)

Ports:
- Input Port: (Left side) - Receives the calculated result

Properties:
- nodeData.inputA: Input value from connected gate (boolean or null)
- nodeData.displayValue: String representation ("ON", "OFF", or "UNDEFINED")
- nodeData.statusColor: Color for the indicator (green, red, or gray)

Behavior:
- Receives input from connected gate or node
- Displays "ON" (green) when input is true
- Displays "OFF" (red) when input is false
- Displays "UNDEFINED" (gray) when input is null or not connected
- Updates automatically when input changes

Visual Appearance:
- Icon: Circle (β—‹)
- Indicator: Circular lamp with colored inner circle
- Colors:
- Green (#4CAF50) for ON (true)
- Red (#F44336) for OFF (false)
- Gray (#9E9E9E) for UNDEFINED (null)
- Smooth color transitions when state changes

Usage Example:
- Connected to AND gate output β†’ Shows result of AND operation
- Connected to OR gate output β†’ Shows result of OR operation
- Not connected β†’ Shows "UNDEFINED" (gray)

Output Node

Display States:
| Input Value | Display | Color |
|-------------|---------|-------|
| true | "ON" | Green (#4CAF50) |
| false | "OFF" | Red (#F44336) |
| null | "UNDEFINED" | Gray (#9E9E9E) |

Code Implementation:

function updateDisplay(value) {
    if (value === null) {
        nodeData.displayValue = "UNDEFINED";
    } else {
        nodeData.displayValue = value ? "ON" : "OFF";
    }
    nodeData.statusColor = getStatusColor(value);
}

function getStatusColor(value) {
    if (value === null) return "#9E9E9E"; // Gray
    return value ? "#4CAF50" : "#F44336"; // Green or Red
}

Node Type Summary Table

Node Type Type ID Input Ports Output Ports Operation Symbol
Input 0 0 1 Provides boolean input ⚑
AND 1 2 1 Logical AND (∧) ∧
OR 2 2 1 Logical OR (∨) ∨
NOT 3 1 1 Logical NOT (~) ~
Output 4 1 0 Displays result β—‹

Data Flow Architecture

The logic circuit follows a signal propagation pattern:

Input Node (ON/OFF)
    ↓
Logic Gates (AND/OR/NOT)
    ↓
Output Node (Visual Indicator)

Signals propagate through the circuit in real-time, updating all downstream gates and outputs automatically.

Data Flow Diagram /p>

c. Step-by-Step Building Guide

This guide will walk you through building the Logic Circuit Example from scratch, explaining each component and how they work together.

Prerequisites

Step 1: Project Setup

1.1 Create Project Structure

Create the following directory structure:

logicCircuit/
β”œβ”€β”€ CMakeLists.txt
β”œβ”€β”€ main.cpp
β”œβ”€β”€ main.qml
└── resources/
    β”œβ”€β”€ Core/
    β”œβ”€β”€ View/
    └── fonts/

1.2 Configure CMakeLists.txt

Create CMakeLists.txt with the following configuration:

cmake_minimum_required(VERSION 3.1.0)

set(MODULE_NAME LogicCircuit)

set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Configure Qt
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui QuickControls2 REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui QuickControls2 REQUIRED)

list(APPEND QML_IMPORT_PATH ${CMAKE_BINARY_DIR}/qml)

# Create executable
qt_add_executable(${MODULE_NAME} main.cpp)

# Set LSpecs as singleton
set_source_files_properties(
    resources/Core/LSpecs.qml
    PROPERTIES
        QT_QML_SINGLETON_TYPE True
)

# Define QML module
qt_add_qml_module(${MODULE_NAME}
    URI "LogicCircuit"
    VERSION 1.0
    QML_FILES
        main.qml
        resources/Core/LSpecs.qml
        resources/Core/LogicCircuitScene.qml
        resources/Core/InputNode.qml
        resources/Core/LogicNode.qml
        resources/Core/AndNode.qml
        resources/Core/OrNode.qml
        resources/Core/NotNode.qml
        resources/Core/OutputNode.qml
        resources/Core/LogicNodeData.qml
        resources/View/LogicCircuitView.qml
        resources/View/LogicCircuitNodeView.qml
    RESOURCES
        resources/fonts/Font\ Awesome\ 6\ Pro-Thin-100.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Solid-900.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Regular-400.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Light-300.otf
)

target_include_directories(${MODULE_NAME} PUBLIC
    Qt${QT_VERSION_MAJOR}::QuickControls2)

target_link_libraries(${MODULE_NAME} PRIVATE
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::QuickControls2
    NodeLinkplugin
    QtQuickStreamplugin
)

Key Points:
- Links to NodeLinkplugin and QtQuickStreamplugin
- Sets LSpecs.qml as a singleton for global access
- Includes Font Awesome fonts for icons
- Includes LogicNode.qml as a base class


Step 2: Create Specifications (LSpecs.qml)

Create resources/Core/LSpecs.qml - a singleton that defines node type constants:

pragma Singleton
import QtQuick

QtObject {
    enum NodeType {
        Input   = 0,
        AND     = 1,
        OR      = 2,
        NOT     = 3,
        Output  = 4,
        Logic   = 5,
        Unknown = 99
    }

    enum OperationType {
        AND     = 0,
        OR      = 1,
        NOT     = 2,
        Unknown = 99
    }

    enum BooleanState {
        FALSE = 0,
        TRUE  = 1,
        UNDEFINED = 2
    }
}

Purpose: Provides type-safe constants for node types, operations, and boolean states.


Step 3: Create Node Data Models

3.1 LogicNodeData.qml

Create resources/Core/LogicNodeData.qml - data model for logic nodes:

import QtQuick
import NodeLink

I_NodeData {
    // Input values for logic gates
    property var inputA: null
    property var inputB: null

    // Output value
    property var output: null

    // For InputNode: current state (true=ON, false=OFF)
    property bool currentState: false

    // For display purposes
    property string displayValue: "OFF"

    // Status color for visualization (used by OutputNode)
    property color statusColor: "#9E9E9E"
}

Purpose: Extends I_NodeData to store input/output values and display information for logic gates.


Step 4: Create Base Node Type

4.1 LogicNode.qml

Create resources/Core/LogicNode.qml - base class for all logic nodes:

import QtQuick
import NodeLink
import LogicCircuit

Node {
    id: root
    property int nodeType: LSpecs.NodeType.AND

    type: LSpecs.NodeType.Logic
    nodeData: LogicNodeData {}

    property var logicScene: null

    guiConfig.autoSize: false
    guiConfig.minWidth: 20
    guiConfig.minHeight: 20

    Component.onCompleted: {
        addPorts();
    }

    function addPorts() {
        if (nodeType == LSpecs.NodeType.OR || nodeType == LSpecs.NodeType.AND) {
            addPortsInput();
            addPortsInput();
            addPortsOutput();
        } else if (nodeType == LSpecs.NodeType.Input) {
            addPortsOutput();
        } else if (nodeType == LSpecs.NodeType.Output) {
            addPortsInput();
        } else if (nodeType == LSpecs.NodeType.NOT) {
            addPortsInput();
            addPortsOutput();
        }
    }

    function addPortsInput() {
        let inputPort = NLCore.createPort();
        inputPort.portType = NLSpec.PortType.Input;
        inputPort.portSide = NLSpec.PortPositionSide.Left;
        inputPort.title = "";
        addPort(inputPort);
    }

    function addPortsOutput() {
        let outputPort = NLCore.createPort();
        outputPort.portType = NLSpec.PortType.Output;
        outputPort.portSide = NLSpec.PortPositionSide.Right;
        outputPort.title = "";
        addPort(outputPort);
    }
}

Key Features:
- Base class for all logic nodes
- Dynamically adds ports based on node type
- AND/OR gates: 2 inputs, 1 output
- NOT gate: 1 input, 1 output
- Input node: 1 output
- Output node: 1 input


Step 5: Create Specific Node Types

5.1 InputNode.qml

Create resources/Core/InputNode.qml:

import QtQuick
import NodeLink
import LogicCircuit

LogicNode {
    nodeType: LSpecs.NodeType.Input

    Component.onCompleted: {
        nodeData.currentState = false;
        nodeData.output = false;
        nodeData.displayValue = "OFF";
    }

    function toggleState() {
        nodeData.currentState = !nodeData.currentState;
        nodeData.output = nodeData.currentState;
        nodeData.displayValue = nodeData.currentState ? "ON" : "OFF";

        // Update the entire circuit
        var mainScene = _qsRepo ? _qsRepo.qsRootObject : null;
        if (mainScene && mainScene.updateLogic) {
            mainScene.updateLogic();
        }
    }
}

Key Features:
- Toggleable switch for ON/OFF state
- Updates circuit when toggled
- Initializes to OFF state


5.2 AndNode.qml

Create resources/Core/AndNode.qml:

import QtQuick
import NodeLink
import LogicCircuit

LogicNode {
    nodeType: LSpecs.NodeType.AND

    function updateData() {
        if (nodeData.inputA !== null && nodeData.inputB !== null) {
            nodeData.output = nodeData.inputA && nodeData.inputB;
        } else {
            nodeData.output = null;
        }
    }
}

Key Features:
- Implements AND logic: output = inputA && inputB
- Returns null if inputs are incomplete


5.3 OrNode.qml

Create resources/Core/OrNode.qml:

import QtQuick
import NodeLink
import LogicCircuit

LogicNode {
    nodeType: LSpecs.NodeType.OR

    function updateData() {
        if (nodeData.inputA !== null && nodeData.inputB !== null) {
            nodeData.output = nodeData.inputA || nodeData.inputB;
        } else {
            nodeData.output = null;
        }
    }
}

Key Features:
- Implements OR logic: output = inputA || inputB
- Returns null if inputs are incomplete


5.4 NotNode.qml

Create resources/Core/NotNode.qml:

import QtQuick
import NodeLink
import LogicCircuit

LogicNode {
    nodeType: LSpecs.NodeType.NOT

    function updateData() {
        if (nodeData.inputA !== null) {
            nodeData.output = !nodeData.inputA;
        } else {
            nodeData.output = null;
        }
    }
}

Key Features:
- Implements NOT logic: output = !inputA
- Returns null if input is missing


5.5 OutputNode.qml

Create resources/Core/OutputNode.qml:

import QtQuick
import NodeLink
import LogicCircuit

LogicNode {
    nodeType: LSpecs.NodeType.Output


property color undefinedColor: "#9E9E9E"
property color offColor: "#F44336"
property color onColor: "#4CAF50"

Component.onCompleted: {
    guiConfig.color = "#2A2A2A";
    updateDisplay(nodeData.inputA);
}

function updateDisplay(value) {
    if (value === null) {
        nodeData.displayValue = "UNDEFINED";
    } else {
        nodeData.displayValue = value ? "ON" : "OFF";
    }
    nodeData.statusColor = getStatusColor(value);
}

function getStatusColor(value) {
    if (value === null) return undefinedColor;
    return value ? onColor : offColor;
}

function updateData() {
    updateDisplay(nodeData.inputA);
}

Key Features:
- Displays input value as colored indicator
- Green for ON, Red for OFF, Gray for UNDEFINED
- Smooth color transitions


Step 6: Create the Scene

6.1 LogicCircuitScene.qml

Create resources/Core/LogicCircuitScene.qml - the main scene that manages nodes, links, and signal propagation:

import QtQuick
import QtQuick.Controls
import NodeLink
import LogicCircuit

I_Scene {
    id: scene

    property color borderColor: "#000000"

    nodeRegistry: NLNodeRegistry {
        _qsRepo: scene._qsRepo
        imports: ["LogicCircuit"]
        defaultNode: LSpecs.NodeType.Input

        nodeTypes: [
            LSpecs.NodeType.Input   = "InputNode",
            LSpecs.NodeType.AND     = "AndNode",
            LSpecs.NodeType.OR      = "OrNode",
            LSpecs.NodeType.NOT     = "NotNode",
            LSpecs.NodeType.Output  = "OutputNode"
        ]

        nodeNames: [
            LSpecs.NodeType.Input   = "Input",
            LSpecs.NodeType.AND     = "AND Gate",
            LSpecs.NodeType.OR      = "OR Gate",
            LSpecs.NodeType.NOT     = "NOT Gate",
            LSpecs.NodeType.Output  = "Output"
        ]

        nodeIcons: [
            LSpecs.NodeType.Input   = "⚑",
            LSpecs.NodeType.AND     = "∧",
            LSpecs.NodeType.OR      = "∨",
            LSpecs.NodeType.NOT     = "~",
            LSpecs.NodeType.Output  = "β—‹"
        ]

        nodeColors: [
            LSpecs.NodeType.Input   = borderColor,
            LSpecs.NodeType.AND     = borderColor,
            LSpecs.NodeType.OR      = borderColor,
            LSpecs.NodeType.NOT     = borderColor,
            LSpecs.NodeType.Output  = borderColor
        ]
    }

    selectionModel: SelectionModel {
        existObjects: [...Object.keys(nodes), ...Object.keys(links)]
    }

    property UndoCore _undoCore: UndoCore {
        scene: scene
    }

    // Update logic when connections change
    onLinkRemoved: updateLogic();
    onNodeRemoved: updateLogic();
    onLinkAdded: updateLogic();

    function createCustomizeNode(nodeType, xPos, yPos) {
        var title = nodeRegistry.nodeNames[nodeType] + "_" +
                   (Object.values(scene.nodes).filter(node => node.type === nodeType).length + 1);
        return createSpecificNode(nodeRegistry.imports, nodeType,
                                 nodeRegistry.nodeTypes[nodeType],
                                 nodeRegistry.nodeColors[nodeType],
                                 title, xPos, yPos);
    }

    // Update all logic in the circuit
    function updateLogic() {
        // Reset all operation nodes
        Object.values(nodes).forEach(node => {
            if (node.type === LSpecs.NodeType.AND ||
                node.type === LSpecs.NodeType.OR ||
                node.type === LSpecs.NodeType.NOT) {
                node.nodeData.inputA = null;
                node.nodeData.inputB = null;
                node.nodeData.output = null;
            }
            if (node.type === LSpecs.NodeType.Output) {
                node.nodeData.inputA = null;
                node.nodeData.displayValue = "UNDEFINED";
            }
        });

        // Track connections to prevent same upstream node connecting to multiple inputs
        var connectionMap = {};

        // Propagate values through the circuit (iterative approach)
        var maxIterations = 999;
        var changed = true;

        for (var i = 0; i < maxIterations && changed; i++) {
            changed = false;

            Object.values(links).forEach(link => {
                var upstreamNode = findNode(link.inputPort._qsUuid);
                var downstreamNode = findNode(link.outputPort._qsUuid);

                if (upstreamNode && downstreamNode && upstreamNode.nodeData.output !== null) {
                    var connectionKey = downstreamNode._qsUuid + "_" + upstreamNode._qsUuid;

                    // Handle 2-input gates (AND, OR)
                    if (downstreamNode.type === LSpecs.NodeType.AND ||
                        downstreamNode.type === LSpecs.NodeType.OR) {

                        if (downstreamNode.nodeData.inputA === null && !connectionMap[connectionKey + "_A"]) {
                            downstreamNode.nodeData.inputA = upstreamNode.nodeData.output;
                            connectionMap[connectionKey + "_A"] = true;
                            changed = true;
                        } else if (downstreamNode.nodeData.inputB === null && !connectionMap[connectionKey + "_B"]) {
                            // Ensure inputB comes from a different node than inputA
                            var inputAUpstream = null;
                            Object.keys(connectionMap).forEach(key => {
                                if (key.startsWith(downstreamNode._qsUuid) && key.endsWith("_A")) {
                                    var upstreamId = key.split("_")[1];
                                    inputAUpstream = upstreamId;
                                }
                            });

                            if (inputAUpstream !== upstreamNode._qsUuid) {
                                downstreamNode.nodeData.inputB = upstreamNode.nodeData.output;
                                connectionMap[connectionKey + "_B"] = true;
                                changed = true;
                            }
                        }

                    // Handle single-input gates (NOT, Output)
                    } else if (downstreamNode.type === LSpecs.NodeType.NOT ||
                              downstreamNode.type === LSpecs.NodeType.Output) {
                        if (downstreamNode.nodeData.inputA === null) {
                            downstreamNode.nodeData.inputA = upstreamNode.nodeData.output;
                            changed = true;
                        }
                    }

                    // Update downstream node if it has all required inputs
                    if (downstreamNode.updateData) {
                        var canUpdate = false;
                        switch(downstreamNode.type) {
                            case LSpecs.NodeType.AND:
                            case LSpecs.NodeType.OR:
                                canUpdate = (downstreamNode.nodeData.inputA !== null &&
                                           downstreamNode.nodeData.inputB !== null);
                                break;
                            case LSpecs.NodeType.NOT:
                            case LSpecs.NodeType.Output:
                                canUpdate = (downstreamNode.nodeData.inputA !== null);
                                break;
                        }

                        if (canUpdate) {
                            downstreamNode.updateData();
                        }
                    }
                }
            });
        }
    }

    // Link validation
    function linkNodes(portA, portB) {
        if (canLinkNodes(portA, portB)) {
            createLink(portA, portB);
            updateLogic();
        }
    }

    function canLinkNodes(portA, portB) {
        if (portA.length === 0 || portB.length === 0) return false;

        var portAObj = findPort(portA);
        var portBObj = findPort(portB);

        if (portAObj.portType !== NLSpec.PortType.Output) return false;
        if (portBObj.portType !== NLSpec.PortType.Input) return false;

        // Prevent duplicate links
        var sameLinks = Object.values(links).filter(link =>
            link.inputPort._qsUuid === portA && link.outputPort._qsUuid === portB);
        if (sameLinks.length > 0) return false;

        // Input port can only have one connection
        var portBObjLinks = Object.values(links).filter(link =>
            link.outputPort._qsUuid === portB);
        if (portBObjLinks.length > 0) return false;

        // Prevent self-connection
        var nodeA = findNodeId(portA);
        var nodeB = findNodeId(portB);
        if (nodeA === nodeB) return false;

        return true;
    }
}

Key Features:
- Node Registry: Defines all gate types with symbols and colors
- Signal Propagation: Iterative algorithm that propagates signals through the circuit
- Connection Management: Prevents same upstream node from connecting to multiple inputs of same gate
- Real-Time Updates: Updates all gates when connections or inputs change

Signal Propagation Algorithm:
1. Reset all gate inputs/outputs
2. Iteratively propagate signals from inputs to outputs
3. For each link, if upstream has output, set downstream input
4. Update downstream gate if all inputs are ready
5. Repeat until no more changes occur (or max iterations)

Scene Architecture


Step 7: Create Views

7.1 LogicCircuitNodeView.qml

Create resources/View/LogicCircuitNodeView.qml - custom view that renders gate symbols:

import QtQuick
import QtQuick.Controls
import NodeLink
import LogicCircuit

NodeView {
    id: nodeView

    // Remove default rectangle background
    color: "transparent"
    border.width: 0
    radius: 0
    isResizable: false

    contentItem: Item {
        // Input Node: Toggle switch
        Rectangle {
            anchors.centerIn: parent
            visible: node.type === LSpecs.NodeType.Input
            width: Math.min(44, parent.width * 0.8)
            height: Math.min(22, parent.height * 0.4)
            radius: height / 2
            color: node.nodeData.currentState ? "#A6E3A1" : "#585B70"
            border.color: Qt.darker(color, 1.2)
            border.width: 1

            Text {
                anchors.centerIn: parent
                text: node.nodeData.currentState ? "ON" : "OFF"
                color: "#1E1E2E"
                font.bold: true
                font.pixelSize: Math.max(8, parent.height * 0.4)
            }

            MouseArea {
                anchors.fill: parent
                cursorShape: Qt.PointingHandCursor
                onClicked: node.toggleState && node.toggleState()
            }
        }

        // Output Node: Lamp indicator
        Rectangle {
            anchors.centerIn: parent
            visible: node.type === LSpecs.NodeType.Output
            width: Math.min(30, parent.width * 0.6)
            height: Math.min(30, parent.height * 0.6)
            radius: width / 2
            color: "white"
            border.color: "#2A2A2A"
            border.width: 2

            Rectangle {
                anchors.centerIn: parent
                width: parent.width * 0.6
                height: parent.height * 0.6
                radius: width / 2
                color: node.nodeData ? node.nodeData.statusColor : "#9E9E9E"

                Behavior on color {
                    ColorAnimation { duration: 200 }
                }
            }
        }

        // Gate symbols (AND, OR, NOT) using Canvas
        Loader {
            id: gateLoader
            anchors.fill: parent
            sourceComponent: {
                switch (node.type) {
                case LSpecs.NodeType.AND:  return andGate
                case LSpecs.NodeType.OR:   return orGate
                case LSpecs.NodeType.NOT:  return notGate
                default: return null
                }
            }
        }

        // AND Gate Component
        Component {
            id: andGate
            Canvas {
                anchors.fill: parent
                onPaint: {
                    var ctx = getContext("2d");
                    ctx.reset();
                    ctx.clearRect(0, 0, width, height);

                    ctx.fillStyle = "white";
                    ctx.strokeStyle = "black";
                    ctx.lineWidth = 2;

                    var margin = width * 0.05;
                    var leftX = margin;
                    var topY = margin;
                    var bottomY = height - margin;
                    var centerY = height / 2;
                    var radius = (height - 2 * margin) / 2;
                    var flatRightX = width - margin - radius;

                    ctx.beginPath();
                    ctx.moveTo(leftX, topY);
                    ctx.lineTo(flatRightX, topY);
                    ctx.arc(flatRightX, centerY, radius, -Math.PI/2, Math.PI/2, false);
                    ctx.lineTo(leftX, bottomY);
                    ctx.closePath();
                    ctx.fill();
                    ctx.stroke();
                }
                onWidthChanged: requestPaint()
                onHeightChanged: requestPaint()
            }
        }

        // OR Gate Component
        Component {
            id: orGate
            Canvas {
                anchors.fill: parent
                onPaint: {
                    var ctx = getContext("2d");
                    ctx.reset();
                    ctx.clearRect(0, 0, width, height);

                    ctx.fillStyle = "white";
                    ctx.strokeStyle = "black";
                    ctx.lineWidth = 2;

                    var margin = width * 0.05;
                    var leftX = margin;
                    var rightX = width - margin;
                    var topY = margin;
                    var bottomY = height - margin;
                    var midY = height / 2;

                    ctx.beginPath();
                    ctx.moveTo(leftX, topY);
                    ctx.quadraticCurveTo(rightX, topY, rightX, midY);
                    ctx.quadraticCurveTo(rightX, bottomY, leftX, bottomY);
                    ctx.quadraticCurveTo(leftX + (width * 0.3), midY, leftX, topY);
                    ctx.closePath();
                    ctx.fill();
                    ctx.stroke();
                }
                onWidthChanged: requestPaint()
                onHeightChanged: requestPaint()
            }
        }

        // NOT Gate Component
        Component {
            id: notGate
            Canvas {
                anchors.fill: parent
                onPaint: {
                    var ctx = getContext("2d");
                    ctx.reset();
                    ctx.clearRect(0, 0, width, height);

                    ctx.fillStyle = "white";
                    ctx.strokeStyle = "black";
                    ctx.lineWidth = 2;

                    var margin = width * 0.05;
                    var leftX = margin;
                    var topY = margin;
                    var bottomY = height - margin;
                    var midY = height / 2;
                    var triangleRight = width - margin - (width * 0.2);
                    var bubbleRadius = Math.min(6, height * 0.1);

                    // Triangle
                    ctx.beginPath();
                    ctx.moveTo(leftX, topY);
                    ctx.lineTo(triangleRight, midY);
                    ctx.lineTo(leftX, bottomY);
                    ctx.closePath();
                    ctx.fill();
                    ctx.stroke();

                    // Bubble
                    ctx.beginPath();
                    ctx.arc(triangleRight + bubbleRadius * 1.5, midY, bubbleRadius, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.stroke();
                }
                onWidthChanged: requestPaint()
                onHeightChanged: requestPaint()
            }
        }
    }
}

Key Features:
- Gate Symbols: Renders standard logic gate symbols using Canvas
- Input Switch: Interactive toggle switch for Input nodes
- Output Lamp: Colored indicator for Output nodes
- No Background: Transparent background to show only gate symbols

Gate Symbols


7.2 LogicCircuitView.qml

Create resources/View/LogicCircuitView.qml - main view container:

import QtQuick
import QtQuick.Controls
import NodeLink
import QtQuickStream
import LogicCircuit

Item {
    id: view
    property LogicCircuitScene scene

    property SceneSession sceneSession: SceneSession {
        enabledOverview: false
        doNodesNeedImage: false
    }

    // Nodes Scene (flickable canvas)
    NodesScene {
        id: nodesScene
        anchors.fill: parent
        scene: view.scene
        sceneSession: view.sceneSession
        sceneContent: NodesRect {
            scene: view.scene
            sceneSession: view.sceneSession
            nodeViewComponent: Qt.createComponent("LogicCircuitNodeView.qml")
        }
    }

    // Side menu for adding nodes
    SideMenu {
        scene: view.scene
        sceneSession: view.sceneSession
        anchors.right: parent.right
        anchors.rightMargin: 45
        anchors.top: parent.top
        anchors.topMargin: 50
    }
}

Key Features:
- NodesScene: Provides the scrollable canvas for gates
- SideMenu: Allows users to add new gates to the circuit
- SceneSession: Manages view state and interactions


Step 8: Create Main Application

8.1 main.cpp

Create main.cpp:

#include <QtGui/QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>

int main(int argc, char* argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    QQuickStyle::setStyle("Material");
    engine.addImportPath(":/");

    const QUrl url(u"qrc:/LogicCircuit/main.qml"_qs);
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

8.2 main.qml

Create main.qml:

import QtQuick
import QtQuick.Dialogs
import QtQuick.Controls

import QtQuickStream
import NodeLink
import LogicCircuit

Window {
    id: window
    property LogicCircuitScene scene: null

    width: 1280
    height: 960
    visible: true
    title: qsTr("Logic Circuit Example")
    color: "#1e1e1e"

    Material.theme: Material.Dark
    Material.accent: "#4890e2"

    Component.onCompleted: {
        NLCore.defaultRepo = NLCore.createDefaultRepo(["QtQuickStream", "LogicCircuit"])
        NLCore.defaultRepo.initRootObject("LogicCircuitScene");
        window.scene = Qt.binding(function() {
            return NLCore.defaultRepo.qsRootObject;
        });
    }

    // Load Font Awesome fonts
    FontLoader { source: "qrc:/LogicCircuit/resources/fonts/Font Awesome 6 Pro-Thin-100.otf" }
    FontLoader { source: "qrc:/LogicCircuit/resources/fonts/Font Awesome 6 Pro-Solid-900.otf" }
    FontLoader { source: "qrc:/LogicCircuit/resources/fonts/Font Awesome 6 Pro-Regular-400.otf" }
    FontLoader { source: "qrc:/LogicCircuit/resources/fonts/Font Awesome 6 Pro-Light-300.otf" }

    // Main view
    LogicCircuitView {
        id: view
        scene: window.scene
        anchors.fill: parent
    }
}

Key Features:
- Dark Theme: Material Dark theme with custom accent color
- Scene Initialization: Creates the LogicCircuitScene using QtQuickStream
- Full-Screen View: LogicCircuitView fills the window

Main Application Layout


Step 9: Build and Run

9.1 Configure Build

  1. Create a build directory:
    bash mkdir build cd build

  2. Configure with CMake:
    bash cmake .. -DCMAKE_PREFIX_PATH=<Qt_Install_Path>

  3. Build the project:
    bash cmake --build .

9.2 Run the Application

Run the executable:

./LogicCircuit  # Linux/Mac
LogicCircuit.exe  # Windows

Step 10: Using the Logic Circuit

Basic Usage

  1. Add Nodes:
    1. Click the side menu to add gates
    2. You'll need: Input nodes, Logic gates (AND/OR/NOT), Output nodes
  2. Connect Gates:
    1. Click and drag from an output port (right side)
    2. Release on an input port (left side)
    3. A link will be created if the connection is valid
  3. Toggle Inputs:
    1. Click on Input nodes to toggle between ON and OFF
    2. The circuit updates automatically
  4. Observe Outputs:
    1. Output nodes show the result:
      • Green = ON (true)
      • Red = OFF (false)
      • Gray = UNDEFINED (not connected or invalid)

Node Connection Example

Example: Simple AND Circuit

Setup:
1. Add two Input nodes
2. Add one AND gate
3. Add one Output node

Connections:
- Input 1 β†’ AND (input A)
- Input 2 β†’ AND (input B)
- AND β†’ Output

Test:
- Both Inputs ON β†’ Output: Green (ON)
- One Input OFF β†’ Output: Red (OFF)
- Both Inputs OFF β†’ Output: Red (OFF)

Example: OR Circuit

Setup:
1. Add two Input nodes
2. Add one OR gate
3. Add one Output node

Connections:
- Input 1 β†’ OR (input A)
- Input 2 β†’ OR (input B)
- OR β†’ Output

Test:
- At least one Input ON β†’ Output: Green (ON)
- Both Inputs OFF β†’ Output: Red (OFF)

Example: NOT Circuit

Setup:
1. Add one Input node
2. Add one NOT gate
3. Add one Output node

Connections:
- Input β†’ NOT
- NOT β†’ Output

Test:
- Input ON β†’ Output: Red (OFF) - inverted
- Input OFF β†’ Output: Green (ON) - inverted

Example: Complex Circuit (A AND (B OR C))

Setup:
1. Add three Input nodes (A, B, C)
2. Add one OR gate
3. Add one AND gate
4. Add one Output node

Connections:
- Input B β†’ OR (input A)
- Input C β†’ OR (input B)
- Input A β†’ AND (input A)
- OR β†’ AND (input B)
- AND β†’ Output

Test:
- A=ON, B=ON, C=OFF β†’ Output: Green (ON)
- A=ON, B=OFF, C=ON β†’ Output: Green (ON)
- A=OFF, B=ON, C=ON β†’ Output: Red (OFF)
- A=ON, B=OFF, C=OFF β†’ Output: Red (OFF)

Example Workflow

Architecture Overview

Component Hierarchy

LogicCircuitScene (I_Scene)
β”œβ”€β”€ NodeRegistry (defines gate types)
β”œβ”€β”€ SelectionModel (manages selection)
β”œβ”€β”€ UndoCore (undo/redo support)
└── Nodes & Links
    β”œβ”€β”€ InputNode (toggle switches)
    β”œβ”€β”€ Logic Gates (AND, OR, NOT)
    └── OutputNode (visual indicators)

LogicCircuitView
β”œβ”€β”€ NodesScene (canvas)
β”‚   └── NodesRect (renders gates)
β”‚       └── LogicCircuitNodeView (custom gate UI)
└── SideMenu (add gates)

Signal Propagation

The circuit uses an iterative propagation algorithm:

  1. Reset Phase: All gate inputs/outputs are reset to null
  2. Propagation Phase:
    1. For each link, if upstream has output, set downstream input
    2. Update downstream gate if all inputs are ready
    3. Repeat until no more changes occur
  3. Update Phase: All gates calculate their outputs based on inputs

This ensures signals propagate correctly through the circuit, even with complex topologies.

Architecture Diagram

Key Concepts

Boolean Logic

The circuit implements fundamental boolean operations:

Signal Propagation

Signals flow from Input nodes through gates to Output nodes:
- Input nodes provide boolean values (true/false)
- Gates process inputs and produce outputs
- Output nodes display the final result

Gate Symbols

Gates are rendered using standard digital circuit symbols:
- AND: Flat left side, curved right side
- OR: Curved on both sides
- NOT: Triangle with bubble on output

Connection Rules

Extending the Logic Circuit

Adding New Gates

To add a new gate (e.g., NAND, NOR, XOR):

  1. Add to LSpecs.qml:
    qml enum NodeType { // ... existing NAND = 5, NOR = 6, XOR = 7 }
  2. Create NandNode.qml:
    import QtQuick
    import NodeLink
    import LogicCircuit
    
    LogicNode {
        nodeType: LSpecs.NodeType.NAND
    
        function updateData() {
            if (nodeData.inputA !== null && nodeData.inputB !== null) {
                nodeData.output = !(nodeData.inputA && nodeData.inputB);
            } else {
                nodeData.output = null;
            }
        }
    }
    
  3. Add Canvas Component in LogicCircuitNodeView.qml for the symbol
  4. Register in LogicCircuitScene.qml

Customizing Gate Appearance

Adding Truth Table Display

Create a component that shows the truth table for selected gates:
- Display all input combinations
- Show corresponding outputs
- Update in real-time as inputs change

Troubleshooting

Common Issues

  1. Output Shows UNDEFINED:
    1. Check that all inputs are connected
    2. Verify gates have all required inputs
    3. Ensure Input nodes are toggled ON/OFF
  2. Signals Not Propagating:
    1. Check for cycles in the circuit
    2. Verify connections are valid
    3. Ensure gates have all inputs connected
  3. Gate Not Updating:
    1. Toggle an Input node to trigger update
    2. Check that connections are properly established
    3. Verify gate has all required inputs
  4. Visual Issues:
    1. Gate symbols not rendering: Check Canvas code
    2. Colors not updating: Verify statusColor property
    3. Ports not visible: Check port creation code

Debug Tips

Truth Tables Reference

AND Gate

A B Output
0 0 0
0 1 0
1 0 0
1 1 1

OR Gate

A B Output
0 0 0
0 1 1
1 0 1
1 1 1

NOT Gate

A Output
0 1
1 0

Common Combinations

NAND (NOT AND): !(A && B)
| A | B | Output |
|---|---|--------|
| 0 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |

NOR (NOT OR): !(A || B)
| A | B | Output |
|---|---|--------|
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 0 |

XOR (Exclusive OR): (A && !B) || (!A && B)
| A | B | Output |
|---|---|--------|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |

Conclusion

The Logic Circuit Example demonstrates how to build an interactive digital logic simulator using NodeLink. Key takeaways:

This example serves as a foundation for building more sophisticated circuit simulators, such as:
- Sequential logic circuits (flip-flops, registers)
- Arithmetic circuits (adders, multipliers)
- Memory circuits
- Complex digital systems

For more examples, see the other examples in the NodeLink repository.

Final Example

Chatbot Example

Overview

The Chatbot Example demonstrates how to build an interactive chatbot application using NodeLink's visual node-based programming system. Unlike traditional chatbots that rely on hardcoded logic, this example shows how to create a rule-based chatbot using regular expressions (Regex) and visual node connections. Users can define conversation patterns visually by connecting nodes, making it easy to design and modify chatbot behavior without writing complex code.

Chatbot Example Overview

a. Purpose and Use Cases

Purpose

The Chatbot Example demonstrates:

  1. Visual Rule-Based Chatbots: Shows how to create chatbots using visual node graphs instead of traditional programming approaches.

  2. Pattern Matching with Regex: Illustrates how to use regular expressions to match user input and trigger appropriate responses.

  3. Conditional Response Logic: Demonstrates branching logic where different responses are triggered based on pattern matching results.

  4. Interactive User Interface: Combines a node-based editor with a real-time chat interface, allowing users to see their chatbot in action.

  5. Real-Time Data Flow: Shows how user input flows through the node graph, gets processed, and generates responses in real-time.

  6. Dual Output Nodes: Demonstrates nodes with multiple output ports for conditional branching (true/false paths).

Use Cases

Example Scenarios

Real-World Applications

Use Case Diagram

b. Node Types Explained

The Chatbot Example implements four distinct node types, each serving a specific role in the chatbot's decision-making process.

1. Source Node (SourceNode)

Purpose: Receives and stores user input messages from the chat interface.

Type ID: CSpecs.NodeType.Source (0)

Properties:
- Contains a single output port named "value"
- Receives text input from the chat interface
- Acts as the entry point for all user messages
- Can be manually edited for testing purposes

Ports:
- Output Port: "value" (Right side) - Emits the user's message text

Behavior:
- Automatically receives messages when users type in the chat box
- The message text is stored in nodeData.data
- When data changes, it triggers the scene's updateData() function
- Can be manually edited by clicking on the node and typing

Visual Appearance:
- Icon: Document/Text icon (Font Awesome)
- Color: Gray (#444)
- Size: 150x100 pixels

Usage Example:
- User types "hello world" in chat β†’ Source node receives "hello world"
- Source node outputs "hello world" to connected nodes

Source Node


2. Regex Node (RegexNode)

Purpose: Matches user input against a regular expression pattern and routes the result through two different output ports.

Type ID: CSpecs.NodeType.Regex (1)

Properties:
- Has one input port and two output ports
- Contains a regular expression pattern in nodeData.data
- Performs case-insensitive pattern matching
- Routes results through "output 1" (match found) or "output 2" (no match)

Ports:
- Input Port: "input" (Left side) - Receives text to match against the pattern
- Output Port 1: "output 1" (Right side) - Emits when pattern matches (FOUND)
- Output Port 2: "output 2" (Right side) - Emits when pattern doesn't match (NOT_FOUND)

Properties:
- inputFirst: Stores the input text to match
- matchedPattern: Contains "FOUND" or "NOT_FOUND" based on match result
- nodeData.data: Contains the regular expression pattern (e.g., "hello|hi|hey")

Behavior:
1. Receives input text from the connected upstream node
2. Creates a case-insensitive RegExp from nodeData.data
3. Tests the input against the pattern
4. Sets matchedPattern to "FOUND" or "NOT_FOUND"
5. Routes the result through the appropriate output port

Regular Expression Examples:
- "hello|hi|hey" - Matches any of these greetings
- "\\d+" - Matches one or more digits
- "[a-zA-Z]+" - Matches one or more letters
- "^hello" - Matches "hello" at the start of the string
- "world$" - Matches "world" at the end of the string

Visual Appearance:
- Icon: Search icon (Font Awesome \uf002)
- Color: Brown/Tan (#C69C6D)
- Size: 150x100 pixels

Usage Example:
- Pattern: "hello|hi"
- Input: "hello world" β†’ Matches β†’ matchedPattern = "FOUND" β†’ Routes to output 1
- Input: "goodbye" β†’ No match β†’ matchedPattern = "NOT_FOUND" β†’ Routes to output 2

Regex Node

Code Implementation:

function updataData() {
  if (!inputFirst) {
  return
  }
  var re = new RegExp(nodeData.data, "i")  // 'i' flag for case-insensitive
  var found = re.test(inputFirst)
  matchedPattern = found ? "FOUND" : "NOT_FOUND"
}

3. Result True Node (ResultTrueNode)

Purpose: Displays a response message when a pattern match is found (FOUND).

Type ID: CSpecs.NodeType.ResultTrue (2)

Properties:
- Contains a single input port
- Displays response text when triggered
- Automatically sends the response to the chat interface
- Read-only (cannot be edited directly)

Ports:
- Input Port: "value" (Left side) - Receives data from Regex node's output 1

Behavior:
- Should be connected to Regex node's "output 1" port
- When triggered, checks if the upstream Regex node has matchedPattern === "FOUND"
- If true, sets nodeData.data to "HI ..." (or custom response)
- Emits botResponse signal to display the message in the chat box
- If the pattern didn't match, sets data to empty string

Visual Appearance:
- Icon: Checkmark/Circle icon (Font Awesome \uf058)
- Color: Green (#4caf50) - indicates positive/successful match
- Size: 150x100 pixels

Usage Example:
- Regex node matches "hello" β†’ Routes to ResultTrue node
- ResultTrue node displays "HI ..." in chat
- User sees the bot's response

Result True Node

Code Implementation:

// In ChatbotScene.qml
  case CSpecs.NodeType.ResultTrue: {
  downStreamNode.nodeData.data = (upstreamNode.matchedPattern === "FOUND") ? "HI ..." : "";
  botResponse(downStreamNode.nodeData.data)
} break;

4. Result False Node (ResultFalseNode)

Purpose: Displays a response message when a pattern match is not found (NOT_FOUND).

Type ID: CSpecs.NodeType.ResultFalse (3)

Properties:
- Contains a single input port
- Displays response text when no pattern matches
- Automatically sends the response to the chat interface
- Read-only (cannot be edited directly)

Ports:
- Input Port: "value" (Left side) - Receives data from Regex node's output 2

Behavior:
- Should be connected to Regex node's "output 2" port
- When triggered, checks if the upstream Regex node has matchedPattern === "NOT_FOUND"
- If true, sets nodeData.data to " :( " (or custom response)
- Emits botResponse signal to display the message in the chat box
- If the pattern matched, sets data to empty string

Visual Appearance:
- Icon: X-mark/Circle icon (Font Awesome \uf057)
- Color: Red (#f44336) - indicates no match/failure
- Size: 150x100 pixels

Usage Example:
- Regex node doesn't match user input β†’ Routes to ResultFalse node
- ResultFalse node displays " :( " in chat
- User sees the bot's "I don't understand" response

Result False Node

Code Implementation:

// In ChatbotScene.qml
  case CSpecs.NodeType.ResultFalse: {
  downStreamNode.nodeData.data = (upstreamNode.matchedPattern === "NOT_FOUND") ? " :( " : "";
  botResponse(downStreamNode.nodeData.data)
} break;

Node Type Summary Table

Node Type Type ID Input Ports Output Ports Purpose Color
Source 0 0 1 Receives user input Gray (#444)
Regex 1 1 2 Pattern matching Brown (#C69C6D)
ResultTrue 2 1 0 Response on match Green (#4caf50)
ResultFalse 3 1 0 Response on no match Red (#f44336)

Data Flow Architecture

The chatbot follows a simple but powerful data flow pattern:

User Input (Chat Box)
    ↓
  Source Node (stores message)
    ↓
  Regex Node (pattern matching)
    β”œβ”€β†’ [Match Found] β†’ ResultTrue Node β†’ Chat Response ("HI ...")
    └─→ [No Match] β†’ ResultFalse Node β†’ Chat Response (" :( ")

Data Flow Diagram

c. Step-by-Step Building Guide

This guide will walk you through building the Chatbot Example from scratch, explaining each component and how they work together.

Prerequisites

Step 1: Project Setup

1.1 Create Project Structure

Create the following directory structure:

chatbot/
  β”œβ”€β”€ CMakeLists.txt
  β”œβ”€β”€ main.cpp
  β”œβ”€β”€ main.qml
  └── resources/
      β”œβ”€β”€ Core/
      β”œβ”€β”€ View/
      └── fonts/

1.2 Configure CMakeLists.txt

Create CMakeLists.txt with the following configuration:

cmake_minimum_required(VERSION 3.1.0)

set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Configure Qt
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui QuickControls2 REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui QuickControls2 REQUIRED)

list(APPEND QML_IMPORT_PATH ${CMAKE_BINARY_DIR}/qml)

# Create executable
qt_add_executable(Chatbot main.cpp)

# Set CSpecs as singleton
set_source_files_properties(
  resources/Core/CSpecs.qml
  PROPERTIES
  QT_QML_SINGLETON_TYPE True
)

# Define QML module
qt_add_qml_module(Chatbot
  URI "Chatbot"
  VERSION 1.0
  QML_FILES
  main.qml
  resources/Core/CSpecs.qml
  resources/Core/ChatbotScene.qml
  resources/Core/SourceNode.qml
  resources/Core/RegexNode.qml
  resources/Core/ResultTrueNode.qml
  resources/Core/ResultFalseNode.qml
  resources/Core/OperationNode.qml
  resources/Core/OperationNodeData.qml
  resources/Core/Chatbox.qml
  resources/View/ChatbotView.qml
  resources/View/ChatbotNodeView.qml
  RESOURCES
  resources/fonts/Font\ Awesome\ 6\ Pro-Thin-100.otf
  resources/fonts/Font\ Awesome\ 6\ Pro-Solid-900.otf
  resources/fonts/Font\ Awesome\ 6\ Pro-Regular-400.otf
  resources/fonts/Font\ Awesome\ 6\ Pro-Light-300.otf
)

target_include_directories(Chatbot PUBLIC
  Qt${QT_VERSION_MAJOR}::QuickControls2)

target_link_libraries(Chatbot PRIVATE
  Qt${QT_VERSION_MAJOR}::Core
  Qt${QT_VERSION_MAJOR}::Gui
  Qt${QT_VERSION_MAJOR}::QuickControls2
  NodeLinkplugin
  QtQuickStreamplugin
)

Key Points:
- Links to NodeLinkplugin and QtQuickStreamplugin
- Sets CSpecs.qml as a singleton for global access
- Includes Font Awesome fonts for icons
- Includes Chatbox.qml for the chat interface


Step 2: Create Specifications (CSpecs.qml)

Create resources/Core/CSpecs.qml - a singleton that defines node type constants:

pragma Singleton

import QtQuick

QtObject {
  enum NodeType {
  Source       = 0,
  Regex        = 1,
  ResultTrue   = 2,
  ResultFalse  = 3,
  Unknown      = 99
  }

  enum OperationType {
  Operation    = 0,
  Regex        = 1,
  Unknown      = 99
  }
}

Purpose: Provides type-safe constants for node types used throughout the application.


Step 3: Create Node Data Models

3.1 OperationNodeData.qml

Create resources/Core/OperationNodeData.qml - data model for operation nodes:

import QtQuick
import NodeLink

I_NodeData {
  property var inputFirst: null
}

Purpose: Extends I_NodeData to store input value for regex operations.


Step 4: Create Base Node Types

4.1 SourceNode.qml

Create resources/Core/SourceNode.qml:

import QtQuick
import NodeLink

Node {
  type: CSpecs.NodeType.Source
  nodeData: I_NodeData {}
  guiConfig.width: 150
  guiConfig.height: 100

  Component.onCompleted: addPorts();

  function addPorts() {
  let _port1 = NLCore.createPort();
  _port1.portType = NLSpec.PortType.Output
  _port1.portSide = NLSpec.PortPositionSide.Right
  _port1.title    = "value";
  addPort(_port1);
  }
}

Key Features:
- Single output port on the right side
- Receives user messages from the chat interface
- Fixed size node (150x100)


4.2 RegexNode.qml

Create resources/Core/RegexNode.qml - the core pattern matching node:

import QtQuick
import NodeLink
import Chatbot

Node {
  type: CSpecs.NodeType.Regex
  nodeData: I_NodeData {}
  property var inputFirst: null
  property var matchedPattern: null
  guiConfig.width: 150
  guiConfig.height: 100

  Component.onCompleted: addPorts();

  function addPorts() {
  let _port1 = NLCore.createPort();
  let _port2 = NLCore.createPort();
  let _port3 = NLCore.createPort();

  _port1.portType = NLSpec.PortType.Input
  _port1.portSide = NLSpec.PortPositionSide.Left
  _port1.enable   = false;
  _port1.title    = "input";

  _port2.portType = NLSpec.PortType.Output
  _port2.portSide = NLSpec.PortPositionSide.Right
  _port2.title    = "output 1";

  _port3.portType = NLSpec.PortType.Output
  _port3.portSide = NLSpec.PortPositionSide.Right
  _port3.title    = "output 2";

  addPort(_port1);
  addPort(_port2);
  addPort(_port3);
  }

  function updataData() {
  if (!inputFirst) {
  return
  }

  var re = new RegExp(nodeData.data, "i")  // Case-insensitive
  var found = re.test(inputFirst)
  matchedPattern = found ? "FOUND" : "NOT_FOUND"
  }
}

Key Features:
- One input port (left) and two output ports (right)
- Stores regex pattern in nodeData.data
- Performs case-insensitive matching
- Sets matchedPattern property for downstream nodes to check


4.3 ResultTrueNode.qml

Create resources/Core/ResultTrueNode.qml:

import QtQuick
import NodeLink

Node {
  type: CSpecs.NodeType.ResultTrue
  nodeData: I_NodeData {}
  guiConfig.width: 150
  guiConfig.height: 100

  Component.onCompleted: addPorts();

  onCloneFrom: function (baseNode) {
  nodeData.data = null;
  }

  function addPorts() {
  let _port1 = NLCore.createPort();
  _port1.portType = NLSpec.PortType.Input
  _port1.portSide = NLSpec.PortPositionSide.Left
  _port1.enable   = false;
  _port1.title    = "value";
  addPort(_port1);
  }
}

Key Features:
- Single input port (left side)
- Connected to Regex node's "output 1"
- Resets data when cloned


4.4 ResultFalseNode.qml

Create resources/Core/ResultFalseNode.qml:

import QtQuick
import NodeLink

Node {
  type: CSpecs.NodeType.ResultFalse
  nodeData: I_NodeData {}
  guiConfig.width: 150
  guiConfig.height: 100

  Component.onCompleted: addPorts();

  onCloneFrom: function (baseNode) {
  nodeData.data = null;
  }

  function addPorts() {
  let _port1 = NLCore.createPort();
  _port1.portType = NLSpec.PortType.Input
  _port1.portSide = NLSpec.PortPositionSide.Left
  _port1.enable   = false;
  _port1.title    = "value";
  addPort(_port1);
  }
}

Key Features:
- Single input port (left side)
- Connected to Regex node's "output 2"
- Resets data when cloned


Step 5: Create the Chat Interface

5.1 Chatbox.qml

Create resources/Core/Chatbox.qml - the chat interface component:

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Item {
  id: chatBox
  width: parent ? parent.width : 400
  height: parent ? parent.height : 300

  signal userMessageSent(string message)

  ListModel {
  id: messagesModel
  }

  ColumnLayout {
  anchors.fill: parent
  spacing: 12

  // Message list with scroll view
  ScrollView {
  id: scrollView
  Layout.fillWidth: true
  Layout.fillHeight: true
  clip: true

  Column {
  id: messageList
  width: scrollView.width
  spacing: 10

  Repeater {
  model: messagesModel

  delegate: Item {
  width: parent.width
  implicitHeight: bubble.implicitHeight + dynamicSpacing
  property int dynamicSpacing: Math.max(36, messageText.lineCount * 24)

  Row {
  width: parent.width
  spacing: 6
  anchors.margins: 8
  layoutDirection: model.isUser ? Qt.RightToLeft : Qt.LeftToRight

  // Avatar
  Rectangle {
  width: 28
  height: 28
  radius: 14
  color: model.isUser ? "#3A7AFE" : "#C69C6D"
  anchors.verticalCenter: parent.verticalCenter

  Text {
  anchors.centerIn: parent
  text: model.isUser ? "U" : "B"
  color: "white"
  font.bold: true
  font.pointSize: 10
  }
  }

  // Message bubble
  Rectangle {
  id: bubble
  color: model.isUser ? "#3A7AFE" : "#C69C6D"
  radius: 12
  width: Math.min(parent.width * 0.7, messageText.implicitWidth + 24)
  height: messageText.paintedHeight + 20

  Text {
  id: messageText
  text: model.text
  color: "white"
  wrapMode: Text.WordWrap
  font.pointSize: 11
  anchors.margins: 10
  anchors.fill: parent
  width: bubble.width - 20
  }
  }
  }
  }
  }
  }
  }

  // Input field and send button
  RowLayout {
  Layout.fillWidth: true
  spacing: 6

  TextField {
  id: inputField
  Layout.fillWidth: true
  placeholderText: "Type your message..."
  font.pointSize: 10
  onAccepted: sendMessage()
  }

  Button {
  text: "Send"
  onClicked: sendMessage()
  }
  }
  }

  Component.onCompleted: {
  // Welcome messages
  messagesModel.append({ text: "Hello there! I'm a chatbot based on visual programming and built using the NodeLink.", isUser: false })
  messagesModel.append({ text: "You can send me any message, and I'll check it using Regex nodes.", isUser: false })
  messagesModel.append({ text: "Type something like:   hello world :)   ", isUser: false })
  Qt.callLater(scrollToBottom)
  }

  function scrollToBottom() {
  if (scrollView.flickableItem) {
  scrollView.flickableItem.contentY =
  Math.max(0, scrollView.flickableItem.contentHeight - scrollView.flickableItem.height);
  }
  }

  function sendMessage() {
  if (inputField.text.trim().length === 0)
  return

  let msg = inputField.text.trim()
  inputField.text = ""

  messagesModel.append({ text: msg, isUser: true })
  chatBox.userMessageSent(msg)
  Qt.callLater(scrollToBottom)
  }

  function addMessage(text, fromUser) {
  if (text && text.trim() !== "") {
  messagesModel.append({ text: text, isUser: fromUser })
  Qt.callLater(scrollToBottom)
  }
  }
}

Key Features:
- Message List: Displays chat history with user and bot messages
- Scroll View: Auto-scrolls to bottom when new messages arrive
- Input Field: Text field for user input with Enter key support
- Send Button: Button to send messages
- Welcome Messages: Shows introductory messages on startup
- Signal: Emits userMessageSent signal when user sends a message

Chat Interface


Step 6: Create the Scene

6.1 ChatbotScene.qml

Create resources/Core/ChatbotScene.qml - the main scene that manages nodes, links, and bot responses:

import QtQuick
import QtQuick.Controls
import NodeLink
import Chatbot

I_Scene {
  id: scene

  nodeRegistry: NLNodeRegistry {
  _qsRepo: scene._qsRepo
  imports: ["Chatbot"]
  defaultNode: CSpecs.NodeType.Source

  nodeTypes: [
  CSpecs.NodeType.Source      = "SourceNode",
  CSpecs.NodeType.Regex       = "RegexNode",
  CSpecs.NodeType.ResultTrue  = "ResultTrueNode",
  CSpecs.NodeType.ResultFalse = "ResultFalseNode"
  ];

  nodeNames: [
  CSpecs.NodeType.Source      = "Source",
  CSpecs.NodeType.Regex       = "Regex",
  CSpecs.NodeType.ResultTrue  = "ResultTrue",
  CSpecs.NodeType.ResultFalse = "ResultFalse"
  ];

  nodeIcons: [
  CSpecs.NodeType.Source      = "\ue4e2",
  CSpecs.NodeType.Regex       = "\uf002",
  CSpecs.NodeType.ResultTrue  = "\uf058",
  CSpecs.NodeType.ResultFalse = "\uf057"
  ];

  nodeColors: [
  CSpecs.NodeType.Source      = "#444",
  CSpecs.NodeType.Regex       = "#C69C6D",
  CSpecs.NodeType.ResultTrue  = "#4caf50",
  CSpecs.NodeType.ResultFalse = "#f44336"
  ];
  }

  selectionModel: SelectionModel {
  existObjects: [...Object.keys(nodes), ...Object.keys(links)]
  }

  property UndoCore _undoCore: UndoCore {
  scene: scene
  }

  // Update node data when links/nodes change
  onLinkRemoved: _upateDataTimer.start();
  onNodeRemoved: _upateDataTimer.start();
  onLinkAdded:   updateData();

  property Timer _upateDataTimer: Timer {
  repeat: false
  running: false
  interval: 1
  onTriggered: scene.updateData();
  }

  // Signal to send bot response to chat
  signal botResponse(string text)

  // Create a node with specific type and position
  function createCustomizeNode(nodeType: int, xPos: real, yPos: real): string {
  var title = nodeRegistry.nodeNames[nodeType] + "_" +
  (Object.values(scene.nodes).filter(node => node.type === nodeType).length + 1);
  return createSpecificNode(nodeRegistry.imports, nodeType,
  nodeRegistry.nodeTypes[nodeType],
  nodeRegistry.nodeColors[nodeType],
  title, xPos, yPos);
  }

  // Link validation and creation (similar to Calculator example)
  function linkNodes(portA: string, portB: string) {
  if (!canLinkNodes(portA, portB)) {
  console.error("[Scene] Cannot link Nodes");
  return;
  }
  let link = Object.values(links).find(conObj =>
  conObj.inputPort._qsUuid === portA &&
  conObj.outputPort._qsUuid === portB);
  if (link === undefined)
  createLink(portA, portB);
  }

  function canLinkNodes(portA: string, portB: string): bool {
  // Validation logic (same as Calculator example)
  // ... (see source file for full implementation)
  return true;
  }

  // Update all node data based on connections
  function updateData() {
  var notReadyLinks = [];

  // Initialize result nodes
  Object.values(nodes).forEach(node => {
  switch (node.type) {
  case CSpecs.NodeType.ResultTrue:
  case CSpecs.NodeType.ResultFalse: {
  node.nodeData.data = null;
  } break;
  }
  });

  // Process links and update data
  Object.values(links).forEach(link => {
  var portA = link.inputPort._qsUuid;
  var portB = link.outputPort._qsUuid;

  var upstreamNode   = findNode(portA);
  var downStreamNode = findNode(portB);

  // Find nodes with valid data that connected to upstreamNode
  var upstreamNodeLinks = Object.values(links).filter(linkObj => {
  var node = findNode(linkObj.outputPort._qsUuid);
  var inputNode = findNode(linkObj.inputPort._qsUuid);
  if (node._qsUuid === upstreamNode._qsUuid) {
  if(inputNode.nodeData.data) {
  return linkObj
  }
  }
  });

  if (!upstreamNode.nodeData.data &&
  upstreamNode.type !== CSpecs.NodeType.Source) {
  if (upstreamNodeLinks.length > 1)
  notReadyLinks.push(link);
  return;
  }

  upadateNodeData(upstreamNode, downStreamNode);
  });

  // Handle nodes waiting for multiple inputs
  while (notReadyLinks.length > 0) {
  notReadyLinks.forEach((link, index) => {
  var portA = link.inputPort._qsUuid;
  var portB = link.outputPort._qsUuid;

  var upstreamNode   = findNode(portA);
  var downStreamNode = findNode(portB);

  var upstreamNodeLinks = Object.values(links).filter(linkObj =>
  findNodeId(linkObj.outputPort._qsUuid) === upstreamNode._qsUuid);

  if (upstreamNode.nodeData.data) {
  notReadyLinks.splice(index, 1);
  }

  upadateNodeData(upstreamNode, downStreamNode);
  });
  }
  }

  // Update specific node data
  function upadateNodeData(upstreamNode: Node, downStreamNode: Node) {
  switch (downStreamNode.type) {
  case CSpecs.NodeType.Regex: {
  downStreamNode.inputFirst = upstreamNode.nodeData.data;
  downStreamNode.updataData();
  } break;

  case CSpecs.NodeType.ResultTrue: {
  downStreamNode.nodeData.data = (upstreamNode.matchedPattern === "FOUND") ? "HI ..." : "";
  botResponse(downStreamNode.nodeData.data)
  } break;

  case CSpecs.NodeType.ResultFalse: {
  downStreamNode.nodeData.data = (upstreamNode.matchedPattern === "NOT_FOUND") ? " :( " : "";
  botResponse(downStreamNode.nodeData.data)
  } break;
  }
  }
}

Key Features:
- Node Registry: Defines all available node types with colors and icons
- Link Validation: Ensures valid connections
- Data Propagation: Automatically updates node values when connections change
- Bot Response Signal: Emits botResponse signal to send messages to chat
- Conditional Logic: Checks matchedPattern to determine which response to send

Data Flow Logic:
1. User sends message β†’ Source node receives it
2. Source node outputs to Regex node
3. Regex node matches pattern and sets matchedPattern
4. ResultTrue/ResultFalse nodes check matchedPattern
5. Appropriate response is sent via botResponse signal
6. Chat interface displays the response

Scene Architecture


Step 7: Create Views

7.1 ChatbotNodeView.qml

Create resources/View/ChatbotNodeView.qml - custom view for displaying nodes:

import QtQuick
import QtQuick.Controls
import NodeLink
import Chatbot

NodeView {
  id: nodeView

  contentItem: Item {
  id: mainContentItem
  property bool iconOnly: ((node?.operationType ?? -1) > -1) ||
  nodeView.isNodeMinimal

  // Header with icon and title
  Item {
  id: titleItem
  anchors.left: parent.left
  anchors.right: parent.right
  anchors.top: parent.top
  anchors.margins: 12
  visible: !mainContentItem.iconOnly
  height: 20

  Text {
  id: iconText
  font.family: NLStyle.fontType.font6Pro
  font.pixelSize: 20
  anchors.left: parent.left
  anchors.verticalCenter: parent.verticalCenter
  text: scene.nodeRegistry.nodeIcons[node.type]
  color: node.guiConfig.color
  }

  NLTextArea {
  id: titleTextArea
  anchors.right: parent.right
  anchors.left: iconText.right
  anchors.verticalCenter: parent.verticalCenter
  anchors.leftMargin: 5
  height: 40
  readOnly: !nodeView.edit
  placeholderText: qsTr("Enter title")
  color: NLStyle.primaryTextColor
  text: node.title
  onTextChanged: {
  if (node && node.title !== text)
  node.title = text;
  }
  }
  }

  // Value display/input field
  NLTextField {
  id: textArea
  anchors.top: titleItem.bottom
  anchors.right: parent.right
  anchors.bottom: parent.bottom
  anchors.left: parent.left
  anchors.margins: 12
  anchors.topMargin: 5
  visible: !mainContentItem.iconOnly
  placeholderText: qsTr("String")
  color: NLStyle.primaryTextColor
  text: node?.nodeData?.data
  readOnly: !nodeView.edit ||
  (node.type === CSpecs.NodeType.ResultTrue) ||
  (node.type === CSpecs.NodeType.ResultFalse)
  wrapMode: TextEdit.WrapAnywhere
  onTextChanged: {
  if (node && (node.nodeData?.data ?? "") !== text) {
  if (node.type === CSpecs.NodeType.Source ||
  node.type === CSpecs.NodeType.Regex) {
  node.nodeData.data = text;
  scene.updateData();
  }
  }
  }
  }

  // Minimal view (icon only at low zoom)
  Rectangle {
  id: minimalRectangle
  anchors.fill: parent
  anchors.margins: 10
  color: mainContentItem.iconOnly ? "#282828" : "transparent"
  radius: NLStyle.radiusAmount.nodeView

  Text {
  font.family: NLStyle.fontType.font6Pro
  font.pixelSize: 60
  anchors.centerIn: parent
  text: scene.nodeRegistry.nodeIcons[node.type]
  color: node.guiConfig.color
  visible: mainContentItem.iconOnly
  }
  }
  }
}

Key Features:
- Editable Title: Users can rename nodes
- Value Display: Shows node data (editable for Source/Regex, read-only for Results)
- Minimal Mode: Shows icon only when zoomed out
- String Input: Accepts text input for Source and Regex nodes


7.2 ChatbotView.qml

Create resources/View/ChatbotView.qml - main view container:

import QtQuick
import QtQuick.Controls
import NodeLink
import QtQuickStream
import Chatbot

Item {
  id: view
  property ChatbotScene scene

  property SceneSession sceneSession: SceneSession {
  enabledOverview: false;
  doNodesNeedImage: false
  }

  // Nodes Scene (flickable canvas)
  NodesScene {
  id: nodesScene
  anchors.fill: parent
  scene: view.scene
  sceneSession: view.sceneSession
  sceneContent: NodesRect {
  scene: view.scene
  sceneSession: view.sceneSession
  nodeViewComponent: Qt.createComponent("ChatbotNodeView.qml")
  }
  }

  // Side menu for adding nodes
  SideMenu {
  scene: view.scene
  sceneSession: view.sceneSession
  anchors.right: parent.right
  anchors.rightMargin: 45
  anchors.top: parent.top
  anchors.topMargin: 50
  }
}

Key Features:
- NodesScene: Provides the scrollable canvas for nodes
- SideMenu: Allows users to add new nodes to the scene
- SceneSession: Manages view state and interactions


Step 8: Create Main Application

8.1 main.cpp

Create main.cpp:

#include <QtGui/QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>

int main(int argc, char* argv[])
{
  QGuiApplication app(argc, argv);
  QQmlApplicationEngine engine;

  // Set Material style
  QQuickStyle::setStyle("Material");

  // Import all items into QML engine
  engine.addImportPath(":/");

  const QUrl url(u"qrc:/Chatbot/main.qml"_qs);
  QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
  &app, [url](QObject *obj, const QUrl &objUrl) {
  if (!obj && url == objUrl)
  QCoreApplication::exit(-1);
  }, Qt::QueuedConnection);
  engine.load(url);

  return app.exec();
}

8.2 main.qml

Create main.qml:

import QtQuick
import QtQuick.Dialogs
import QtQuick.Controls
import QtQuick.Layouts

import QtQuickStream
import NodeLink
import Chatbot

Window {
  id: window
  property ChatbotScene scene: null

  width: 1280
  height: 960
  visible: true
  title: qsTr("Chatbot Example")
  color: "#1e1e1e"

  Material.theme: Material.Dark
  Material.accent: "#4890e2"

  Component.onCompleted: {
  // Create root object
  NLCore.defaultRepo = NLCore.createDefaultRepo(["QtQuickStream", "Chatbot"])
  NLCore.defaultRepo.initRootObject("ChatbotScene");
  window.scene = Qt.binding(function() {
  return NLCore.defaultRepo.qsRootObject;
  });

  // Connect bot response signal to chat box
  Qt.callLater(() => {
  if (window.scene) {
  window.scene.botResponse.connect(function(msg) {
  chatBox.addMessage(msg, false)
  })
  }
  })
  }

  // Load Font Awesome fonts
  FontLoader { source: "qrc:/Chatbot/resources/fonts/Font Awesome 6 Pro-Thin-100.otf" }
  FontLoader { source: "qrc:/Chatbot/resources/fonts/Font Awesome 6 Pro-Solid-900.otf" }
  FontLoader { source: "qrc:/Chatbot/resources/fonts/Font Awesome 6 Pro-Regular-400.otf" }
  FontLoader { source: "qrc:/Chatbot/resources/fonts/Font Awesome 6 Pro-Light-300.otf" }

  // Main layout: Node view (left) and Chat box (right)
  RowLayout {
  anchors.fill: parent
  spacing: 4
  anchors.margins: 4

  // Node-based view (left)
  ChatbotView {
  id: view
  scene: window.scene
  Layout.fillWidth: true
  Layout.fillHeight: true
  }

  // Chat box (right)
  Chatbox {
  id: chatBox
  Layout.preferredWidth: 400
  Layout.fillHeight: true
  onUserMessageSent: {
  // Find Source node and update it with user message
  let sourceNode = Object.values(window.scene.nodes).find(n => n.type === 0)
  if (sourceNode) {
  sourceNode.nodeData.data = message
  window.scene.updateData()
  }
  }
  }
  }
}

Key Features:
- Dark Theme: Material Dark theme with custom accent color
- Scene Initialization: Creates the ChatbotScene using QtQuickStream
- Signal Connection: Connects bot response signal to chat box
- Split Layout: Node editor on left, chat interface on right
- Message Handling: Updates Source node when user sends a message

Main Application Layout


Step 9: Build and Run

9.1 Configure Build

  1. Create a build directory:
    bash mkdir build cd build

  2. Configure with CMake:
    bash cmake .. -DCMAKE_PREFIX_PATH=<Qt_Install_Path>

  3. Build the project:
    bash cmake --build .

9.2 Run the Application

Run the executable:

./Chatbot  # Linux/Mac
Chatbot.exe  # Windows

Step 10: Using the Chatbot

Basic Usage

  1. Add Nodes:
  2. Click the side menu to add nodes
  3. You'll need at least: 1 Source, 1 Regex, 1 ResultTrue, 1 ResultFalse

  4. Configure Regex Node:

  5. Click on the Regex node
  6. Type a regular expression pattern (e.g., "hello|hi|hey")
  7. Press Enter or click outside

  8. Connect Nodes:

  9. Connect Source output β†’ Regex input
  10. Connect Regex "output 1" β†’ ResultTrue input
  11. Connect Regex "output 2" β†’ ResultFalse input

  12. Test the Chatbot:

  13. Type a message in the chat box (right side)
  14. If the message matches the regex pattern, you'll see "HI ..."
  15. If it doesn't match, you'll see " :( "

Node Connection Example

Example: Greeting Bot

Setup:
1. Add a Source node
2. Add a Regex node, set pattern to: "hello|hi|hey|greetings"
3. Add a ResultTrue node
4. Add a ResultFalse node

Connections:
- Source β†’ Regex (input)
- Regex (output 1) β†’ ResultTrue
- Regex (output 2) β†’ ResultFalse

Test:
- User: "hello" β†’ Bot: "HI ..."
- User: "hi there" β†’ Bot: "HI ..."
- User: "goodbye" β†’ Bot: " :( "

Example: Number Detection

Setup:
1. Add a Source node
2. Add a Regex node, set pattern to: "\\d+" (matches one or more digits)
3. Add ResultTrue and ResultFalse nodes
4. Connect as above

Test:
- User: "123" β†’ Bot: "HI ..."
- User: "abc" β†’ Bot: " :( "
- User: "I have 5 apples" β†’ Bot: "HI ..." (contains digits)

Example: Email Pattern

Setup:
1. Regex pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"

Test:
- User: "test@example.com" β†’ Bot: "HI ..."
- User: "not an email" β†’ Bot: " :( "

Example Workflow

Architecture Overview

Component Hierarchy

ChatbotScene (I_Scene)
  β”œβ”€β”€ NodeRegistry (defines node types)
  β”œβ”€β”€ SelectionModel (manages selection)
  β”œβ”€β”€ UndoCore (undo/redo support)
  └── Nodes & Links
      β”œβ”€β”€ SourceNode (user input)
      β”œβ”€β”€ RegexNode (pattern matching)
      β”œβ”€β”€ ResultTrueNode (match response)
      └── ResultFalseNode (no-match response)

Main Window
  β”œβ”€β”€ ChatbotView (left side)
  β”‚   β”œβ”€β”€ NodesScene (canvas)
  β”‚   β”‚   └── NodesRect (renders nodes)
  β”‚   β”‚       └── ChatbotNodeView (custom node UI)
  β”‚   └── SideMenu (add nodes)
  └── Chatbox (right side)
      β”œβ”€β”€ Message List (scrollable)
      └── Input Field + Send Button

Data Flow

User Types Message
    ↓
Chatbox emits userMessageSent signal
    ↓
Source Node receives message (nodeData.data)
    ↓
Source Node outputs to Regex Node
    ↓
Regex Node matches pattern
    β”œβ”€β†’ [Match] β†’ matchedPattern = "FOUND" β†’ ResultTrue β†’ "HI ..."
    └─→ [No Match] β†’ matchedPattern = "NOT_FOUND" β†’ ResultFalse β†’ " :( "
    ↓
botResponse signal emitted
    ↓
Chatbox displays response

Architecture Diagram

Key Concepts

Regular Expressions (Regex)

Regular expressions are patterns used to match character combinations in strings. The chatbot uses JavaScript's RegExp object.

Common Patterns:
- "hello" - Exact match
- "hello|hi" - Matches "hello" OR "hi"
- "\\d+" - One or more digits
- "[a-z]+" - One or more lowercase letters
- "^start" - Starts with "start"
- "end$" - Ends with "end"
- ".*" - Any characters (wildcard)

Flags:
- "i" - Case-insensitive (used in the chatbot)

Dual Output Ports

The Regex node has two output ports to enable conditional branching:
- Output 1: Triggered when pattern matches
- Output 2: Triggered when pattern doesn't match

This allows the chatbot to respond differently based on whether the pattern matched.

Signal-Based Communication

The chatbot uses Qt signals to communicate between components:
- userMessageSent: Chatbox β†’ Source Node
- botResponse: Scene β†’ Chatbox

This decouples the components and makes the system more flexible.

Extending the Chatbot

Adding Custom Response Messages

Modify ChatbotScene.qml to change response messages:

case CSpecs.NodeType.ResultTrue: {
  downStreamNode.nodeData.data = (upstreamNode.matchedPattern === "FOUND")
  ? "Great! I understood that!" : "";
  botResponse(downStreamNode.nodeData.data)
} break;

Adding Multiple Regex Patterns

You can chain multiple Regex nodes:

  1. Source β†’ Regex1 (pattern: "hello")
  2. Regex1 (output 1) β†’ Regex2 (pattern: "world")
  3. Regex2 (output 1) β†’ ResultTrue

This creates an AND condition: message must match both patterns.

Adding New Node Types

To add a new node type (e.g., "KeywordNode"):

  1. Add to CSpecs.qml:
    qml enum NodeType { // ... existing Keyword = 4 }

  2. Create KeywordNode.qml:
    qml Node { type: CSpecs.NodeType.Keyword // ... implementation }

  3. Register in ChatbotScene.qml:
    qml nodeTypes: [ // ... existing CSpecs.NodeType.Keyword = "KeywordNode" ]

Customizing Chat Interface

Modify Chatbox.qml to:
- Change colors and styling
- Add emoji support
- Add message timestamps
- Add user avatars
- Add typing indicators

Troubleshooting

Common Issues

  1. No Response from Bot:
  2. Check that nodes are properly connected
  3. Verify Regex pattern is correct
  4. Ensure Source node receives the message

  5. Regex Not Matching:

  6. Test your regex pattern in an online regex tester
  7. Remember the pattern is case-insensitive
  8. Check for special characters that need escaping

  9. Multiple Responses:

  10. Ensure only one Source node is connected
  11. Check that Result nodes are connected to correct Regex outputs

  12. Chat Not Updating:

  13. Verify botResponse signal is connected
  14. Check that addMessage function is called

Debug Tips

Regular Expression Reference

Basic Patterns

Pattern Matches Example
"hello" Exact string "hello"
"hello\|hi" Either string "hello" or "hi"
"\\d" Single digit "5"
"\\d+" One or more digits "123"
"[a-z]" Single lowercase letter "a"
"[A-Z]+" One or more uppercase letters "HELLO"
"." Any single character "a", "1", "!"
".*" Any characters "anything"

Special Characters

Character Meaning
^ Start of string
$ End of string
\| OR operator
+ One or more
* Zero or more
? Zero or one
\\ Escape character

Common Use Cases

Conclusion

The Chatbot Example demonstrates how to build interactive, rule-based chatbots using NodeLink's visual programming system. Key takeaways:

This example serves as a foundation for building more sophisticated conversational interfaces, such as:
- Multi-turn conversations
- Context-aware responses
- Natural language processing integration
- Database-backed responses
- Multi-language support

For more examples, see the other examples in the NodeLink repository.

Final Example

Performance Analyzer

Overview

The Performance Analyzer Example is a benchmarking and stress-testing tool designed to evaluate NodeLink's performance under various load conditions. This example allows you to create large numbers of nodes and links programmatically, measure operation times, and monitor system performance metrics. It's an essential tool for developers who want to understand NodeLink's scalability, optimize their applications, or test the framework's limits.

Performance Analyzer Overview

Performance Analyzer in Action

a. Purpose and Use Cases

Purpose

The Performance Analyzer Example demonstrates:

  1. Performance Benchmarking: Measure NodeLink's performance when handling large numbers of nodes and links.

  2. Stress Testing: Test the framework's limits by creating thousands of nodes and connections.

  3. Batch Operations: Create multiple nodes and links efficiently using batch creation methods.

  4. Performance Monitoring: Real-time monitoring of scene statistics (node count, link count, selection count).

  5. Operation Timing: Measure execution time for critical operations like node creation, selection, and scene clearing.

  6. Scalability Analysis: Understand how NodeLink performs with different scene sizes and configurations.

Use Cases

Example Scenarios

Real-World Applications

Use Case Diagram

b. Node Types Explained

The Performance Analyzer Example implements two simple node types designed for performance testing rather than functional complexity.

1. Start Node (StartNode)

Purpose: A simple node with an output port, used as the starting point in node pairs for performance testing.

Type ID: CSpecs.NodeType.StartNode (0)

Properties:
- Contains a single output port
- Minimal implementation for performance testing
- Acts as the source node in test pairs

Ports:
- Output Port: (Right side) - Emits data to connected End nodes

Properties:
- guiConfig.width: 100 pixels
- guiConfig.height: 100 pixels
- guiConfig.color: Gray (#444444)

Behavior:
- Simple node with minimal overhead
- Designed for fast creation and rendering
- Used in pairs with End nodes for link testing

Visual Appearance:
- Icon: Play button (Font Awesome \uf04b)
- Color: Gray (#444)
- Size: 100x100 pixels (fixed)

Usage: Created programmatically in batches for performance testing.

Start Node


2. End Node (EndNode)

Purpose: A simple node with an input port, used as the endpoint in node pairs for performance testing.

Type ID: CSpecs.NodeType.EndNode (1)

Properties:
- Contains a single input port
- Minimal implementation for performance testing
- Acts as the destination node in test pairs

Ports:
- Input Port: (Left side) - Receives data from connected Start nodes

Properties:
- guiConfig.width: 100 pixels
- guiConfig.height: 100 pixels
- guiConfig.color: Gray (#444444)

Behavior:
- Simple node with minimal overhead
- Designed for fast creation and rendering
- Used in pairs with Start nodes for link testing

Visual Appearance:
- Icon: Stop button (Font Awesome \uf11e)
- Color: Gray (#444)
- Size: 100x100 pixels (fixed)

Usage: Created programmatically in batches for performance testing.

End Node


Node Pair Structure

For performance testing, nodes are created in pairs:
- Start Node: Output port on the right
- End Node: Input port on the left
- Link: Connects Start output to End input

This simple structure allows testing:
- Node creation performance
- Link creation performance
- Scene rendering performance
- Selection performance

Node Pair


Node Type Summary Table

Node Type Type ID Input Ports Output Ports Purpose
Start 0 0 1 Source node for testing
End 1 1 0 Destination node for testing

c. Step-by-Step Building Guide

This guide will walk you through building the Performance Analyzer Example from scratch, explaining each component and how they work together.

Prerequisites

Step 1: Project Setup

1.1 Create Project Structure

Create the following directory structure:

performanceAnalyzer/
β”œβ”€β”€ CMakeLists.txt
β”œβ”€β”€ main.cpp
β”œβ”€β”€ Main.qml
└── resources/
    β”œβ”€β”€ Core/
    β”œβ”€β”€ View/
    └── fonts/

1.2 Configure CMakeLists.txt

Create CMakeLists.txt with the following configuration:

cmake_minimum_required(VERSION 3.1.0)
set(MODULE_NAME PerformanceAnalyzer)

set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui QuickControls2 REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui QuickControls2 REQUIRED)

list(APPEND QML_IMPORT_PATH ${CMAKE_BINARY_DIR}/qml)

# Create executable
qt_add_executable(${MODULE_NAME} main.cpp)

# Set CSpecs as singleton
set_source_files_properties(
    resources/Core/CSpecs.qml
    PROPERTIES
        QT_QML_SINGLETON_TYPE True
)

# Define QML module
qt_add_qml_module(${MODULE_NAME}
    URI ${MODULE_NAME}
    VERSION 1.0
    QML_FILES
        Main.qml
        resources/Core/CSpecs.qml
        resources/Core/StartNode.qml
        resources/Core/EndNode.qml
        resources/Core/PerformanceScene.qml
        resources/View/PerformanceAnalyzerView.qml
    RESOURCES
        resources/fonts/Font\ Awesome\ 6\ Pro-Thin-100.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Solid-900.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Regular-400.otf
        resources/fonts/Font\ Awesome\ 6\ Pro-Light-300.otf
)

target_include_directories(${MODULE_NAME} PUBLIC
    Qt${QT_VERSION_MAJOR}::QuickControls2)

target_link_libraries(${MODULE_NAME} PRIVATE
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::QuickControls2
    NodeLinkplugin
    QtQuickStreamplugin
)

Key Points:
- Links to NodeLinkplugin and QtQuickStreamplugin
- Sets CSpecs.qml as a singleton for global access
- Includes Font Awesome fonts for icons


Step 2: Create Specifications (CSpecs.qml)

Create resources/Core/CSpecs.qml - a singleton that defines node type constants:

pragma Singleton
import QtQuick

QtObject {
    enum NodeType {
        StartNode = 0,
        EndNode = 1
    }
}

Purpose: Provides type-safe constants for node types used in performance testing.


Step 3: Create Node Types

3.1 StartNode.qml

Create resources/Core/StartNode.qml:

import QtQuick
import NodeLink

Node {
    type: CSpecs.NodeType.StartNode
    nodeData: I_NodeData {}

    guiConfig.width: 100
    guiConfig.height: 100

    Component.onCompleted: addPorts();

    onCloneFrom: function (baseNode) {
        nodeData.data = null;
    }

    function addPorts() {
        let _port1 = NLCore.createPort();
        _port1.portType = NLSpec.PortType.Output
        _port1.portSide = NLSpec.PortPositionSide.Right
        _port1.enable = true
        addPort(_port1);
    }
}

Key Features:
- Single output port on the right side
- Fixed size (100x100) for consistent performance
- Minimal implementation for fast creation


3.2 EndNode.qml

Create resources/Core/EndNode.qml:

import QtQuick
import NodeLink

Node {
    type: CSpecs.NodeType.EndNode
    nodeData: I_NodeData {}

    guiConfig.width: 100
    guiConfig.height: 100

    Component.onCompleted: addPorts();

    onCloneFrom: function (baseNode) {
        nodeData.data = null;
    }

    function addPorts() {
        let _port1 = NLCore.createPort();
        _port1.portType = NLSpec.PortType.Input
        _port1.portSide = NLSpec.PortPositionSide.Left
        _port1.enable = false
        addPort(_port1);
    }
}

Key Features:
- Single input port on the left side
- Fixed size (100x100) for consistent performance
- Minimal implementation for fast creation


Step 4: Create the Scene

4.1 PerformanceScene.qml

Create resources/Core/PerformanceScene.qml - the main scene with batch creation methods:

import QtQuick
import NodeLink
import PerformanceAnalyzer
import QtQuickStream

Scene {
    id: scene

    nodeRegistry: NLNodeRegistry {
        _qsRepo: scene._qsRepo
        imports: ["PerformanceAnalyzer"]
        defaultNode: CSpecs.NodeType.StartNode

        nodeTypes: [
            CSpecs.NodeType.StartNode = "StartNode",
            CSpecs.NodeType.EndNode = "EndNode"
        ];

        nodeNames: [
            CSpecs.NodeType.StartNode = "Start",
            CSpecs.NodeType.EndNode = "End"
        ];

        nodeIcons: [
            CSpecs.NodeType.StartNode = "\uf04b",
            CSpecs.NodeType.EndNode = "\uf11e"
        ];

        nodeColors: [
            CSpecs.NodeType.StartNode = "#444",
            CSpecs.NodeType.EndNode = "#444"
        ];
    }

    selectionModel: SelectionModel {
        existObjects: [...Object.keys(nodes), ...Object.keys(links)]
    }

    property UndoCore _undoCore: UndoCore {
        scene: scene
    }

    // Batch link creation for performance
    function createLinks(linkDataArray) {
        if (!linkDataArray || linkDataArray.length === 0) {
            return;
        }

        var addedLinks = [];

        for (var i = 0; i < linkDataArray.length; i++) {
            var linkData = linkDataArray[i];

            // Validate the link can be created
            if (!canLinkNodes(linkData.portA, linkData.portB)) {
                console.warn("Cannot create link between " + linkData.portA + " and " + linkData.portB);
                continue;
            }

            var nodeX = linkData.nodeA
            var nodeY = linkData.nodeB

            // Update children and parents
            nodeX.children[nodeY._qsUuid] = nodeY;
            nodeX.childrenChanged();

            nodeY.parents[nodeX._qsUuid] = nodeX;
            nodeY.parentsChanged();

            // Create the link object
            var obj = NLCore.createLink();
            obj.inputPort = findPort(linkData.portA);
            obj.outputPort = findPort(linkData.portB);
            obj._qsRepo = sceneActiveRepo;

            // Add to local administration
            links[obj._qsUuid] = obj;
            addedLinks.push(obj);
        }

        if (addedLinks.length > 0) {
            linksChanged();
            linksAdded(addedLinks);
        }
    }

    // Batch node pair creation for performance testing
    function createPairNodes(pairs) {
        // pairs format: [{xPos, yPos, nodeName}, {xPos, yPos, nodeName}, ...]
        var nodesToAdd = []
        var linksToCreate = []
        if (!pairs || pairs.length === 0) return;

        // Pre-allocate arrays for better performance
        nodesToAdd.length = pairs.length * 2;
        linksToCreate.length = pairs.length;

        var nodeIndex = 0;

        for (var i = 0; i < pairs.length; i++) {
            var pair = pairs[i]

            // Create start node
            var startNode = NLCore.createNode()
            startNode.type = CSpecs.NodeType.StartNode
            startNode._qsRepo = scene?._qsRepo ?? NLCore.defaultRepo
            startNode.title = pair.nodeName + "_start"
            startNode.guiConfig.position.x = pair.xPos
            startNode.guiConfig.position.y = pair.yPos
            startNode.guiConfig.color = "#444444"
            startNode.guiConfig.width = 150
            startNode.guiConfig.height = 100

            var outputPort = NLCore.createPort()
            outputPort.portType = NLSpec.PortType.Output
            outputPort.portSide = NLSpec.PortPositionSide.Right
            startNode.addPort(outputPort)

            // Create end node
            var endNode = NLCore.createNode()
            endNode.type = CSpecs.NodeType.EndNode
            endNode._qsRepo = scene?._qsRepo ?? NLCore.defaultRepo
            endNode.title = pair.nodeName + "_end"
            endNode.guiConfig.position.x = pair.xPos + 230
            endNode.guiConfig.position.y = pair.yPos + 30
            endNode.guiConfig.color = "#444444"
            endNode.guiConfig.width = 150
            endNode.guiConfig.height = 100

            var inputPort = NLCore.createPort()
            inputPort.portType = NLSpec.PortType.Input
            inputPort.portSide = NLSpec.PortPositionSide.Left
            endNode.addPort(inputPort)

            nodesToAdd[nodeIndex++] = startNode;
            nodesToAdd[nodeIndex++] = endNode;

            linksToCreate[i] = {
                nodeA: startNode,
                nodeB: endNode,
                portA: outputPort._qsUuid,
                portB: inputPort._qsUuid,
            };
        }

        // Add all nodes at once
        addNodes(nodesToAdd, false)

        // Create all links at once
        createLinks(linksToCreate)
    }

    // Clear scene efficiently
    function clearScene() {
        console.time("Scene_clear")
        gc()  // Garbage collection
        scene.selectionModel.clear()
        var nodeIds = Object.keys(nodes)
        scene.deleteNodes(nodeIds)
        links = []
        console.timeEnd("Scene_clear")
    }
}

Key Features:
- Batch Creation: createPairNodes() creates multiple node pairs efficiently
- Batch Linking: createLinks() creates multiple links in one operation
- Performance Optimized: Pre-allocates arrays and minimizes function calls
- Scene Clearing: Efficient scene clearing with garbage collection

Performance Optimizations:
1. Pre-allocated arrays for nodes and links
2. Batch node addition using addNodes()
3. Batch link creation using createLinks()
4. Minimal validation during batch operations
5. Garbage collection before clearing

Scene Architecture


Step 5: Create Views

5.1 PerformanceAnalyzerView.qml

Create resources/View/PerformanceAnalyzerView.qml - main view container:

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import NodeLink
import QtQuickStream
import PerformanceAnalyzer

Item {
    id: view
    property PerformanceScene scene: null

    property SceneSession sceneSession: SceneSession {
        enabledOverview: false;
        doNodesNeedImage: false
    }

    // Nodes Scene (flickable canvas)
    NodesScene {
        id: nodesScene
        anchors.fill: parent
        scene: view.scene
        sceneSession: view.sceneSession
        sceneContent: NodesRect {
            scene: view.scene
            sceneSession: view.sceneSession
        }
    }

    // Side menu for adding nodes
    SideMenu {
        scene: view.scene
        sceneSession: view.sceneSession
        anchors.right: parent.right
        anchors.rightMargin: 45
        anchors.top: parent.top
        anchors.topMargin: 50
    }
}

Key Features:
- NodesScene: Provides the scrollable canvas for nodes
- SideMenu: Allows manual node addition (for testing)
- SceneSession: Manages view state and interactions


Step 6: Create Main Application

6.1 main.cpp

Create main.cpp:

#include <QtGui/QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>

int main(int argc, char* argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    QQuickStyle::setStyle("Material");
    engine.addImportPath(":/");

    const QUrl url(u"qrc:/PerformanceAnalyzer/Main.qml"_qs);
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

6.2 Main.qml

Create Main.qml - the main application with performance controls:

import QtQuick
import QtQuickStream
import QtQuick.Controls
import NodeLink
import PerformanceAnalyzer

ApplicationWindow {
    id: window

    visible: true
    width: 1280
    height: 960
    title: qsTr("Performance Test Example")
    color: "#1e1e1e"

    property PerformanceScene scene: null
    property int nodeCount: 100
    property bool spawnInsideView: true

    Component.onCompleted: {
        NLCore.defaultRepo = NLCore.createDefaultRepo(["QtQuickStream", "PerformanceAnalyzer"])
        NLCore.defaultRepo.initRootObject("PerformanceScene")
        window.scene = Qt.binding(function() {
            return NLCore.defaultRepo.qsRootObject
        })
    }

    PerformanceAnalyzerView {
        id: view
        scene: window.scene
        anchors.fill: parent
    }

    property var startTime

    BusyIndicator {
        id: busyIndicator
        running: false
        anchors.centerIn: parent
    }

    // Select all nodes function with timing
    function selectAll() {
        busyIndicator.running = true
        statusText.text = "Selecting..."
        statusText.color = "#FF9800"

        Qt.callLater(function () {
            const startTime = Date.now()
            console.log("(" + Object.keys(scene.nodes).length + ") Nodes, (" +
                        Object.keys(scene.links).length + ") Links and (" +
                        Object.keys(scene.containers).length + ") Containers to select")

            scene.selectionModel.selectAll(scene.nodes, [], scene.containers)

            const elapsed = Date.now() - startTime
            console.log("Selected items:", Object.keys(scene.selectionModel.selectedModel).length)
            console.log("Time elapsed:", elapsed, "ms")
            statusText.text = "Selected all items (" + elapsed + "ms)"
            statusText.color = "#4CAF50"
            busyIndicator.running = false
        })
    }

    // Keyboard shortcut for select all
    Shortcut {
        sequence: "Ctrl+A"
        onActivated: selectAll()
    }

    // Performance Controls Panel
    Rectangle {
        anchors.right: view.right
        anchors.top: view.top
        anchors.topMargin: 50
        anchors.rightMargin: 50
        width: 220
        height: 460
        color: "#2d2d2d"
        border.color: "#3e3e3e"
        radius: 8
        z: 10

        Column {
            anchors.fill: parent
            anchors.margins: 15
            spacing: 15

            Text {
                text: "Performance Controls"
                color: "#ffffff"
                font.bold: true
                font.pixelSize: 16
            }

            // Node pair count input
            Row {
                spacing: 10
                width: parent.width - 30

                Text {
                    text: "Node Pairs:"
                    color: "#cccccc"
                    anchors.verticalCenter: parent.verticalCenter
                    font.pixelSize: 14
                }

                TextField {
                    id: nodePairInput
                    width: 80
                    text: "100"
                    validator: IntValidator { bottom: 1; top: 10000 }
                    color: "#ffffff"
                    background: Rectangle {
                        color: "#1e1e1e"
                        border.color: "#3e3e3e"
                        radius: 4
                    }
                }
            }

            // Spawn mode toggle
            Button {
                id: spawnModeButton
                width: parent.width - 30
                checkable: true
                checked: spawnInsideView
                text: checked ? "Inside View" : "Across Scene"
                onToggled: spawnInsideView = checked
                highlighted: true
            }

            // Start test button
            Button {
                id: startButton
                text: "Start Test"
                width: parent.width - 30
                onClicked: {
                    if (!nodePairInput.acceptableInput) {
                        statusText.text = "Enter a value between 1 and 10000"
                        statusText.color = "#F44336"
                        return
                    }
                    nodeCount = parseInt(nodePairInput.text)
                    timer.running = true
                    startTime = Date.now()
                    statusText.text = "Creating " + nodeCount + " pairs..."
                    statusText.color = "#4CAF50"
                    enabled = false
                    busyIndicator.running = true
                }
            }

            // Status text
            Text {
                id: statusText
                text: "Ready"
                color: "#cccccc"
                font.pixelSize: 12
                width: parent.width - 30
                wrapMode: Text.WordWrap
            }

            // Clear scene button
            Button {
                text: "Clear Scene"
                width: parent.width - 30
                onClicked: {
                    scene.clearScene()
                    statusText.text = "Scene cleared"
                    statusText.color = "#cccccc"
                }
            }

            // Select all button
            Button {
                text: "Select All"
                width: parent.width - 30
                enabled: scene?.nodes ? Object.keys(scene.nodes).length > 0 : false
                onClicked: selectAll()
            }

            // Clear selection button
            Button {
                text: "Clear Selection"
                width: parent.width - 30
                enabled: scene?.selectionModel ? Object.keys(scene.selectionModel.selectedModel).length > 0 : false
                onClicked: {
                    const startTime = Date.now()
                    console.log("Items to deselect:", Object.keys(scene.selectionModel.selectedModel).length)
                    scene.selectionModel.clear()
                    const elapsed = Date.now() - startTime
                    console.log("Time elapsed:", elapsed, "ms")
                    statusText.text = `Cleared selection (${elapsed}ms)`
                    statusText.color = "#FF9800"
                }
            }
        }
    }

    // Scene Monitor Panel
    Rectangle {
        anchors.left: view.left
        anchors.top: view.top
        anchors.topMargin: 50
        anchors.leftMargin: 50
        width: 160
        height: 160
        color: "#2d2d2d"
        border.color: "#3e3e3e"
        radius: 8
        z: 10

        Column {
            anchors.fill: parent
            anchors.margins: 15
            spacing: 12

            Text {
                text: "Scene Monitor"
                color: "#ffffff"
                font.bold: true
                font.pixelSize: 16
            }

            Rectangle {
                width: parent.width - 30
                height: 1
                color: "#3e3e3e"
            }

            // Node count
            Row {
                spacing: 10
                Text {
                    text: "Nodes:"
                    color: "#cccccc"
                    font.pixelSize: 13
                    width: 80
                }
                Text {
                    id: nodeCountText
                    text: "0"
                    color: "#FF9800"
                    font.pixelSize: 13
                    font.bold: true
                }
            }

            // Link count
            Row {
                spacing: 10
                Text {
                    text: "Links:"
                    color: "#cccccc"
                    font.pixelSize: 13
                    width: 80
                }
                Text {
                    id: linkCountText
                    text: "0"
                    color: "#9C27B0"
                    font.pixelSize: 13
                    font.bold: true
                }
            }

            // Selected count
            Row {
                spacing: 10
                Text {
                    text: "Selected:"
                    color: "#cccccc"
                    font.pixelSize: 13
                    width: 80
                }
                Text {
                    id: selectedCountText
                    text: "0"
                    color: "#4CAF50"
                    font.pixelSize: 13
                    font.bold: true
                }
            }
        }
    }

    // Helper function for random positioning
    function randBetween(min, max) {
        return Math.random() * (max - min) + min;
    }

    // Node creation timer
    Timer {
        id: timer
        interval: 1
        running: false
        repeat: false
        onTriggered: {
            // Create all node pairs in one batch
            var pairs = []
            var guiCfg = scene.sceneGuiConfig
            const invZoom = 1 / guiCfg.zoomFactor
            const left = guiCfg.contentX * invZoom
            const top = guiCfg.contentY * invZoom
            const right = left + guiCfg.sceneViewWidth * invZoom - 380
            const bottom = top + guiCfg.sceneViewHeight * invZoom - 130

            for (let i = 0; i < nodeCount; ++i) {
                const xPos = spawnInsideView ? randBetween(left, right)
                                             : Math.random() * guiCfg.contentWidth
                const yPos = spawnInsideView ? randBetween(top, bottom)
                                             : Math.random() * guiCfg.contentHeight

                pairs.push({ xPos, yPos, nodeName: "test_" + i })
            }

            scene.createPairNodes(pairs)

            var elapsedTime = Date.now() - startTime
            statusText.text = "Completed in " + elapsedTime + "ms"
            statusText.color = "#4CAF50"
            console.log("Elapsed time: " + elapsedTime + "ms")
            startButton.enabled = true
            busyIndicator.running = false
            running = false
        }
    }

    // Node monitoring timer (updates every 500ms)
    Timer {
        interval: 500
        running: true
        repeat: true
        onTriggered: {
            updateNodesInfo()
        }
    }

    // Update monitoring information
    function updateNodesInfo() {
        if (scene) {
            nodeCountText.text = Object.keys(scene.nodes).length
            linkCountText.text = Object.keys(scene.links).length
            selectedCountText.text = Object.keys(scene.selectionModel?.selectedModel).length ?? 0
        }
    }

    // Load Font Awesome fonts
    FontLoader { source: "qrc:/PerformanceAnalyzer/resources/fonts/Font Awesome 6 Pro-Thin-100.otf" }
    FontLoader { source: "qrc:/PerformanceAnalyzer/resources/fonts/Font Awesome 6 Pro-Solid-900.otf" }
    FontLoader { source: "qrc:/PerformanceAnalyzer/resources/fonts/Font Awesome 6 Pro-Regular-400.otf" }
    FontLoader { source: "qrc:/PerformanceAnalyzer/resources/fonts/Font Awesome 6 Pro-Light-300.otf" }
}

Key Features:
- Performance Controls: Panel for configuring and starting tests
- Scene Monitor: Real-time statistics display
- Batch Creation: Creates all node pairs in one operation
- Timing: Measures and displays operation times
- Spawn Modes: Inside viewport or across entire scene

Main Application Layout


Step 7: Build and Run

7.1 Configure Build

  1. Create a build directory:
    bash mkdir build cd build

  2. Configure with CMake:
    bash cmake .. -DCMAKE_PREFIX_PATH=<Qt_Install_Path>

  3. Build the project:
    bash cmake --build .

7.2 Run the Application

Run the executable:

./PerformanceAnalyzer  # Linux/Mac
PerformanceAnalyzer.exe  # Windows

Step 8: Using the Performance Analyzer

Basic Usage

  1. Configure Test:
  2. Enter number of node pairs (1-10000)
  3. Choose spawn mode: "Inside View" or "Across Scene"
  4. Click "Start Test"

  5. Monitor Performance:

  6. Watch the Scene Monitor for real-time statistics
  7. Check console for detailed timing information
  8. Status text shows operation completion time

  9. Test Operations:

  10. Select All: Select all nodes/links (Ctrl+A)
  11. Clear Selection: Deselect all items
  12. Clear Scene: Remove all nodes and links

Performance Testing Scenarios

Scenario 1: Small Scale (100 pairs)
- Node Pairs: 100
- Expected: Fast creation (< 100ms)
- Use Case: Testing basic functionality

Scenario 2: Medium Scale (1000 pairs)
- Node Pairs: 1000
- Expected: Moderate creation time (< 1000ms)
- Use Case: Testing typical application loads

Scenario 3: Large Scale (5000 pairs)
- Node Pairs: 5000
- Expected: Longer creation time (< 5000ms)
- Use Case: Stress testing

Scenario 4: Extreme Scale (10000 pairs)
- Node Pairs: 10000
- Expected: Maximum creation time
- Use Case: Finding performance limits

Performance Testing

Interpreting Results

Creation Time:
- Measures time to create nodes, ports, and links
- Includes batch operations
- Displayed in milliseconds

Selection Time:
- Measures time to select all items
- Includes nodes, links, and containers
- Displayed in milliseconds

Scene Statistics:
- Nodes: Total number of nodes in scene
- Links: Total number of links in scene
- Selected: Number of currently selected items

Performance Tips

  1. Batch Operations: Always use batch creation methods for multiple nodes
  2. Spawn Mode: "Inside View" is faster than "Across Scene"
  3. Clear Before Test: Clear scene before running new tests for accurate timing
  4. Monitor Memory: Watch memory usage with large scenes
  5. Console Logging: Check console for detailed performance metrics

Architecture Overview

Component Hierarchy

PerformanceScene (Scene)
β”œβ”€β”€ NodeRegistry (defines node types)
β”œβ”€β”€ SelectionModel (manages selection)
β”œβ”€β”€ UndoCore (undo/redo support)
└── Batch Operations
    β”œβ”€β”€ createPairNodes() (batch node creation)
    └── createLinks() (batch link creation)

Main Window
β”œβ”€β”€ PerformanceAnalyzerView (canvas)
β”‚   β”œβ”€β”€ NodesScene
β”‚   └── SideMenu
β”œβ”€β”€ Performance Controls Panel
β”‚   β”œβ”€β”€ Node pair input
β”‚   β”œβ”€β”€ Spawn mode toggle
β”‚   β”œβ”€β”€ Start test button
β”‚   └── Operation buttons
└── Scene Monitor Panel
    β”œβ”€β”€ Node count
    β”œβ”€β”€ Link count
    └── Selected count

Performance Optimizations

  1. Pre-allocated Arrays: Arrays are pre-sized to avoid resizing
  2. Batch Operations: Multiple nodes/links created in single operations
  3. Minimal Validation: Reduced validation during batch creation
  4. Efficient Clearing: Garbage collection before scene clearing
  5. Lazy Updates: UI updates only when necessary

Architecture Diagram

Key Concepts

Batch Creation

Instead of creating nodes one by one, batch creation:
- Creates all nodes in memory first
- Adds them to the scene in one operation
- Creates all links in one operation
- Significantly faster than individual creation

Performance Metrics

Key metrics to monitor:
- Creation Time: Time to create nodes and links
- Selection Time: Time to select/deselect items
- Clear Time: Time to clear the scene
- Memory Usage: Memory consumed by the scene
- Frame Rate: Rendering performance

Spawn Modes

Inside View:
- Nodes created only in visible viewport
- Faster rendering
- Better for testing viewport performance

Across Scene:
- Nodes created across entire scene
- Tests scene-wide performance
- Better for testing scalability

Extending the Performance Analyzer

Adding New Metrics

To add new performance metrics:

  1. Add monitoring code in Main.qml:
    qml function measureOperation() { const startTime = Date.now() // Perform operation const elapsed = Date.now() - startTime console.log("Operation time:", elapsed, "ms") }

  2. Add UI elements to display metrics

  3. Update monitoring timer to refresh metrics

Custom Node Types

To test with custom node types:

  1. Add node types to CSpecs.qml
  2. Create node QML files
  3. Register in PerformanceScene.qml
  4. Modify createPairNodes() to use custom types

Performance Profiling

Integrate with profiling tools:
- Qt Creator Profiler
- Chrome DevTools (for QML)
- Custom performance counters
- Memory profilers

Troubleshooting

Common Issues

  1. Slow Creation:
  2. Reduce node count
  3. Use "Inside View" mode
  4. Check for memory issues
  5. Verify batch operations are used

  6. High Memory Usage:

  7. Clear scene regularly
  8. Monitor memory with profiling tools
  9. Reduce node count if needed

  10. UI Freezing:

  11. Use Qt.callLater() for heavy operations
  12. Break operations into smaller chunks
  13. Show busy indicator during operations

  14. Inaccurate Timing:

  15. Clear scene before each test
  16. Close other applications
  17. Run multiple tests and average results

Debug Tips

Performance Benchmarks

Typical Performance (Reference)

These are example benchmarks - actual performance depends on hardware:

Node Pairs Nodes Links Creation Time Selection Time
100 200 100 ~50ms ~10ms
500 1000 500 ~200ms ~50ms
1000 2000 1000 ~400ms ~100ms
5000 10000 5000 ~2000ms ~500ms
10000 20000 10000 ~4000ms ~1000ms

Note: These are approximate values and will vary based on:
- Hardware specifications
- Operating system
- Qt version
- NodeLink optimizations

Conclusion

The Performance Analyzer Example provides essential tools for:
- Benchmarking: Measure NodeLink's performance
- Optimization: Identify performance bottlenecks
- Testing: Verify framework scalability
- Development: Test during framework development

Key takeaways:
- Batch Operations: Always use batch methods for multiple items
- Performance Monitoring: Real-time statistics help identify issues
- Scalability: NodeLink can handle large scenes efficiently
- Optimization: Pre-allocation and batching significantly improve performance

This example serves as a foundation for:
- Performance testing in custom applications
- Benchmarking framework improvements
- Stress testing production applications
- Capacity planning for large-scale deployments

For more examples, see the other examples in the NodeLink repository.

Final Example

πŸ—οΈ Architecture (MVC)

NodeLink follows a Model-View-Controller pattern. You typically implement or configure:

  • A graph model that knows which nodes exist, how they are connected, and what data they carry.
  • A NodeEditor view that visualizes the model using QML components.
  • A controller layer (C++ / QML logic) that translates UI actions into model changes.
                

                
Application Layer
Your custom nodes, scenes, and application logic
β–Ό
View Layer (View)
NLView β†’ NodesScene β†’ NodeView, LinkView, PortView (Visual representation and user interaction)
β–Ό
Controller / Coordinator Layer
SceneSession, UndoCore, NLView (Coordinates between Model and View)
β–Ό
Model Layer (Core)
Scene, Node, Link, Port, Container, SelectionModel (Data models and business logic)
β–Ό
Persistence Layer
QtQuickStream (QSRepository, QSSerializer) (Serialization and storage)

Directory Structure

                
resources/
β”œβ”€β”€ Core/          # Model Layer
β”‚   β”œβ”€β”€ Scene.qml
β”‚   β”œβ”€β”€ Node.qml
β”‚   β”œβ”€β”€ Link.qml
β”‚   β”œβ”€β”€ Port.qml
β”‚   β”œβ”€β”€ Container.qml
β”‚   β”œβ”€β”€ SelectionModel.qml
β”‚   └── ...
β”‚
└── View/          # View Layer
    β”œβ”€β”€ NLView.qml
    β”œβ”€β”€ NodesScene.qml
    β”œβ”€β”€ NodeView.qml
    β”œβ”€β”€ LinkView.qml
    β”œβ”€β”€ PortView.qml
    β”œβ”€β”€ SceneSession.qml
    └── ...
                
            

MVC Pattern in NodeLink (Traditional MVC vs NodeLink MVC)

Traditional MVC:

  • Model: Data and business logic
  • View: User interface
  • Controller: Handles user input and updates Model/View

NodeLink MVC:

  • Model (Core): Data models, business logic, scene management
  • View (View): Visual components, rendering, user interaction
  • Controller/Coordinator: SceneSession, UndoCore, NLView coordinate between Model and View

Key Differences:

  • QML Property Binding: Views automatically update when models change (declarative)
  • Signal/Slot Communication: Models emit signals, views react via Connections
  • SceneSession: Acts as a ViewModel/Controller hybrid managing view state
  • Separation: Clear separation between data (Core) and presentation (View)

Model Layer (Core)

Purpose:

The Model layer contains all data structures, business logic, and scene management. Models are data-only and have no knowledge of how they're displayed.

Core Components

Scene (resources/Core/Scene.qml)

The root model managing all nodes, links, and containers:

// resources/Core/Scene.qml
I_Scene {
  // Data properties
  property var nodes: ({})           // Map
    property var links: ({})           // Map
    property var containers: ({})      // Map

    // Business logic
    function addNode(node) { ... }
    function deleteNode(uuid) { ... }
    function linkNodes(portA, portB) { ... }
    function unlinkNodes(portA, portB) { ... }

    // Signals
    signal nodeAdded(Node node)
    signal nodeRemoved(Node node)
    signal linkAdded(Link link)
    signal linkRemoved(Link link)
}

Responsibilities:

  • Manage collection of nodes, links, containers
  • Provide business logic (add, delete, link, unlink)
  • Emit signals when data changes
  • No UI/rendering logic

Data Flow

Model β†’ View Flow

Property Binding (Automatic):

// View automatically updates when model changes
NodeView {
    x: node.guiConfig.position.x      // Bound to model
    y: node.guiConfig.position.y      // Bound to model
    color: node.guiConfig.color       // Bound to model
}

Signal/Slot (Reactive):

// View reacts to model signals
Connections {
    target: scene
    function onNodeAdded(node) {
        // Create view for new node
        createNodeView(node)
    }
}

View β†’ Model Flow

Direct Property Updates:

// View updates model when user interacts
MouseArea {
    onPositionChanged: {
        // Update model position
        node.guiConfig.position.x += deltaX
        node.guiConfig.position.y += deltaY
    }
}

Function Calls:

// View calls model functions
MouseArea {
    onClicked: {
        // Call model function
        scene.selectionModel.selectNode(node)
    }
}

Controller Coordination

SceneSession Coordinates State:

// Controller manages view state
SceneSession {
    property bool connectingMode: false
}

// View reads controller state
NodeView {
    enabled: !sceneSession.connectingMode
}

// Model operation updates controller
Scene {
    function linkNodes(portA, portB) {
        // ... link logic ...
        sceneSession.connectingMode = false  // Update controller
    }
}

Complete Data Flow Example

User Drags Node:

1. User drags NodeView (View)
   ↓
2. NodeView.MouseArea.onPositionChanged (View)
   ↓
3. node.guiConfig.position.x += deltaX (Model update)
   ↓
4. NodeView.x property binding updates (View auto-update)
   ↓
5. PortView positions update (View auto-update)
   ↓
6. LinkView repaints (View auto-update)
   ↓
7. UndoCore observer detects change (Controller)
   ↓
8. UndoCore creates command (Controller)

Component Hierarchy

Model Hierarchy

Scene (I_Scene)
β”œβ”€β”€ Node (I_Node)
β”‚   β”œβ”€β”€ Port
β”‚   β”œβ”€β”€ NodeGuiConfig
β”‚   └── I_NodeData
β”œβ”€β”€ Link
β”‚   β”œβ”€β”€ Port (inputPort)
β”‚   β”œβ”€β”€ Port (outputPort)
β”‚   └── LinkGUIConfig
β”œβ”€β”€ Container
β”‚   β”œβ”€β”€ Node (contained)
β”‚   └── ContainerGuiConfig
β”œβ”€β”€ SelectionModel
└── SceneGuiConfig

View Hierarchy

NLView
β”œβ”€β”€ NodesScene (I_NodesScene)
β”‚   β”œβ”€β”€ NodesRect (I_NodesRect)
β”‚   β”‚   β”œβ”€β”€ NodeView (I_NodeView)
β”‚   β”‚   β”‚   β”œβ”€β”€ PortView
β”‚   β”‚   β”‚   └── ContentItem (custom)
β”‚   β”‚   β”œβ”€β”€ LinkView (I_LinkView)
β”‚   β”‚   └── ContainerView
β”‚   └── SceneViewBackground
β”œβ”€β”€ NodesOverview
└── SideMenu

Controller Hierarchy

NLView (Coordinator)
β”œβ”€β”€ SceneSession (View State)
β”‚   └── ZoomManager
└── UndoCore (Undo/Redo)
    β”œβ”€β”€ UndoStack
    └── Observers
        β”œβ”€β”€ UndoNodeObserver
        β”œβ”€β”€ UndoLinkObserver
        └── UndoSceneObserver

🧩 Nodes

(resources/Core/Node.qml)

Represents a single node in the graph:

// resources/Core/Node.qml
I_Node {
  // Data properties
  property string title: ""
    property int type: 0
    property var ports: ({})           // Map
    property var children: ({})        // Map
    property var parents: ({})         // Map

    // Configuration
    property NodeGuiConfig guiConfig: NodeGuiConfig { ... }
    property I_NodeData nodeData: null

    // Business logic
    function addPort(port) { ... }
    function deletePort(port) { ... }

    // Signals
    signal portAdded(var portId)
    signal nodeCompleted()
}
            

Responsibilities:

  • Store node data (title, type, ports)
  • Manage parent/child relationships
  • Provide node-specific logic
  • No visual representation

πŸ”Œ Ports

(resources/Core/Port.qml)

Represents a connection point on a node:

// resources/Core/Port.qml
QSObject {
    // Data properties
    property Node node: null           // Parent node
    property int portType: NLSpec.PortType.Input
    property int portSide: NLSpec.PortPositionSide.Left
    property string title: ""
    property string color: "white"
    property bool enable: true

    // Computed position (set by view)
    property vector2d _position: Qt.vector2d(0, 0)
}
            

Responsibilities:

  • Store port data (type, side, title)
  • Reference parent node
  • No visual representation

SelectionModel

(resources/Core/SelectionModel.qml)

Manages selected objects:

// resources/Core/SelectionModel.qml
QtObject {
  // Data properties
  property var selectedModel: ({})   // Map
    property var existObjects: []      // All object UUIDs

    // Business logic
    function selectNode(node) { ... }
    function clear() { ... }
    function isSelected(uuid) { ... }

    // Signals
    signal selectedObjectChanged()
}
            

Responsibilities:

  • Track selected nodes, links, containers
  • Provide selection logic
  • Emit signals when selection changes
  • No visual representation

Model Characteristics:

  • Data-Only: Models contain no QML visual elements (Rectangle, Item, etc.)
  • Business Logic: Models contain functions for manipulating data
  • Signals: Models emit signals when data changes
  • Serializable: Models inherit from QSObject for serialization
  • No View Dependencies: Models don't import or reference View components

View Layer (View)

Purpose:

The View layer contains all visual components that render the models. Views are presentation-only and react to model changes.

NLView

(resources/View/NLView.qml)

The main view component that coordinates the entire UI:

// resources/View/NLView.qml
Item {
    property Scene scene
    property SceneSession sceneSession: SceneSession {}

    // Main scene view
    Loader {
        sourceComponent: nodesScene
    }

    // Overview
    NodesOverview {
        scene: view.scene
        sceneSession: view.sceneSession
    }

    // Side menu
    SideMenu {
        scene: view.scene
        sceneSession: view.sceneSession
    }
}

Responsibilities:

  • Compose main UI (scene, overview, menu)
  • Coordinate between components
  • Handle copy/paste operations

NodeView

(resources/View/NodeView.qml)

Visual representation of a Node:

// resources/View/NodeView.qml
InteractiveNodeView {
    property var node              // Model reference
    property I_Scene scene
    property SceneSession sceneSession

    // Visual properties bound to model
    x: node.guiConfig.position.x
    y: node.guiConfig.position.y
    width: node.guiConfig.width
    height: node.guiConfig.height
    color: node.guiConfig.color

    // Selection state bound to model
    property bool isSelected: scene.selectionModel.isSelected(node._qsUuid)

    // User interaction
    MouseArea {
        onClicked: {
            scene.selectionModel.selectNode(node)
        }
        onPositionChanged: {
            // Update model position
            node.guiConfig.position.x += deltaX
            node.guiConfig.position.y += deltaY
        }
    }
}

Responsibilities:

  • Render node visually (Rectangle, text, etc.)
  • Handle user interaction (click, drag)
  • Update model when user interacts
  • React to model changes (property binding)

LinkView

(resources/View/LinkView.qml)

Visual representation of a Link:

// resources/View/LinkView.qml
Canvas {
    property var link              // Model reference
    property I_Scene scene
    property SceneSession sceneSession

    // Visual properties bound to model
    property vector2d inputPos: link.inputPort._position
    property vector2d outputPos: link.outputPort._position
    property bool isSelected: scene.selectionModel.isSelected(link._qsUuid)

    // Paint link
    onPaint: {
        var context = getContext("2d")
        LinkPainter.createLink(context, inputPos, outputPos, ...)
    }
}

Responsibilities:

  • Render link visually (Canvas, Bezier curves)
  • React to model changes (port positions, selection)
  • Handle link-specific rendering

PortView

(resources/View/PortView.qml)

Visual representation of a Port:

// resources/View/PortView.qml
Rectangle {
    property Port port             // Model reference
    property var node              // Parent node view

    // Visual properties bound to model
    color: port.color
    visible: port.enable

    // Update model position
    Component.onCompleted: {
        port._position = Qt.vector2d(x, y)
    }
}

Responsibilities:

  • Render port visually (circle, rectangle)
  • Update model position when moved
  • Handle port interaction (hover, click)

View Characteristics

  1. Visual Only: Views contain QML visual elements (Rectangle, Canvas, etc.)
  2. Property Binding: Views bind to model properties for automatic updates
  3. User Interaction: Views handle mouse/keyboard events
  4. Model Updates: Views update models when user interacts
  5. Reactive: Views automatically update when models change

Controller/Coordinator Layer

Purpose:

The Controller/Coordinator layer bridges the Model and View layers, managing state, coordinating operations, and handling complex interactions.

SceneSession

(resources/View/SceneSession.qml)

Manages view state and coordinates between model and view:

// resources/View/SceneSession.qml
QtObject {
    // View state (not saved)
    property bool connectingMode: false
    property bool isShiftModifierPressed: false
    property bool isCtrlPressed: false
    property bool isRubberBandMoving: false
    property bool isSceneEditable: true

    // View configuration
    property ZoomManager zoomManager: ZoomManager {}
    property var portsVisibility: ({})
    property var linkColorOverrideMap: ({})

    // Signals
    signal sceneForceFocus
    signal marqueeSelectionStart(var mouse)
    signal updateMarqueeSelection(var mouse)
}

Responsibilities:

  • Manage view state (selection mode, connecting mode, etc.)
  • Coordinate zoom/pan operations
  • Handle view-specific settings (port visibility, link colors)
  • Provide signals for view coordination
  • Not serialized (temporary state)

UndoCore

(resources/Core/Undo/UndoCore.qml)

Manages undo/redo operations:

// resources/Core/Undo/UndoCore.qml
QtObject {
    property Scene scene
    property UndoStack undoStack: UndoStack {}

    // Observer pattern
    property UndoNodeObserver nodeObserver: UndoNodeObserver {}
    property UndoLinkObserver linkObserver: UndoLinkObserver {}
    property UndoSceneObserver sceneObserver: UndoSceneObserver {}
}

Responsibilities:

  • Track model changes via observers
  • Create commands for undo/redo
  • Execute undo/redo operations
  • Coordinate between model and view during undo/redo

NLViewController

(resources/View/NLView.qml)

Main coordinator component:

// resources/View/NLView.qml
Item {
    property Scene scene
    property SceneSession sceneSession: SceneSession {}

    // Coordinate copy/paste
    function copyNodes() {
        // Copy from model
        var selectedNodes = Object.values(scene.selectionModel.selectedModel)
        // Store in NLCore
        NLCore._copiedNodes = selectedNodes
    }

    function pasteNodes() {
        // Create new nodes from copied data
        // Add to scene
        scene.addNodes(newNodes, false)
    }
}

Responsibilities:

  • Coordinate between scene (model) and views
  • Handle complex operations (copy/paste, clone)
  • Manage component lifecycle
  • Bridge model and view layers

Controller Characteristics:

  1. State Management: Controllers manage view state and temporary data
  2. Coordination:Controllers coordinate between models and views
  3. Complex Operations:Controllers handle multi-step operations
  4. No Direct Rendering:Controllers don't render UI directly
  5. Business Logic:Controllers contain coordination logic

Key Design Patterns

1. Observer Pattern

Models emit signals, views observe:

// Model emits signal
Scene {
    signal nodeAdded(Node node)

    function addNode(node) {
        nodes[node._qsUuid] = node
        nodeAdded(node)  // Emit signal
    }
}

// View observes
Connections {
    target: scene
    function onNodeAdded(node) {
        createNodeView(node)  // React
    }
}

2. Property Binding

Views automatically update when models change:

// View binds to model
NodeView {
    x: node.guiConfig.position.x  // Auto-updates
    y: node.guiConfig.position.y  // Auto-updates
}

3. Factory Pattern

NLCore provides factory functions:

// Factory creates models
var node = NLCore.createNode()
var port = NLCore.createPort()
var link = NLCore.createLink()

4. Command Pattern

Undo/Redo uses commands:

// Command encapsulates operation
AddNodeCommand {
    node: newNode
    scene: scene

    function execute() {
        scene.addNode(node)
    }

    function undo() {
        scene.deleteNode(node._qsUuid)
    }
}

5. Registry Pattern

NLNodeRegistry maps types to components:

// Registry maps type to component
NLNodeRegistry {
    nodeTypes: {
        0: "SourceNode",
        1: "AdditiveNode"
    }
    nodeView: "NodeView.qml"
    linkView: "LinkView.qml"
}

🧯 Troubleshooting

Common issues

  • Empty or broken scene: verify the QML import (import NodeLink 1.0) and ensure the module is deployed and found by the engine.
  • Missing Qt modules: ensure QtQuick and QtQuick.Controls are installed and available in your Qt installation.
  • CMake errors: remove your build directory and reconfigure.
  • Submodule problems: run git submodule update --init --recursive.

❓ FAQ

Can I use NodeLink without QML?

The core concept (graph model) can be used headlessly, but NodeLink is primarily designed as a QML node editor. For pure C++ or non-Qt UIs, you would need to build your own view layer.

Can I embed multiple NodeEditors in one app?

Yes. You can either attach each editor to an independent model, or share a model between editors for multiple views of the same graph.

Is NodeLink suitable for commercial projects?

Yes. It is licensed under Apache-2.0, which permits use in both open-source and closed-source applications, as long as you respect the license terms.

πŸ“„ License & Attribution

NodeLink is released under the Apache License, Version 2.0. You are free to:

  • Use the library in open-source and commercial products.
  • Modify and redistribute the code under the same license.
  • Bundle NodeLink with your Qt applications.

When redistributing, you must:

  • Include the LICENSE file from the repository.
  • Provide attribution (for example, β€œNodeLink by RONIA AB”).

Core Library (The Engine)

Overview

The Core Library is the engine that powers NodeLink. It provides the fundamental data models, business logic, and core services for building node-based applications. The Core Library is completely independent of the View layer, making it possible to use NodeLink's engine with different UI implementations.

Core Library Architecture

Purpose

The Core Library provides:

  • Data Models: Scene, Node, Link, Port, Container
  • Business Logic: Scene management, node operations, linking
  • Core Services: Factory functions, registry, selection, undo/redo
  • Serialization: Integration with QtQuickStream
  • Type System: Specifications, enums, interfaces

Key Principles

  1. Data-Only: Core components contain no UI/rendering logic
  2. Serializable: All components inherit from QSObject for persistence
  3. Signal-Based: Components emit signals for change notification
  4. Extensible: Interface-based design allows customization
  5. Independent: No dependencies on View layer

Directory Structure

resources/Core/
β”œβ”€β”€ NLCore.qml              # Engine singleton
β”œβ”€β”€ NLSpec.qml              # Specifications and enums
β”œβ”€β”€ NLNodeRegistry.qml      # Node type registry
β”œβ”€β”€ NLUtils.qml             # Utilities
β”‚
β”œβ”€β”€ I_Scene.qml             # Scene interface
β”œβ”€β”€ Scene.qml               # Scene implementation
β”œβ”€β”€ SceneGuiConfig.qml      # Scene GUI configuration
β”‚
β”œβ”€β”€ I_Node.qml              # Node interface
β”œβ”€β”€ Node.qml                # Node implementation
β”œβ”€β”€ NodeGuiConfig.qml       # Node GUI configuration
β”œβ”€β”€ I_NodeData.qml          # Node data interface
β”œβ”€β”€ NodeData.qml            # Node data implementation
β”‚
β”œβ”€β”€ Link.qml                # Link model
β”œβ”€β”€ LinkGUIConfig.qml       # Link GUI configuration
β”‚
β”œβ”€β”€ Port.qml                # Port model
β”‚
β”œβ”€β”€ Container.qml           # Container model
β”œβ”€β”€ ContainerGuiConfig.qml  # Container GUI configuration
β”‚
β”œβ”€β”€ SelectionModel.qml      # Selection management
β”œβ”€β”€ SelectionSpecificTool.qml
β”‚
β”œβ”€β”€ ImagesModel.qml         # Image management
β”‚
└── Undo/                   # Undo/Redo system
    β”œβ”€β”€ UndoCore.qml
    β”œβ”€β”€ UndoStack.qml
    β”œβ”€β”€ CommandStack.qml
    └── Commands/
        β”œβ”€β”€ AddNodeCommand.qml
        β”œβ”€β”€ RemoveNodeCommand.qml
        β”œβ”€β”€ CreateLinkCommand.qml
        └── ...

NLCore - The Engine Singleton

Overview

NLCore is the central singleton that provides factory functions and core services for NodeLink.

Location: resources/Core/NLCore.qml

Type: QML Singleton (pragma Singleton)

Properties

NLCore {
    // Default repository (QtQuickStream)
    property QSRepository defaultRepo: QSRepository { ... }

    // Copy/paste storage
    property var _copiedNodes: ({})
    property var _copiedLinks: ({})
    property var _copiedContainers: ({})
}
        

Factory Functions

Create Scene

// Create a new scene
        var scene = NLCore.createScene();
        scene.title = "My Scene";
        

Implementation:

function createScene() {
            let obj = QSSerializer.createQSObject("Scene", ["NodeLink"], defaultRepo);
            obj._qsRepo = defaultRepo;
            return obj;
}

Create Node

// Create a new node
var node = NLCore.createNode();
node.type = 0;
node.title = "My Node";

Implementation:

function createNode() {
    let obj = QSSerializer.createQSObject("Node", ["NodeLink"], defaultRepo);
    obj._qsRepo = defaultRepo;
    return obj;
}

Create Port

// Create a new port
var port = NLCore.createPort();
port.portType = NLSpec.PortType.Input;
port.portSide = NLSpec.PortPositionSide.Left;
port.title = "input";

Implementation:

function createPort(qsRepo = null) {
    if (!qsRepo)
        qsRepo = defaultRepo;

    let obj = QSSerializer.createQSObject("Port", ["NodeLink"], qsRepo);
    obj._qsRepo = qsRepo;
    return obj;
}
// Create a new link
var link = NLCore.createLink();
link.inputPort = inputPort;
link.outputPort = outputPort;

Implementation:

function createLink() {
    let obj = QSSerializer.createQSObject("Link", ["NodeLink"], defaultRepo);
    obj._qsRepo = defaultRepo;
    return obj;
}

Default Repository

NLCore.defaultRepo is the default QtQuickStream repository used for serialization:

// Initialize repository
NLCore.defaultRepo = NLCore.createDefaultRepo([
    "QtQuickStream",
    "NodeLink",
    "YourApp"
]);

// Initialize root object
NLCore.defaultRepo.initRootObject("Scene");

Copy/Paste Storage

NLCore provides temporary storage for copy/paste operations:

// Copy nodes
NLCore._copiedNodes = selectedNodes;
NLCore._copiedNodesChanged();

// Paste nodes
var copiedNodes = NLCore._copiedNodes;
// ... create new nodes from copied data ...

Core Components

Scene

Location: resources/Core/Scene.qml

Interface: I_Scene (resources/Core/I_Scene.qml)

The Scene is the root model that manages all nodes, links, and containers in a graph.

Properties

Scene {
    // Collections
    property var nodes: ({})           // Map<UUID, Node>
    property var links: ({})           // Map<UUID, Link>
    property var containers: ({})      // Map<UUID, Container>

    // Metadata
    property string title: "<Untitled>"

    // Services
    property SelectionModel selectionModel: SelectionModel { ... }
    property NLNodeRegistry nodeRegistry: null
    property UndoCore _undoCore: UndoCore { ... }

    // Configuration
    property SceneGuiConfig sceneGuiConfig: SceneGuiConfig { ... }
}

Key Functions

// Node management
function addNode(node) { ... }
function deleteNode(uuid) { ... }
function addNodes(nodeArray, autoSelect = true) { ... }
function deleteNodes(nodeUUIds) { ... }
function cloneNode(uuid) { ... }

// Link management
function linkNodes(portA, portB) { ... }
function unlinkNodes(portA, portB) { ... }
function createLink(portA, portB) { ... }

// Container management
function addContainer(container) { ... }
function deleteContainer(uuid) { ... }
function createContainer() { ... }
function cloneContainer(uuid) { ... }

// Utilities
function findNode(uuid) { ... }
function findPort(uuid) { ... }
function canLinkNodes(portA, portB) { ... }
function createCustomizeNode(nodeType, xPos, yPos) { ... }

Signals

signal nodeAdded(Node node)
signal nodeRemoved(Node node)
signal nodesAdded(list<Node> nodes)
signal nodesRemoved(list<Node> nodes)

signal linkAdded(Link link)
signal linkRemoved(Link link)
signal linksAdded(list<Link> links)

signal containerAdded(Container container)
signal containerRemoved(Container container)

Node

Location: resources/Core/Node.qml

Interface: I_Node (resources/Core/I_Node.qml)

A Node represents a single element in the graph with ports for connections.

Properties

Node {
    // Identity
    property string title: "<No Title>"
    property int type: 0
    property int objectType: NLSpec.ObjectType.Node

    // Structure
    property var ports: ({})           // Map<UUID, Port>
    property var children: ({})        // Map<UUID, Node>
    property var parents: ({})         // Map<UUID, Node>

    // Configuration
    property NodeGuiConfig guiConfig: NodeGuiConfig { ... }
    property I_NodeData nodeData: null
    property ImagesModel imagesModel: ImagesModel { ... }
}

Key Functions

// Port management
function addPort(port) { ... }
function deletePort(port) { ... }
function findPort(uuid) { ... }
function findPortByPortSide(side) { ... }

Signals

signal portAdded(var portId)
signal nodeCompleted()

Location: resources/Core/Link.qml

A Link represents a connection between two ports.

Properties

Link {
    // Connection
    property Port inputPort: null
    property Port outputPort: null

    // Geometry
    property var controlPoints: []     // Array of vector2d

    // Configuration
    property int direction: NLSpec.LinkDirection.LeftToRight
    property LinkGUIConfig guiConfig: LinkGUIConfig { ... }
}

Port

Location: resources/Core/Port.qml

A Port represents a connection point on a node.

Properties

Port {
    // Parent
    property Node node: null           // Parent node

    // Type and position
    property int portType: NLSpec.PortType.Input
    property int portSide: NLSpec.PortPositionSide.Left

    // Appearance
    property string title: ""
    property string color: "white"
    property bool enable: true

    // Position (set by view)
    property vector2d _position: Qt.vector2d(0, 0)
    property real _measuredTitleWidth: 0
}

Container

Location: resources/Core/Container.qml

Interface: I_Node (inherits from)

A Container groups nodes and other containers together.

Properties

Container {
    // Identity
    property string title: "Untitled"
    property int objectType: NLSpec.ObjectType.Container

    // Contents
    property var nodes: ({})           // Map<UUID, Node>
    property var containersInside: ({}) // Map<UUID, Container>

    // Configuration
    property ContainerGuiConfig guiConfig: ContainerGuiConfig { ... }
}

Key Functions

function addNode(node) { ... }
function removeNode(node) { ... }
function addContainerInside(container) { ... }
function removeContainerInside(container) { ... }

Interfaces

I_Node

Location: resources/Core/I_Node.qml

Base interface for all objects in the scene (Node, Container).

I_Node {
    property int objectType: NLSpec.ObjectType.Unknown
    property I_NodeData nodeData: null

    signal cloneFrom(baseNode: I_Node)

    onCloneFrom: function (baseNode) {
        objectType = baseNode.objectType;
        nodeData?.setProperties(baseNode.nodeData);
    }
}

Purpose:

  • Provides common interface for Node and Container
  • Enables polymorphic operations
  • Supports cloning mechanism

I_Scene

Location: resources/Core/I_Scene.qml

Base interface for Scene.

I_Scene {
    property string title: "<Untitled>"
    property var nodes: ({})
    property var links: ({})
    property var containers: ({})
    property SelectionModel selectionModel: null
    property NLNodeRegistry nodeRegistry: null
    property SceneGuiConfig sceneGuiConfig: SceneGuiConfig { ... }

    signal nodeAdded(Node node)
    signal nodeRemoved(Node node)
    // ... more signals ...
}

Purpose:

  • Defines Scene contract
  • Enables custom Scene implementations
  • Provides type safety

I_NodeData

Location: resources/Core/I_NodeData.qml

Base interface for node-specific data.

I_NodeData {
    property var data: null
}

Purpose:

  • Stores node-specific data
  • Enables type-safe data access
  • Supports serialization

Usage:

// Custom node data
NodeData {
    property real value: 0.0
    property string text: ""
}

Configuration Objects

NodeGuiConfig

Location: resources/Core/NodeGuiConfig.qml

Stores GUI-related properties for nodes.

NodeGuiConfig {
    property string description: ""
    property string logoUrl: ""
    property vector2d position: Qt.vector2d(0, 0)
    property real width: 200
    property real height: 150
    property string color: "#4A90E2"
    property int colorIndex: 0
    property real opacity: 1.0
    property bool locked: false
    property bool autoSize: true
    property real minWidth: 120
    property real minHeight: 80
    property real baseContentWidth: 0
}

LinkGUIConfig

Location: resources/Core/LinkGUIConfig.qml

Stores GUI-related properties for links.

LinkGUIConfig {
    property string description: ""
    property string color: "#FFFFFF"
    property int colorIndex: 0
    property int style: NLSpec.LinkStyle.Solid
    property int type: NLSpec.LinkType.Bezier
    property bool _isEditableDescription: true
}

ContainerGuiConfig

Location: resources/Core/ContainerGuiConfig.qml

Stores GUI-related properties for containers.

ContainerGuiConfig {
    property real width: 400
    property real height: 300
    property string color: "#4A90E2"
    property int colorIndex: 0
    property vector2d position: Qt.vector2d(0, 0)
    property bool locked: false
    property real containerTextHeight: 30
}

SceneGuiConfig

Location: resources/Core/SceneGuiConfig.qml

Stores GUI-related properties for scenes.

SceneGuiConfig {
    property real zoomFactor: 1.0
    property real contentWidth: 10000
    property real contentHeight: 10000
    property real contentX: 0
    property real contentY: 0
    property real sceneViewWidth: 1280
    property real sceneViewHeight: 960
    property vector2d _mousePosition: Qt.vector2d(0, 0)
}

Registry System

NLNodeRegistry

Location: resources/Core/NLNodeRegistry.qml

Maps node type IDs to QML component names, display names, icons, and colors.

Properties

NLNodeRegistry {
    // Imports for creating nodes
    property var imports: []

    // Type mappings
    property var nodeTypes: ({})       // Map<id, "ComponentName">
    property var nodeNames: ({})       // Map<id, "Display Name">
    property var nodeIcons: ({})       // Map<id, "\uf123">
    property var nodeColors: ({})      // Map<id, "#4A90E2">

    // Default node type
    property int defaultNode: 0

    // View component URLs
    property string nodeView: "NodeView.qml"
    property string linkView: "LinkView.qml"
    property string containerView: "ContainerView.qml"
}

Usage

// Register node types
nodeRegistry.imports = ["NodeLink", "MyApp"];

nodeRegistry.nodeTypes[0] = "SourceNode";
nodeRegistry.nodeNames[0] = "Source";
nodeRegistry.nodeIcons[0] = "\uf04b";
nodeRegistry.nodeColors[0] = "#4A90E2";

nodeRegistry.nodeTypes[1] = "AdditiveNode";
nodeRegistry.nodeNames[1] = "Add";
nodeRegistry.nodeIcons[1] = "\uf067";
nodeRegistry.nodeColors[1] = "#9C27B0";

Selection Management

SelectionModel

Location: resources/Core/SelectionModel.qml

Manages selected objects in the scene.

Properties

SelectionModel {
    property var selectedModel: ({})   // Map<UUID, Object>
    property var existObjects: []      // All object UUIDs
    property bool notifySelectedObject: true
}

Key Functions

// Selection operations
function selectNode(node) { ... }
function selectLink(link) { ... }
function selectContainer(container) { ... }
function selectAll(nodes, links, containers) { ... }

// Deselection
function clear() { ... }
function clearAllExcept(uuid) { ... }
function remove(uuid) { ... }

// Queries
function isSelected(uuid): bool { ... }
function lastSelectedObject(objType) { ... }

Signals

signal selectedObjectChanged()

Usage

// Select a node
scene.selectionModel.selectNode(node);

// Check if selected
if (scene.selectionModel.isSelected(node._qsUuid)) {
    // Node is selected
}

// Clear selection
scene.selectionModel.clear();

// Select all
scene.selectionModel.selectAll(scene.nodes, scene.links, scene.containers);

Undo/Redo System

UndoCore

Location: resources/Core/Undo/UndoCore.qml

Manages undo/redo operations for the scene.

UndoCore {
    required property I_Scene scene
    property CommandStack undoStack: CommandStack { }

    // Observers track changes
    property UndoSceneObserver undoSceneObserver: UndoSceneObserver { ... }
    property UndoNodeObserver undoNodeObserver: UndoNodeObserver { ... }
    property UndoLinkObserver undoLinkObserver: UndoLinkObserver { ... }
}

CommandStack

Location: resources/Core/Undo/CommandStack.qml

Manages undo/redo command stacks.

CommandStack {
    property var undoStack: []
    property var redoStack: []
    readonly property bool isValidUndo: undoStack.length > 0
    readonly property bool isValidRedo: redoStack.length > 0
    property bool isReplaying: false

    function push(cmd, appliedAlready = true) { ... }
    function undo() { ... }
    function redo() { ... }
    function clear() { ... }
}

Commands

Commands encapsulate operations for undo/redo:

See: Undo/Redo System Documentation

Specifications and Enums

NLSpec

Location: resources/Core/NLSpec.qml

Type: QML Singleton

Defines all enums and specifications used throughout NodeLink.

Object Types

enum ObjectType {
    Node = 0,
    Link = 1,
    Container = 2,
    Unknown = 99
}

Port Types

enum PortType {
    Input = 0,
    Output = 1,
    InAndOut = 2
}

Port Positions

enum PortPositionSide {
    Top = 0,
    Bottom = 1,
    Left = 2,
    Right = 3,
    Unknown = 99
}
enum LinkType {
    Bezier = 0,      // Bezier curve
    LLine = 1,       // L-shaped line
    Straight = 2,    // Straight line
    Unknown = 99
}
enum LinkDirection {
    Nondirectional = 0,
    Unidirectional = 1,
    Bidirectional = 2
}
enum LinkStyle {
    Solid = 0,
    Dash = 1,
    Dot = 2
}

Selection Tool Types

enum SelectionSpecificToolType {
    Node = 0,        // Single node selection
    Link = 1,        // Single link selection
    Any = 2,         // Single selection (any type)
    All = 3,         // Multiple selection (any types)
    Unknown = 99
}

Selection Type

enum Selection Type {
    Rectangle  = 0,        // Rectangle type selection
    Lasso  = 1,        // Lasso type selection
    Unknown = 99
}

Undo Configuration

property QtObject undo: QtObject {
    property bool blockObservers: false
}

Utilities

NLUtils

Location: resources/Core/NLUtils.qml

Wrapper for C++ utility functions.

NLUtilsCPP {
    // Image conversion
    function imageURLToImageString(url): string { ... }

    // Key sequence formatting
    function keySequenceToString(keySequence): string { ... }
}

See: API Reference: C++ Classes

Repository Management

QtQuickStream Integration

NodeLink uses QtQuickStream for serialization. All Core components inherit from QSObject:

// All core components inherit from QSObject
QSObject {
    property string _qsUuid: ""        // Unique identifier
    property QSRepository _qsRepo: null // Repository reference
    property QSObject _qsParent: null   // Parent object
}

Repository Lifecycle

// 1. Create repository
NLCore.defaultRepo = NLCore.createDefaultRepo([
    "QtQuickStream",
    "NodeLink",
    "YourApp"
]);

// 2. Initialize root object
NLCore.defaultRepo.initRootObject("Scene");

// 3. Get scene
var scene = NLCore.defaultRepo.qsRootObject;

// 4. Save
NLCore.defaultRepo.saveToFile("scene.QQS.json");

// 5. Load
NLCore.defaultRepo.loadFromFile("scene.QQS.json");

See: Serialization Format Documentation

Factory Functions

Creating Objects

All Core objects should be created using factory functions:

// βœ… Correct: Use factory functions
var node = NLCore.createNode();
var port = NLCore.createPort();
var link = NLCore.createLink();
var scene = NLCore.createScene();

// ❌ Incorrect: Don't create directly
var node = Qt.createQmlObject("Node {}", parent);  // Wrong!

Why Factory Functions?

  1. Repository Assignment: Automatically assigns _qsRepo
  2. Serialization: Ensures objects are registered for serialization
  3. Type Safety: Validates component types
  4. Consistency: Ensures all objects follow same creation pattern

Custom Node Creation

For custom nodes, use QSSerializer.createQSObject:

// Create custom node
var node = QSSerializer.createQSObject(
    "MyCustomNode",           // Component name
    ["NodeLink", "MyApp"],    // Imports
    scene._qsRepo             // Repository
);
node._qsRepo = scene._qsRepo;
node.type = 0;

Core Library Usage

Basic Setup

import QtQuick
import NodeLink
import QtQuickStream

ApplicationWindow {
    Component.onCompleted: {
        // 1. Initialize repository
        NLCore.defaultRepo = NLCore.createDefaultRepo([
            "QtQuickStream",
            "NodeLink"
        ]);

        // 2. Initialize scene
        NLCore.defaultRepo.initRootObject("Scene");
        var scene = NLCore.defaultRepo.qsRootObject;

        // 3. Setup registry
        scene.nodeRegistry = NLNodeRegistry {
            imports: ["NodeLink"]
            // ... register node types ...
        };

        // 4. Create nodes
        var node = NLCore.createNode();
        node.type = 0;
        node.title = "My Node";
        scene.addNode(node);
    }
}

Working with Nodes

// Create node
var node = NLCore.createNode();
node.type = 0;
node.title = "Source";
node.guiConfig.position = Qt.vector2d(100, 100);
node.guiConfig.color = "#4A90E2";

// Add ports
var inputPort = NLCore.createPort();
inputPort.portType = NLSpec.PortType.Input;
inputPort.portSide = NLSpec.PortPositionSide.Left;
inputPort.title = "input";
node.addPort(inputPort);

var outputPort = NLCore.createPort();
outputPort.portType = NLSpec.PortType.Output;
outputPort.portSide = NLSpec.PortPositionSide.Right;
outputPort.title = "output";
node.addPort(outputPort);

// Add to scene
scene.addNode(node);
// Create link between two ports
var link = scene.createLink(
    outputPort._qsUuid,  // From port
    inputPort._qsUuid    // To port
);

// Or manually
var link = NLCore.createLink();
link.inputPort = inputPort;
link.outputPort = outputPort;
link.guiConfig.color = "#FFFFFF";
scene.links[link._qsUuid] = link;
scene.linksChanged();

Selection Management

// Select node
scene.selectionModel.selectNode(node);

// Check selection
if (scene.selectionModel.isSelected(node._qsUuid)) {
    console.log("Node is selected");
}

// Get selected nodes
var selectedNodes = Object.values(scene.selectionModel.selectedModel)
    .filter(obj => obj.objectType === NLSpec.ObjectType.Node);

// Clear selection
scene.selectionModel.clear();

Best Practices

1. Always Use Factory Functions

// βœ… Good
var node = NLCore.createNode();

// ❌ Bad
var node = Qt.createQmlObject("Node {}", parent);

2. Set Repository

// βœ… Good
var node = NLCore.createNode();
node._qsRepo = scene._qsRepo;

// ❌ Bad
var node = NLCore.createNode();
// Missing _qsRepo assignment

3. Use Signals for Notifications

// βœ… Good: Listen to signals
Connections {
    target: scene
    function onNodeAdded(node) {
        console.log("Node added:", node.title);
    }
}

// ❌ Bad: Poll for changes
Timer {
    interval: 100
    onTriggered: {
        // Check if nodes changed
    }
}

4. Validate Operations

// βœ… Good: Check before linking
if (scene.canLinkNodes(portA, portB)) {
    scene.linkNodes(portA, portB);
} else {
    console.warn("Cannot link these ports");
}

// ❌ Bad: Link without validation
scene.linkNodes(portA, portB);  // May fail silently

5. Use Interfaces for Extensibility

// βœ… Good: Use interface
function processNode(node: I_Node) {
    if (node.objectType === NLSpec.ObjectType.Node) {
        // Handle node
    } else if (node.objectType === NLSpec.ObjectType.Container) {
        // Handle container
    }
}

// ❌ Bad: Check concrete type
function processNode(node) {
    if (node instanceof Node) {  // Not extensible
        // ...
    }
}

main.cpp (Main Application Entry Point)

Overview

The main.cpp file serves as the entry point for the Qt application, responsible for setting up the QML runtime environment and loading the main QML file.

Class Description

The main function is the entry point of the application, and it utilizes the following Qt classes:

Properties and Explanations

None.

Signals

None.

Functions

Example Usage in QML

Not applicable, as this file is written in C++.

Extension/Override Points

To extend or customize the application entry point, you can:

Caveats or Assumptions

Code Snippets

The following code snippet demonstrates how to load a QML file using the QQmlApplicationEngine:

QQmlApplicationEngine engine;
const QUrl url(u"qrc:/ColorTools/main.qml"_qs);
engine.load(url);

Note that this code snippet is a subset of the main.cpp file and is provided for illustrative purposes only.

main.qml (Main QML Application Window)

Overview

The main.qml file serves as the entry point for the Qt/QML application, defining the main application window and its layout. It demonstrates a simple color wheel application using the NodeLink MVC architecture.

Architecture

In the context of the NodeLink MVC (Model-View-Controller) architecture, main.qml acts as the View, which is responsible for rendering the UI components. The ColorTool and ColorPickerDialog components interact with each other to provide a color selection interface.

Component Description

The main.qml file defines an ApplicationWindow with a Pane containing a Column layout. This layout includes a ColorTool component, which provides a color selection interface.

Properties and Explanations

The following properties are relevant:

Signals

No custom signals are defined in this file. However, the following signals are used:

Functions

No custom functions are defined in this file.

Example Usage in QML

To use this component, simply create a new QML file (e.g., main.qml) and paste the provided code. You can then run the application using qmlscene or another QML runtime.

Extension/Override Points

To extend or customize this component, consider the following:

Caveats or Assumptions

The following assumptions are made:

The following components are related:

Usage Example

// Create a new QML file (e.g., main.qml) and paste the provided code
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    // ... (paste the code here)
}

NodeExample.qml

Overview

The NodeExample.qml file provides an example implementation of a Node in the NodeLink MVC architecture. This node demonstrates how to create a basic node with multiple ports.

In the NodeLink MVC architecture, NodeExample.qml serves as a View component. It represents a visual node that can be used in a NodeLink diagram. The node is designed to work with the NodeLink Model and Controller components, which manage the node's data and behavior.

Component Description

The NodeExample component is a QML-based node that inherits from the Node type. It has a type property set to 0, which can be used to identify the node type.

Properties

The following properties are exposed by the NodeExample component:

Signals

The NodeExample component does not emit any custom signals. However, it inherits signals from the Node base type, such as onClicked and onDoubleClicked.

Functions

The NodeExample component provides the following functions:

Example Usage in QML

To use the NodeExample component in a QML file, simply import the NodeLink module and create an instance of the node:

import NodeLink

NodeExample {
    x: 100
    y: 100
}

Extension/Override Points

To extend or customize the behavior of the NodeExample component, you can:

Caveats or Assumptions

The NodeExample component assumes that the NodeLink module is properly installed and imported in the QML file. Additionally, the component uses the NLCore object to create ports, which is assumed to be available in the NodeLink module.

The NodeExample component is related to the following components in the NodeLink module:

ImageProcessor.cpp

Overview

The ImageProcessor class provides a set of image processing functions for loading, manipulating, and converting images. It is designed to work seamlessly with the NodeLink MVC architecture, providing a robust and efficient way to handle image-related tasks.

In the NodeLink MVC architecture, the ImageProcessor class acts as a service provider, offering a range of image processing functions that can be used by the application's controllers and models. By separating image processing logic into a dedicated class, the architecture remains modular and maintainable.

Class Description

The ImageProcessor class is a QObject-based class that provides the following key features:

Properties

The ImageProcessor class does not have any properties.

Signals

The ImageProcessor class does not emit any signals.

Functions

Image Loading

Image Filters

Image Conversion

Example Usage in QML

import QtQuick 2.12
import QtQuick.Window 2.12

Window {
    visible: true
    width: 640
    height: 480

    ImageProcessor {
        id: imageProcessor
    }

    Image {
        id: image
        source: ""
        anchors.centerIn: parent
    }

    Component.onCompleted: {
        var imageData = imageProcessor.loadImage("path/to/image.jpg")
        if (imageProcessor.isValidImage(imageData)) {
            var blurredImage = imageProcessor.applyBlur(imageData, 5.0)
            var dataUrl = imageProcessor.saveToDataUrl(blurredImage)
            image.source = dataUrl
        }
    }
}

Extension/Override Points

To extend or override the functionality of the ImageProcessor class, you can:

Caveats or Assumptions

ImageProcessor.h

Overview

The ImageProcessorCPP class provides a set of image loading and processing functions, designed to be used within the NodeLink MVC architecture. It allows for the manipulation of images, including loading, applying effects, and saving.

In the NodeLink MVC architecture, ImageProcessorCPP serves as a Model component, providing a set of functions for image processing. It can be used by Views (QML components) to load and manipulate images.

Class Description

The ImageProcessorCPP class is a singleton QObject that provides a set of image processing functions. It is designed to be used in QML, with a set of Q_INVOKABLE functions that can be called from QML.

Properties

None.

Signals

None.

Functions

Constructors

Image Processing Functions

Private Helper Functions

Example Usage in QML

import QtQuick 2.0
import NodeLink 1.0

Item {
    ImageProcessorCPP {
        id: imageProcessor
    }

    Component.onCompleted: {
        var imageData = imageProcessor.loadImage("path/to/image.jpg")
        var blurredImage = imageProcessor.applyBlur(imageData, 10)
        var brightenedImage = imageProcessor.applyBrightness(blurredImage, 1.5)
        var dataUrl = imageProcessor.saveToDataUrl(brightenedImage)
        console.log(dataUrl)
    }
}

Extension/Override Points

To extend or override the functionality of ImageProcessorCPP, you can:

Caveats or Assumptions

QSCoreCpp.h

Overview

The QSCoreCpp class is the core component of any QtQuickStream application, providing access to the default repository (defaultRepo) and a collection of QtQuickStream repositories (qsRepos). It serves as a central hub for managing repositories and facilitating communication between them.

In the NodeLink MVC architecture, QSCoreCpp acts as the Model component, responsible for managing the data and business logic of the application. It interacts with the Controller and View components to provide a seamless user experience.

Component Description

QSCoreCpp is a C++ class that inherits from QObject, allowing it to be easily integrated with QML. It provides a set of properties, signals, and functions that enable repository management and communication.

Properties

The following properties are available:

Signals

The following signals are emitted:

Functions

The following functions are available:

Example Usage in QML

import QtQuick 2.15
import QtQuickStream 1.0

ApplicationWindow {
    id: window
    visible: true

    // Create a QSCoreCpp instance
    QSCoreCpp {
        id: core
    }

    // Access the default repository
    Component.onCompleted: {
        console.log("Default Repository:", core.defaultRepo)
    }

    // Create a new repository
    Button {
        text: "Create Repository"
        onClicked: core.sigCreateRepo("myRepo")
    }
}

Extension/Override Points

To extend or override the behavior of QSCoreCpp, you can:

Caveats or Assumptions

QSObjectCpp.h

Overview

The QSObjectCpp class provides a base class for objects in the NodeLink MVC architecture, offering UUID and JSON serialization functionality. It serves as a foundation for creating serializable objects that can be easily stored, retrieved, and manipulated within the application.

In the NodeLink MVC architecture, QSObjectCpp acts as a model component, providing a standardized way to represent and manage data. It integrates with the QSRepositoryCpp class, which handles data storage and retrieval.

Component Description

QSObjectCpp is a QML-enabled QObject that provides the following key features:

Properties

The following properties are exposed by QSObjectCpp:

Signals

The following signals are emitted by QSObjectCpp:

Functions

The following functions are provided by QSObjectCpp:

Example Usage in QML

import QtQuick 2.15
import QtQuick.Window 2.15
import com.example.qsobjectcpp 1.0

Window {
    visible: true
    width: 640
    height: 480

    QSObjectCpp {
        id: obj
        qsType: "exampleType"
        qsIsAvailable: true
        _qsUuid: "123e4567-e89b-12d3-a456-426614174000"
    }

    Component.onCompleted: {
        console.log(obj.qsType) // Output: exampleType
        console.log(obj.qsIsAvailable) // Output: true
        console.log(obj._qsUuid) // Output: 123e4567-e89b-12d3-a456-426614174000
    }
}

Extension/Override Points

To extend or override the behavior of QSObjectCpp, you can:

Caveats or Assumptions

Future Development

By following these guidelines and using QSObjectCpp as a foundation, you can create robust and serializable objects within the NodeLink MVC architecture.

QSRepositoryCpp.h

Overview

The QSRepositoryCpp class is a container that stores and manages QSObjectCpp instances. It provides functionality for serialization to and from disk and enables other mechanisms such as RPCs.

In the NodeLink MVC architecture, QSRepositoryCpp serves as a model component. It manages a collection of QSObjectCpp instances and provides data binding and notification mechanisms.

Component Description

QSRepositoryCpp is a QML-enabled C++ class that inherits from QSObjectCpp. It provides properties, signals, and slots for managing QSObjectCpp instances.

Properties

The following properties are available:

Signals

The following signals are emitted:

Functions

The following functions are available:

Example Usage in QML

import QtQuick 2.15
import NodeLink 1.0

QSRepositoryCpp {
    id: repository
    name: "My Repository"

    // Create and add an object to the repository
    QSObjectCpp {
        id: myObject
        uuid: "my-object-uuid"
    }

    Component.onCompleted: {
        repository.registerObject(myObject)
    }
}

Extension/Override Points

The following points can be extended or overridden:

Caveats or Assumptions

The following caveats or assumptions should be noted:

The following components are related to QSRepositoryCpp:

QSCore.qml

Overview

The QSCore.qml file provides the core functionality for the QtQuickStream framework. It acts as the central hub for managing repositories and provides essential functions for creating and handling repository objects.

In the NodeLink MVC architecture, QSCore.qml serves as the Model component. It encapsulates the data and business logic related to repository management, providing a bridge between the C++ backend and the QML frontend.

Component Description

The QSCore.qml component is an instance of QSCoreCpp, a C++-based QML component that provides the underlying functionality for repository management. This QML component extends the C++ backend with QML-specific functionality and signal handling.

Properties

The following properties are exposed by the QSCore.qml component:

Signals

The QSCore.qml component handles the following signal:

Functions

The QSCore.qml component provides the following functions:

createDefaultRepo(imports: object)

Creates a new default repository with the specified imports.

createRepo(repoId: string, isRemote: bool)

Creates a new repository with the specified ID and remote status.

Example Usage in QML

import QtQuick
import QtQuickStream

Item {
    Component.onCompleted: {
        // Create a new default repository with some imports
        var imports = { "someImport": "someValue" };
        var newRepo = core.createDefaultRepo(imports);

        // Create a new repository with a specific ID and remote status
        var repoId = "myRepoId";
        var isRemote = true;
        var customRepo = core.createRepo(repoId, isRemote);
    }
}

Extension/Override Points

To extend or override the functionality of the QSCore.qml component, you can:

Caveats or Assumptions

The following components are related to QSCore.qml:

QSFileIO.qml

Overview

The QSFileIO QML component is a singleton wrapper around the C++-implemented FileIO class. It provides a convenient interface for performing file input/output operations within the QtQuickStream framework.

In the NodeLink MVC architecture, QSFileIO serves as a model component, responsible for encapsulating data access and manipulation logic. It interacts with the C++-implemented FileIO class to provide file I/O capabilities.

Component Description

The QSFileIO component is a singleton, meaning only one instance of it exists throughout the application. It acts as a proxy to the underlying FileIO C++ class, exposing its functionality through QML.

Properties

The following properties are available:

Signals

The following signals are emitted:

Functions

The following functions are available:

Example Usage in QML

import QtQuick 2.0
import QtQuickStream 1.0

Item {
    Component.onCompleted: {
        // Use QSFileIO to read/write files
        QSFileIO.writeFile("example.txt", "Hello, World!")
        var content = QSFileIO.readFile("example.txt")
        console.log(content) // Output: "Hello, World!"
    }
}

Extension/Override Points

To extend or override the behavior of QSFileIO, you can:

Caveats or Assumptions

Note: As QSFileIO is a singleton wrapper, it does not have a detailed property, signal and function list. For a detailed list, please refer to the FileIO C++ class documentation.

QSObject.qml

Overview

The QSObject.qml provides additional JavaScript/QML helper functionality for managing objects in the NodeLink MVC architecture. It extends the QSObjectCpp class with utility functions for adding, removing, and updating elements in containers, as well as setting properties.

Architecture

In the NodeLink MVC architecture, QSObject.qml serves as a utility component that facilitates interactions between the Model, View, and Controller. It provides a set of functions that can be used to manage objects and their relationships, making it easier to implement complex logic in QML.

Component Description

The QSObject.qml component is an extension of QSObjectCpp and provides the following features:

Properties

The following properties are available:

Signals

The following signals are emitted:

Functions

The following functions are available:

addElement

function addElement(container, element: QSObject, containerChangedSignal, emit = true)

Adds an element to a container and emits the specified signal.

setElements

function setElements(container, elements, containerChangedSignal)

Adds or removes elements from a container to match the specified elements and emits the specified signal.

removeElement

function removeElement(container, element: QSObject, containerChangedSignal, emit = true)

Removes an element from a container and emits the specified signal.

setProperties

function setProperties(propertiesMap: object, qsRepo = _qsRepo)

Copies all properties from the specified map to the object.

Example Usage in QML

import QtQuick

Item {
    id: root

    // Create a QSObject instance
    QSObject.qml {
        id: qsObject
    }

    // Define a container and elements
    property var container: ({})
    property var elements: [
        QSObject.qml { id: element1 },
        QSObject.qml { id: element2 }
    ]

    // Add elements to container
    Component.onCompleted: {
        qsObject.addElement(container, element1, onContainerChanged)
        qsObject.addElement(container, element2, onContainerChanged)
    }

    // Handle container changes
    function onContainerChanged() {
        console.log("Container changed")
    }
}

Extension/Override Points

To extend or override the behavior of QSObject.qml, you can:

Caveats or Assumptions

By using QSObject.qml, you can simplify the management of objects and their relationships in your QML applications, making it easier to implement complex logic and interactions.

QSRepository.qml

Overview

The QSRepository component serves as a central container for storing and managing QSObject instances. It provides functionality for serialization and deserialization to and from disk, enabling features like data persistence and restoration.

Architecture

Within the NodeLink MVC architecture, QSRepository acts as a model component. It interacts with the controller by providing data access and manipulation capabilities, while also notifying the view of changes through signals.

Component Description

QSRepository is a QML component that wraps a C++ implementation (QSRepositoryCpp). It manages a collection of QSObject instances and provides methods for initializing, serializing, and deserializing the repository.

Properties

Signals

No signals are explicitly declared in this component. However, it reacts to changes in qsRootObject and name through on-changed handlers.

Functions

Initialization

Serialization and Deserialization

File Operations

Example Usage in QML

import QtQuick 2.15

Item {
    QSRepository {
        id: repository
        _localImports: ["MyCustomModule"]
        imports: ["QtQuickStream", "MyOtherModule"]

        Component.onCompleted: {
            repository.initRootObject("MyRootObjectType.qml")
            // Use the repository
            repository.saveToFile("myrepo.json")
        }
    }
}

Extension/Override Points

Caveats or Assumptions

QSSerializer.qml

Overview

The QSSerializer is a singleton QML object responsible for transforming objects from their in-memory form to a format that can be saved or loaded to/from disk. It plays a crucial role in the NodeLink MVC architecture by providing a way to serialize and deserialize QtQuickStream objects.

In the NodeLink MVC architecture, the QSSerializer acts as a utility component that helps with data persistence and retrieval. It works closely with the QSRepository component to resolve object references and handle serialization/deserialization tasks.

Component Description

The QSSerializer component provides a set of functions for serializing and deserializing QtQuickStream objects. It supports two serialization types: STORAGE and NETWORK, which determine how objects are represented during serialization.

Properties

Signals

None

Functions

createQSObject

fromQSUrlProps

fromQSUrlProp

getQSProps

getQSProp

Helper Functions

Example Usage in QML

import QtQuick 2.15
import QtQuickStream 1.0

Item {
    id: root

    // Create a serializer instance
    QtObject {
        id: serializer
        // Use QSSerializer as a singleton
        function createQSObject(qsType, imports, parent) {
            return QSSerializer.createQSObject(qsType, imports, parent);
        }
    }

    // Serialize an object
    function serializeObject(obj) {
        var props = QSSerializer.getQSProps(obj, QSSerializer.SerialType.STORAGE);
        console.log(props);
    }

    // Deserialize an object
    function deserializeObject(props, repo) {
        var obj = QSSerializer.fromQSUrlProps({}, props, repo);
        console.log(obj);
    }
}

Extension/Override Points

Caveats or Assumptions

QSObjectCpp.cpp

Overview

The QSObjectCpp class is a base class for objects in the QtQuickStream (QS) framework. It provides a common interface for QS objects, including properties, signals, and functions for managing object metadata and repository registration.

High-Level Purpose

The QSObjectCpp class serves as a foundation for creating QS objects that can be used in QML applications. It provides a standardized way of managing object metadata, such as UUIDs, types, and interfaces, and facilitates registration with a repository.

In the NodeLink MVC architecture, QSObjectCpp objects play a crucial role as model objects. They represent the data and behavior of a node in the application, and provide a interface for interacting with the node.

Component or Class Description

The QSObjectCpp class is a C++ class that inherits from QObject. It provides a set of properties, signals, and functions for managing object metadata and repository registration.

Properties

The following properties are available:

Signals

The following signals are emitted:

Functions

The following functions are available:

Example Usage in QML

import QtQuick 2.0
import QtQuickStream 1.0

QSObjectCpp {
    id: myObject
    objectName: "MyObject"

    // Accessing properties
    onIsAvailableChanged: console.log("Is available:", isAvailable)
    onRepoChanged: console.log("Repo changed:", repo)
    onUuidChanged: console.log("UUID changed:", uuidStr)

    // Calling functions
    function getInterfacePropNames() {
        return myObject.getInterfacePropNames()
    }
}

Extension/Override Points

To extend or override the behavior of QSObjectCpp, you can:

Caveats or Assumptions

QSRepositoryCpp.cpp

Overview

The QSRepositoryCpp class is a C++ implementation of a repository in the NodeLink MVC architecture. It manages a collection of QSObjectCpp instances, providing functionality for adding, removing, and updating objects.

Purpose

The primary purpose of QSRepositoryCpp is to serve as a centralized storage and management system for QSObjectCpp instances. It provides a way to register, unregister, and forward objects, as well as propagate availability changes to its contained objects.

In the NodeLink MVC architecture, QSRepositoryCpp acts as a model component, responsible for managing a collection of data objects (QSObjectCpp instances). It interacts with other components, such as views and controllers, to provide data access and manipulation functionality.

Class Description

QSRepositoryCpp is a subclass of QSObjectCpp and provides the following key features:

Properties

The following properties are available:

Signals

The following signals are emitted:

Functions

The following functions are available:

Example Usage in QML

import QtQuick 2.15

Item {
    id: root

    // Create a QSRepositoryCpp instance
    property QSRepositoryCpp repository: QSRepositoryCpp {
        id: repository
    }

    // Register an object with the repository
    Component.onCompleted: {
        var obj = QSObjectCpp {
            id: obj
        }
        repository.registerObject(obj)
    }
}

Extension/Override Points

To extend or override the behavior of QSRepositoryCpp, you can:

Caveats or Assumptions

Container.qml

Overview

The Container.qml file defines a QML component that represents a container for grouping items in the NodeLink architecture. This component is a crucial part of the NodeLink Model-View-Controller (MVC) architecture, serving as a model that can hold multiple nodes and containers.

In the NodeLink MVC architecture, the Container component acts as a model. It manages a collection of nodes and containers, providing methods for adding and removing them. The view components can then bind to the properties of this model to display the contents of the container.

Component Description

The Container component is an instance of I_Node, which is a base interface for all nodes in the NodeLink architecture. It has several properties and methods that enable it to manage its contents and notify about changes.

Properties

Signals

The Container component emits the following signals:

Functions

Example Usage in QML

import NodeLink

Container {
    id: myContainer
    title: "My Container"

    // Adding nodes
    Component.onCompleted: {
        var node1 = Node {}
        myContainer.addNode(node1)

        var node2 = Node {}
        myContainer.addNode(node2)
    }
}

Extension/Override Points

Caveats or Assumptions

ContainerGuiConfig.qml

Overview

The ContainerGuiConfig QML component provides a configuration object for container GUI elements in the NodeLink framework. It encapsulates properties that define the visual appearance and behavior of a container in the NodeLink scene.

In the NodeLink Model-View-Controller (MVC) architecture, ContainerGuiConfig serves as a configuration object that can be used by the View components to display container GUI elements. It does not directly interact with the Model or Controller but provides essential data for rendering and user interaction.

Component Description

ContainerGuiConfig is a QSObject that aggregates properties related to the GUI configuration of a container. It includes properties for zoom factor, size, color, position, lock status, and title text height.

Properties

The following properties are available:

Signals

None.

Functions

None.

Example Usage in QML

import QtQuick
import NodeLink

// Create a ContainerGuiConfig object
ContainerGuiConfig {
    id: containerConfig
    zoomFactor: 1.5
    width: 300
    height: 200
    color: "blue"
    position: Qt.vector2d(100, 100)
}

// Use the config object in a NodeLink component
NodeLinkComponent {
    containerGuiConfig: containerConfig
}

Extension/Override Points

To extend or customize the behavior of ContainerGuiConfig, you can:

Caveats or Assumptions

ImagesModel.qml

Overview

The ImagesModel component is a QML object that manages a collection of images for a node in the NodeLink MVC architecture. It provides properties and functions to add and delete images, as well as notify listeners of changes to the image collection.

In the NodeLink MVC architecture, ImagesModel serves as a model component that stores and manages the images associated with a node. It interacts with the controller to provide data and notify listeners of changes.

Component Description

ImagesModel is a QSObject that provides the following features:

Properties

Property Name Type Description
imagesSources var A list of image sources (e.g., base64 encoded strings)
coverImageIndex int The index of the cover image (-1 means no cover image)

Signals

Signal Name Description
imagesSourcesChanged() Emitted when the imagesSources list changes

Functions

Function Name Description
addImage(base64int) Adds an image to the imagesSources list
deleteImage(base64int) Deletes an image from the imagesSources list

Example Usage in QML

import NodeLink 1.0

Node {
    id: node

    // Create an ImagesModel instance
    property ImagesModel imagesModel: ImagesModel {
        id: imagesModel
    }

    // Add an image
    Component.onCompleted: imagesModel.addImage("base64 encoded string")

    // Delete an image
    Button {
        text: "Delete Image"
        onClicked: imagesModel.deleteImage("base64 encoded string")
    }
}

Extension/Override Points

To extend or override the behavior of ImagesModel, you can:

Caveats or Assumptions

I_Node.qml

Overview

The I_Node QML component serves as the base interface for all objects within the NodeLink scene. It provides a foundation for node-like objects, defining essential properties, signals, and functions that can be extended or overridden by derived classes.

In the NodeLink Model-View-Controller (MVC) architecture, I_Node acts as a crucial part of the Model component. It represents the data and behavior of nodes within the scene, providing a blueprint for concrete node implementations. The I_Node component interacts with the NodeLink module, facilitating the management of node data and behavior.

Component Description

The I_Node component is a QSObject that encapsulates the basic characteristics of a node. It includes properties for the node's type and associated data, as well as signals and functions for managing node cloning.

Properties

Signals

Functions

Example Usage in QML

import NodeLink

// Create a concrete node component that extends I_Node
Item {
    id: myNode
    objectType: NLSpec.ObjectType.CustomNode
    nodeData: MyNodeData { /* initialize node data */ }

    // Handle cloning
    onCloneFrom: function(baseNode) {
        // Extend the base cloning behavior
        mySpecificProperty = baseNode.mySpecificProperty;
    }
}

Extension/Override Points

Caveats or Assumptions

I_NodeData.qml

Overview

The I_NodeData QML interface serves as the base class for defining node data in the NodeLink architecture. It provides a fundamental structure for node data, allowing for flexibility and extensibility.

In the NodeLink Model-View-Controller (MVC) architecture, I_NodeData plays a crucial role in the Model component. It acts as a data container and interface for node-specific data, which is then utilized by the Controller to manage node behavior and the View to display node information.

Component Description

The I_NodeData interface is a QSObject that defines a single property for storing node data. This property can hold any type of data, including arrays, maps of objects, or any other QML-supported types.

Properties

Signals

None.

Functions

None.

Example Usage in QML

import QtQuick
import NodeLink

// Create an instance of I_NodeData
I_NodeData {
    id: nodeData
    data: { "name": "Example Node", "type": "info" }
}

// Accessing the data property
console.log(nodeData.data.name)  // Outputs: Example Node

Extension/Override Points

To create a custom node data class, you can create a QML object that inherits from I_NodeData and adds additional properties or functions as needed. For example:

// CustomNodeData.qml
import QtQuick
import NodeLink

I_NodeData {
    property string nodeName
    property string nodeType

    // Additional functions or properties can be added here
}

Caveats or Assumptions

By using the I_NodeData interface, developers can ensure consistency and flexibility in node data management within the NodeLink architecture.

I_Scene.qml

Overview

The I_Scene QML component represents a scene in the NodeLink architecture, responsible for managing nodes, links, and containers. It provides a comprehensive set of properties, signals, and functions to interact with the scene.

In the NodeLink MVC (Model-View-Controller) architecture, I_Scene serves as the Model component. It manages the data and business logic of the scene, including nodes, links, and containers. The View components, such as NodeOverview and LinkView, interact with the I_Scene model to display and update the scene.

Component Description

The I_Scene component is a QSObject that provides a centralized management system for nodes, links, and containers. It maintains a map of nodes, links, and containers, and provides signals and functions to add, remove, and manipulate these objects.

Properties

Signals

Functions

Example Usage in QML

import QtQuick 2.15
import NodeLink 1.0

Item {
    I_Scene {
        id: scene
        title: "My Scene"
        nodeRegistry: NLNodeRegistry {}
        selectionModel: SelectionModel {}
    }

    // Add a node to the scene
    Component.onCompleted: {
        var node = QSSerializer.createQSObject("Node", ["NodeLink"], scene.sceneActiveRepo);
        scene.addNode(node, true);
    }
}

Extension/Override Points

Caveats or Assumptions

Link.qml

Overview

The Link component represents a connection between two nodes in the NodeLink architecture. It maintains references to the input and output ports, as well as control points for line rendering. This allows for interactive connection selection and manipulation.

In the NodeLink Model-View-Controller (MVC) architecture, the Link component serves as a model that represents a connection between two nodes. It interacts with the Port components, which are part of the node architecture, to establish and manage connections.

Component Description

The Link component inherits from I_Node and has the following key features:

Properties

The Link component has the following properties:

Signals

The Link component does not emit any custom signals. However, it inherits signals from its base class I_Node.

Functions

The Link component has the following functions:

Example Usage in QML

import NodeLink

Link {
    id: myLink
    inputPort: Port { }
    outputPort: Port { }
    controlPoints: [ { x: 10, y: 20 }, { x: 30, y: 40 } ]
    direction: NLSpec.LinkDirection.Unidirectional
    guiConfig: LinkGUIConfig { }
}

Extension/Override Points

To extend or customize the behavior of the Link component, you can:

Caveats or Assumptions

LinkGUIConfig.qml

Overview

The LinkGUIConfig component is a QML object that stores UI-related properties for a link in the NodeLink MVC architecture. It provides a centralized way to manage link-specific settings, such as description, color, style, and type.

Architecture Integration

In the NodeLink MVC architecture, LinkGUIConfig serves as a configuration object for link-related UI components. It is typically used in conjunction with the Link model and LinkView components to provide a cohesive user interface for link editing and visualization.

Component Description

The LinkGUIConfig component is a QSObject that exposes several properties to control the appearance and behavior of a link in the UI.

Properties

Signals

None

Functions

None

Example Usage in QML

import QtQuick

Item {
    LinkGUIConfig {
        id: linkConfig
        description: "Example Link"
        color: "blue"
        style: NLSpec.LinkStyle.Dashed
        type: NLSpec.LinkType.Straight
    }

    // Use linkConfig properties in your UI components
    Text {
        text: linkConfig.description
    }
}

Extension/Override Points

To extend or customize the behavior of LinkGUIConfig, you can:

Caveats or Assumptions

NLCore.qml

Overview

The NLCore QML component is a singleton that serves as the core of the NodeLink framework. It is responsible for creating the default repository scene and handling top-level functionalities such as de/serialization and network connectivity.

In the NodeLink Model-View-Controller (MVC) architecture, NLCore acts as a controller, providing an interface to create and manage scenes, nodes, ports, and links. It interacts with the model (repository) to create and retrieve data.

Component Description

The NLCore component is a subclass of QSCore and provides the following key features:

Properties

The following properties are exposed by the NLCore component:

Signals

None

Functions

The following functions are provided by the NLCore component:

Example Usage in QML

import NodeLink 1.0

Item {
    id: root

    // Access the NLCore singleton
    property NLCore core: NLCore {}

    // Create a new scene
    Component.onCompleted: {
        var scene = core.createScene()
        console.log("Scene created:", scene)
    }
}

Extension/Override Points

To extend or override the behavior of the NLCore component, you can:

Caveats or Assumptions

The following components are related to NLCore:

NLNodeRegistry.qml

Overview

The NLNodeRegistry component is a crucial part of the NodeLink MVC architecture, responsible for registering and managing node types, their corresponding views, and other relevant metadata. It acts as a central registry for all node-related information, enabling the creation and customization of nodes within the application.

Architecture Integration

In the NodeLink MVC architecture, NLNodeRegistry plays a key role in the Model layer, providing essential data and configuration for node creation and rendering. It interacts closely with the NLNode and NLLink components, as well as the NodeLink model, to ensure a cohesive and extensible node-based system.

Component Description

The NLNodeRegistry component is a QSObject that encapsulates the following key features:

Properties

The following properties are exposed by the NLNodeRegistry component:

Signals

None.

Functions

None.

Example Usage in QML

import NodeLink

NLNodeRegistry {
    id: nodeRegistry

    // Register node types
    nodeTypes: {
        1: "MyNodeType1",
        2: "MyNodeType2"
    }

    // Configure node metadata
    nodeNames: {
        1: "Node 1",
        2: "Node 2"
    }

    nodeIcons: {
        1: "qrc:/icons/node1.png",
        2: "qrc:/icons/node2.png"
    }

    nodeColors: {
        1: "#FF0000",
        2: "#00FF00"
    }

    // Set default node type
    defaultNode: 1

    // Configure view URLs
    nodeView: "qrc:/views/MyNodeView.qml"
    linkView: "qrc:/views/MyLinkView.qml"
    containerView: "qrc:/views/MyContainerView.qml"
}

Extension/Override Points

To extend or customize the behavior of NLNodeRegistry, you can:

Caveats or Assumptions

NLSpec.qml

Overview

NLSpec.qml is a singleton QML file that provides a centralized specification for various enumerations and properties used throughout the NodeLink (NL) framework. It serves as a reference point for developers to ensure consistency across the application.

In the NodeLink MVC architecture, NLSpec.qml acts as a supporting module that provides essential definitions for the Model, View, and Controller components. It does not directly correspond to a specific MVC component but rather supplements the architecture by offering a unified set of specifications.

Component Description

NLSpec.qml defines a QtObject with several enumerations and properties that describe various aspects of the NodeLink framework, including:

Properties

The following properties are exposed by NLSpec.qml:

Enumerations

The following enumerations are defined in NLSpec.qml:

ObjectType

SelectionSpecificToolType

PortPositionSide

PortType

LinkType

LinkDirection

LinkStyle

NodeType

Example Usage in QML

import NLSpec 1.0

QtObject {
    property int linkType: NLSpec.LinkType.Bezier
    property int nodeType: NLSpec.NodeType.CustomNode

    Component.onCompleted: {
        console.log("Link type:", linkType)
        console.log("Node type:", nodeType)
    }
}

Extension/Override Points

Developers can extend or override the enumerations and properties provided by NLSpec.qml by creating custom QML modules or C++ classes that inherit from the existing types.

Caveats or Assumptions

NLUtils.qml

Overview

The NLUtils component is a utility module within the NodeLink framework, providing a set of helper functions and properties to facilitate development and integration with the NodeLink MVC architecture.

Architecture Integration

In the NodeLink MVC architecture, NLUtils serves as a supporting module that can be used across various components, including models, views, and controllers. It does not directly correspond to a specific MVC layer but rather acts as a utility belt, offering functionalities that can be leveraged to simplify development and improve code readability.

Component Description

NLUtils is essentially a QML wrapper around the NLUtilsCPP C++ class, which is implemented in C++. This component exposes a set of static utility functions and properties that can be easily accessed and used within QML files.

Properties

Property Name Type Description
None - NLUtils does not expose any properties. It is used solely for its functions.

Signals

Signal Name Parameters Description
None - NLUtils does not emit any signals.

Functions

The following functions are available in NLUtils. Note that since NLUtils is a wrapper around NLUtilsCPP, the actual implementation details are in C++, but they are exposed in a QML-friendly manner.

Function Name Parameters Return Type Description
None - - Currently, no functions are directly exposed through NLUtils.qml. The actual utility functions are part of the NLUtilsCPP class and would be documented separately.

Example Usage in QML

import QtQuick 2.0
import NodeLink 1.0

Item {
    id: root

    // Example usage, assuming a utility function 'exampleFunction' exists in NLUtilsCPP
    // and is properly exposed.
    Component.onCompleted: {
        // NLUtils.exampleFunction(); // Uncomment if exampleFunction is available
    }
}

Extension/Override Points

To extend or override the behavior of NLUtils, you would typically work with the NLUtilsCPP class, as it contains the core implementation. This might involve:

  1. C++ Implementation: Modifying or extending the C++ class NLUtilsCPP to add new functionalities or modify existing ones.
  2. QML Wrappers: If additional QML-specific functionality is needed, creating a new QML component that wraps NLUtils and adds the required features.

Caveats or Assumptions

Node.qml

Overview

The Node QML component is a model that manages node properties, serving as a fundamental building block in the NodeLink MVC architecture. It encapsulates the data and behavior of a node, including its GUI configuration, title, type, children, parents, and ports.

In the NodeLink MVC architecture, the Node component acts as the Model, which holds the data and business logic of a node. It interacts with the NodeGuiConfig component to manage the node's GUI properties and with the Port component to manage the node's ports.

Component Description

The Node component is an implementation of the I_Node interface and provides the following key features:

Properties

The Node component has the following properties:

Signals

The Node component emits the following signals:

Functions

The Node component provides the following functions:

Example Usage in QML

import NodeLink

Node {
    id: myNode
    title: "My Node"
    type: 1

    // Add a new port to the node
    function addMyPort() {
        var myPort = Port {}
        myNode.addPort(myPort)
    }

    // Delete a port from the node
    function deleteMyPort(portId) {
        var myPort = myNode.findPort(portId)
        if (myPort) {
            myNode.deletePort(myPort)
        }
    }
}

Extension/Override Points

To extend or override the behavior of the Node component, you can:

Caveats or Assumptions

By using the Node QML component, you can create and manage nodes in your NodeLink-based application, leveraging its built-in features and signals to simplify your development process.

NodeData.qml

Overview

The NodeData.qml file provides a basic implementation of the I_NodeData interface, which is a crucial part of the NodeLink Model-View-Controller (MVC) architecture. This component serves as a foundation for representing data associated with nodes in a graph or network.

In the NodeLink MVC architecture, NodeData.qml acts as the Model component. It encapsulates the data and business logic related to nodes, providing a standardized interface for accessing and manipulating node data. This allows for a clear separation of concerns between the data representation, visualization, and control logic.

Component Description

The NodeData.qml component implements the I_NodeData interface, which defines the contract for node data objects. This interface ensures that any node data object provides a consistent set of properties and methods, facilitating interoperability and reuse across the NodeLink framework.

Properties

The following properties are inherited from the I_NodeData interface:

Signals

No custom signals are defined in this component. However, it may emit signals as specified in the I_NodeData interface.

Functions

No custom functions are defined in this component. However, it must implement the functions specified in the I_NodeData interface.

Example Usage in QML

import QtQuick
import NodeLink

Node {
    id: exampleNode
    data: NodeData {
        // Access and manipulate node data properties here
    }
}

Extension/Override Points

To extend or customize the behavior of NodeData.qml, you can:

Caveats or Assumptions

NodeGuiConfig.qml

Overview

The NodeGuiConfig component is a QML object that stores the UI properties of a node in the NodeLink architecture. It provides a centralized location for managing node-specific properties, such as position, size, color, and opacity.

In the NodeLink Model-View-Controller (MVC) architecture, NodeGuiConfig serves as a View Configuration object. It provides the necessary properties for rendering a node in the graphical user interface (GUI). The NodeLink Controller updates this object's properties, which in turn are used by the NodeLink View to render the node.

Component Description

The NodeGuiConfig component is a QSObject that stores the UI properties of a node. It is designed to be used as a configuration object for nodes in the NodeLink GUI.

Properties

The following properties are available:

Signals

None.

Functions

None.

Example Usage in QML

import QtQuick
import NodeLink

Node {
    id: myNode
    config: NodeGuiConfig {
        description: "My Node"
        logoUrl: "qrc:/images/node_logo.png"
        position: Qt.vector2d(100, 100)
        width: 200
        height: 100
        color: "blue"
    }
}

Extension/Override Points

To extend or override the behavior of NodeGuiConfig, you can:

Caveats or Assumptions

Port.qml

Overview

The Port QML component represents a port in the NodeLink architecture, managing its properties and behavior. It is a crucial part of the NodeLink Model-View-Controller (MVC) architecture, serving as a model that interacts with the view and controller to facilitate the creation and manipulation of nodes and their ports.

In the NodeLink MVC architecture, the Port component acts as the model. It encapsulates the data and properties of a port, such as its position, type, and enablement. The view (e.g., a QML visual component) binds to the properties of the Port model to display the port's information. The controller interacts with the Port model to update its properties and respond to user interactions.

Component Description

The Port component is a QSObject that manages port properties. It is designed to be used within a node to represent input or output ports.

Properties

The following properties are exposed by the Port component:

Signals

The Port component does not emit any signals.

Functions

The Port component does not expose any functions.

Example Usage in QML

import NodeLink

Node {
    id: myNode

    Port {
        node: myNode
        title: "Input Port"
        portType: NLSpec.PortType.Input
        portSide: NLSpec.PortPositionSide.Left
    }

    Port {
        node: myNode
        title: "Output Port"
        portType: NLSpec.PortType.Output
        portSide: NLSpec.PortPositionSide.Right
    }
}

Extension/Override Points

To extend or customize the behavior of the Port component, you can:

Caveats or Assumptions

Scene.qml

Overview

The Scene component is a crucial part of the NodeLink MVC architecture, responsible for managing nodes and links between them. It provides a comprehensive set of properties, functions, and signals to facilitate the creation, manipulation, and interaction of nodes and links within a scene.

Architecture Integration

In the NodeLink MVC architecture, the Scene component serves as the central hub for node and link management. It interacts closely with other components, such as nodes, links, and the undo core, to provide a seamless and intuitive user experience.

Component Description

The Scene component is an instance of I_Scene and provides the following key features:

Properties

The Scene component has the following properties:

Functions

The Scene component provides the following functions:

Signals

The Scene component does not emit any signals.

Example Usage in QML

import QtQuick
import NodeLink

Scene {
    id: myScene

    // Create a custom node
    function createCustomNode() {
        var nodeType = 1; // Node type
        var xPos = 100; // X position
        var yPos = 100; // Y position
        return createCustomizeNode(nodeType, xPos, yPos);
    }

    // Link two nodes
    function linkTwoNodes() {
        var portA = "portA"; // Port A UUID
        var portB = "portB"; // Port B UUID
        linkNodes(portA, portB);
    }
}

Extension/Override Points

The Scene component provides several extension and override points:

Caveats and Assumptions

The Scene component assumes that nodes and links are properly registered and managed within the NodeLink framework. It also assumes that the undo core is properly configured and integrated.

The Scene component interacts closely with the following components:

SceneGuiConfig.qml

Overview

The SceneGuiConfig QML component is designed to store and manage the visual properties of a scene within the NodeLink MVC architecture. It provides a centralized location for scene-specific GUI configuration, facilitating the synchronization of visual settings across different parts of the application.

In the NodeLink MVC (Model-View-Controller) architecture, SceneGuiConfig serves as a supporting model component. It does not directly control the scene's logic but provides essential visual configuration properties that can be observed and bound to by views and controllers. This separation allows for a flexible and decoupled design, enabling easier maintenance and extension of the application's GUI.

Component Description

SceneGuiConfig is a QSObject that encapsulates several key visual properties of a scene. These include zoom factors, content dimensions, and positions within a flickable area, as well as scene view dimensions. The component automatically unregisters itself from the _qsRepo upon destruction, ensuring proper cleanup.

Properties

The following properties are exposed by SceneGuiConfig:

Signals

This component does not emit any signals.

Functions

There are no functions provided by SceneGuiConfig.

Example Usage in QML

import QtQuick
import NodeLink

// Assuming access to a SceneGuiConfig instance named 'sceneGuiConfig'
SceneGuiConfig {
    id: sceneGuiConfig
    // Example of binding a property
    contentWidth: 800
    contentHeight: 600
    // ...
}

// Usage in another component
Flickable {
    id: flickable
    width: sceneGuiConfig.contentWidth
    height: sceneGuiConfig.contentHeight
    contentX: sceneGuiConfig.contentX
    contentY: sceneGuiConfig.contentY
    // ...
}

Extension/Override Points

To extend or customize the behavior of SceneGuiConfig, you can:

Caveats or Assumptions

SelectionModel.qml

Overview

The SelectionModel.qml component is a crucial part of the NodeLink MVC architecture, responsible for managing the selection of nodes, links, and containers in a scene. It keeps track of the currently selected items and provides methods for selecting, deselecting, and checking the selection status of objects.

Component Description

The SelectionModel.qml component is a QtObject that stores the selection state of nodes, links, and containers in a scene. It provides a centralized way to manage the selection of objects, ensuring that the selection state is consistent across the application.

Properties

Signals

Functions

Example Usage in QML

import QtQuick 2.0
import NodeLink 1.0

Item {
    SelectionModel {
        id: selectionModel
    }

    Node {
        id: node1
        _qsUuid: "node1_uuid"
    }

    Node {
        id: node2
        _qsUuid: "node2_uuid"
    }

    // Select node1
    selectionModel.selectNode(node1)

    // Check if node1 is selected
    console.log(selectionModel.isSelected("node1_uuid")) // Output: true

    // Clear selection
    selectionModel.clear()

    // Select all nodes
    selectionModel.selectAll([node1, node2], [], [])
}

Extension/Override Points

The SelectionModel.qml component can be extended or overridden by subclassing it and adding custom functionality. For example, you can add additional selection logic or integrate it with other components.

Caveats or Assumptions

By using the SelectionModel.qml component, you can easily manage the selection of objects in your NodeLink-based application, ensuring a consistent and efficient selection mechanism.

SelectionSpecificTool.qml

Overview

The SelectionSpecificTool component is a QtObject that represents a selection tool button model. It provides a way to create a customizable button that can be used in a menu or toolbar to perform a specific action on a selected node.

In the NodeLink MVC architecture, SelectionSpecificTool fits into the View category. It is a UI component that displays a button and emits a signal when clicked. The button's visibility and behavior can be controlled through its properties.

Component Description

The SelectionSpecificTool component is a simple QtObject that has a few properties and a signal. It does not have any visual representation on its own and is intended to be used as a model for a button in a menu or toolbar.

Properties

Signals

Functions

None.

Example Usage in QML

import QtQuick
import NodeLink

// Create a SelectionSpecificTool instance
SelectionSpecificTool {
    id: myTool
    name: "My Tool"
    icon: "\uF0A8" // Font Awesome 6 icon
    enable: true
    type: NLSpec.SelectionSpecificToolType.Any

    // Connect to the clicked signal
    onClicked: (node) => {
        console.log("My tool clicked on node:", node)
        // Perform action on node
    }
}

Extension/Override Points

The SelectionSpecificTool component can be extended or overridden by creating a custom QML component that inherits from it. This allows developers to add custom properties, signals, or functions to the component.

Caveats or Assumptions

CommandStack.qml

Overview

The CommandStack component is a QtObject that manages a stack of commands for undo and redo functionality. It is designed to work within the NodeLink MVC architecture.

In the NodeLink MVC architecture, the CommandStack component serves as a central hub for managing commands that modify the model. It provides a way to record, undo, and redo changes made to the model.

Component Description

The CommandStack component is a QtObject that provides the following features:

Properties

The CommandStack component has the following properties:

Signals

The CommandStack component emits the following signals:

Functions

The CommandStack component provides the following functions:

Example Usage in QML

import QtQuick
import NodeLink

Item {
    CommandStack {
        id: commandStack
    }

    // Create a command
    var myCommand = {
        undo: function() {
            console.log("Undoing command")
        },
        redo: function() {
            console.log("Redoing command")
        }
    }

    // Push the command onto the stack
    commandStack.push(myCommand)

    // Undo the command
    commandStack.undo()

    // Redo the command
    commandStack.redo()
}

Extension/Override Points

The CommandStack component can be extended or overridden by creating a custom command stack component that inherits from CommandStack. This allows developers to add custom functionality or modify the existing behavior of the command stack.

Caveats or Assumptions

The CommandStack component assumes that commands have undo and redo functions that can be called to perform the corresponding actions. It also assumes that commands are objects with undo and redo properties.

The CommandStack component is related to the following components:

Command Object Structure

A command object should have the following structure:

var command = {
    undo: function() {
        // Undo the command
    },
    redo: function() {
        // Redo the command
    }
}

This structure allows the command stack to call the undo and redo functions on the command object to perform the corresponding actions.

HashCompareString.qml

Overview

The HashCompareString QML component serves as a bridge between QML and the HashCompareStringCPP C++ class, facilitating the comparison of strings using hash functions. This singleton component is designed to work within the NodeLink Model-View-Controller (MVC) architecture.

Architecture Integration

In the NodeLink MVC architecture, HashCompareString acts as a model component that provides a QML interface to the HashCompareStringCPP C++ class. This allows QML views to interact with the string comparison functionality without directly accessing C++ code.

Component Description

The HashCompareString component is a singleton, meaning only one instance of it can exist throughout the application. It wraps the functionality of HashCompareStringCPP, making it accessible from QML.

Properties

Property Name Type Description
None - This component does not expose any properties.

Signals

Signal Name Parameters Description
None - This component does not emit any signals.

Functions

Function Name Parameters Return Type Description
None - - This component does not expose any functions.

Example Usage in QML

import QtQuick
import NodeLink

Item {
    // Using the singleton instance
    HashCompareString {
        id: hashCompareString
    }

    // Example usage (assuming HashCompareStringCPP has a function compareStrings(string, string))
    // Note: Actual usage depends on the implementation of HashCompareStringCPP
    Component.onCompleted: {
        console.log("Comparing strings:", hashCompareString.compareStrings("Hello", "World"))
    }
}

Extension/Override Points

To extend or customize the behavior of HashCompareString, you can:

  1. Modify the C++ Backend: Since HashCompareString is a wrapper around HashCompareStringCPP, modifications and extensions should be made at the C++ level. This involves altering the HashCompareStringCPP class to add new functionality or override existing behavior.

  2. Create a Subclass: While not directly applicable due to the singleton nature and the fact that it's a QML wrapper, you can create similar components that wrap different C++ classes, providing a similar interface but with different implementations.

Caveats or Assumptions

This documentation provides a comprehensive overview of the HashCompareString.qml component, its role within the NodeLink architecture, and how to interact with it from QML.

UndoContainerGuiObserver.qml

Overview

The UndoContainerGuiObserver is a QML component that observes changes to the properties of a ContainerGuiConfig object and pushes undo commands onto a CommandStack when changes occur. This component is part of the NodeLink MVC architecture and plays a crucial role in enabling undo/redo functionality for GUI configurations.

In the NodeLink MVC architecture, the UndoContainerGuiObserver acts as a controller component that listens to changes in the model (ContainerGuiConfig) and updates the command stack accordingly. This allows users to undo and redo changes made to the GUI configuration.

Component Description

The UndoContainerGuiObserver is an Item component that observes changes to the following properties of the ContainerGuiConfig object:

When a change is detected, it pushes an undo command onto the CommandStack.

Properties

Signals

None.

Functions

Example Usage in QML

import NodeLink 1.0

Item {
    id: root

    ContainerGuiConfig {
        id: guiConfig
        position: Qt.vector2d(10, 20)
        width: 100
        height: 50
        color: "red"
    }

    CommandStack {
        id: undoStack
    }

    UndoContainerGuiObserver {
        guiConfig: guiConfig
        undoStack: undoStack
    }
}

Extension/Override Points

To extend or override the behavior of the UndoContainerGuiObserver, you can:

Caveats or Assumptions

UndoContainerObserver.qml

Overview

The UndoContainerObserver is a QML component that updates the UndoStack when properties of a Container change. It plays a crucial role in the NodeLink MVC architecture by providing a way to track and revert changes made to container properties.

In the NodeLink MVC architecture, the UndoContainerObserver acts as an observer of the Container model. When a property of the Container changes, the observer creates a PropertyCommand and pushes it onto the UndoStack. This allows the application to maintain a history of changes and provide undo/redo functionality.

Component Description

The UndoContainerObserver is an Item component that observes a Container and updates the UndoStack accordingly. It has two main properties: container and undoStack, which are used to track the container being observed and the undo stack being updated.

Properties

Signals

None.

Functions

Example Usage in QML

import NodeLink 1.0

Item {
    UndoContainerObserver {
        id: observer
        container: myContainer
        undoStack: myUndoStack
    }

    Container {
        id: myContainer
        title: "My Container"
    }

    CommandStack {
        id: myUndoStack
    }
}

Extension/Override Points

To extend or override the behavior of the UndoContainerObserver, you can:

Caveats or Assumptions

UndoCore.qml

Overview

The UndoCore.qml file provides the core functionality for managing undo and redo operations within the NodeLink MVC architecture. It acts as a central component that coordinates the interaction between the scene, undo/redo stacks, and observers.

Architecture Integration

In the NodeLink MVC architecture, UndoCore plays a crucial role in the Model-View-Controller (MVC) pattern by providing a mechanism for tracking and reverting changes made to the scene. It integrates with the I_Scene interface, which represents the scene being managed, and utilizes command-based undo/redo stacks (CommandStack) to store and execute commands.

Component Description

The UndoCore component is a QtObject that serves as a container for managing undo/redo functionality. It has the following key responsibilities:

Properties

The UndoCore component has the following properties:

Signals

The UndoCore component does not emit any signals directly. However, the undoSceneObserver property may emit signals related to scene changes.

Functions

The UndoCore component does not provide any functions beyond those inherited from QtObject.

Example Usage in QML

To use the UndoCore component in a QML file, you can create an instance and bind its scene property to your scene instance:

import QtQuick
import NodeLink

// Assuming you have a scene instance
I_Scene {
    id: myScene
    // Other properties and children
}

// Create an UndoCore instance
UndoCore {
    id: undoCore
    scene: myScene
}

Extension/Override Points

To extend or customize the behavior of UndoCore, you can:

Caveats or Assumptions

The following components are related to UndoCore:

UndoLinkGuiObserver.qml

Overview

The UndoLinkGuiObserver is a QML component that observes changes to the properties of a LinkGUIConfig object and pushes undo commands onto a CommandStack when changes occur. This allows for easy implementation of undo/redo functionality in a NodeLink-based application.

Architecture

The UndoLinkGuiObserver fits into the NodeLink MVC architecture as a supporting component that works closely with the LinkGUIConfig model and the CommandStack controller. It observes changes to the LinkGUIConfig properties and pushes undo commands onto the CommandStack, which can then be used to undo and redo changes.

Component Description

The UndoLinkGuiObserver is an Item component that has the following properties:

The component uses a cache to store the previous values of the LinkGUIConfig properties, allowing it to compute the old and new values of each property when a change occurs.

Properties

Signals

None.

Functions

Example Usage in QML

import NodeLink 1.0

Item {
    LinkGUIConfig {
        id: guiConfig
    }

    CommandStack {
        id: undoStack
    }

    UndoLinkGuiObserver {
        guiConfig: guiConfig
        undoStack: undoStack
    }
}

Extension/Override Points

The UndoLinkGuiObserver can be extended or overridden by creating a custom component that inherits from UndoLinkGuiObserver and overrides one or more of its functions.

Caveats or Assumptions

UndoLinkObserver.qml

Overview

The UndoLinkObserver is a QML component that updates the UndoStack when properties of a Link change. It plays a crucial role in the NodeLink MVC architecture by providing undo and redo functionality for link property changes.

In the NodeLink MVC architecture, the UndoLinkObserver acts as an observer of the Link model. When a Link property changes, the observer notifies the UndoStack, which stores the changes as commands. These commands can then be used to undo or redo the changes.

Component Description

The UndoLinkObserver is an Item component that:

Properties

Signals

None

Functions

Example Usage in QML

import NodeLink

// Create a Link object
Link {
    id: myLink
    title: "My Link"
    type: "input"
}

// Create an UndoStack
CommandStack {
    id: myUndoStack
}

// Create an UndoLinkObserver
UndoLinkObserver {
    link: myLink
    undoStack: myUndoStack
}

Extension/Override Points

To extend or override the behavior of the UndoLinkObserver, you can:

Caveats or Assumptions

UndoNodeGuiObserver.qml

Overview

The UndoNodeGuiObserver is a QML component that observes changes to a NodeGuiConfig object's properties and pushes undo commands onto a CommandStack when changes occur. This component is part of the NodeLink MVC architecture and plays a crucial role in enabling undo/redo functionality for NodeGuiConfig properties.

In the NodeLink MVC architecture, the UndoNodeGuiObserver acts as a controller component that listens to changes in the NodeGuiConfig model and updates the CommandStack accordingly. This allows users to undo and redo changes made to NodeGuiConfig properties.

Component Description

The UndoNodeGuiObserver is an Item component that observes changes to a NodeGuiConfig object's properties and pushes undo commands onto a CommandStack. It uses a cache to store the previous values of the properties and compares them with the new values to determine if an undo command should be pushed.

Properties

Signals

None.

Functions

Example Usage in QML

import NodeLink 1.0

Item {
    NodeGuiConfig {
        id: nodeConfig
    }

    CommandStack {
        id: commandStack
    }

    UndoNodeGuiObserver {
        guiConfig: nodeConfig
        undoStack: commandStack
    }
}

Extension/Override Points

To extend or override the behavior of the UndoNodeGuiObserver, you can:

Caveats or Assumptions

UndoNodeObserver.qml

Overview

The UndoNodeObserver is a QML component that plays a crucial role in the NodeLink MVC architecture by updating the UndoStack when properties of a Node change. This allows for efficient undo and redo functionality within the application.

In the NodeLink MVC (Model-View-Controller) architecture, UndoNodeObserver acts as a bridge between the Model (Node) and the Controller (CommandStack or UndoStack). It observes changes to a Node's properties and pushes corresponding commands onto the UndoStack, enabling the tracking of changes for undo and redo operations.

Component Description

The UndoNodeObserver component is designed to be used in conjunction with a Node and an UndoStack. It listens for changes to the Node's properties (currently title and type) and creates commands to push onto the UndoStack to record these changes.

Properties

Signals

The UndoNodeObserver component does not emit any signals directly. However, it reacts to signals emitted by the Node object it observes.

Functions

Example Usage in QML

import NodeLink 1.0

Item {
    Node {
        id: myNode
        title: "My Node"
        type: "example"
    }

    UndoStack {
        id: myUndoStack
    }

    UndoNodeObserver {
        node: myNode
        undoStack: myUndoStack
    }
}

Extension/Override Points

To extend or customize the behavior of UndoNodeObserver, you can:

Caveats or Assumptions

UndoSceneObserver.qml

Overview

The UndoSceneObserver is a QML component that plays a crucial role in integrating the scene model with the command stack, enabling undo functionality within the NodeLink MVC architecture. It acts as an observer, monitoring changes to the scene model and updating the command stack accordingly.

In the NodeLink MVC (Model-View-Controller) architecture, UndoSceneObserver serves as a bridge between the Model (scene) and the Controller (command stack). It ensures that any changes to the scene model are properly recorded and can be undone or redone through the command stack.

Component Description

The UndoSceneObserver component is an Item that requires two properties:

Properties

Property Type Description Required
scene I_Scene The scene model being observed. Yes
undoStack CommandStack The command stack for undo/redo functionality. Yes

Signals

None.

Functions

None.

Child Components

The UndoSceneObserver component contains three Repeater components, each responsible for observing a different aspect of the scene model:

Example Usage in QML

import NodeLink

// Assuming you have a scene model and a command stack
Item {
    property I_Scene myScene
    property CommandStack myUndoStack

    UndoSceneObserver {
        scene: myScene
        undoStack: myUndoStack
    }
}

Extension/Override Points

To extend or customize the behavior of UndoSceneObserver, you can:

Caveats or Assumptions

UndoStack.qml

Overview

The UndoStack component is responsible for managing undo and redo operations in the NodeLink MVC architecture. It maintains two stacks, one for undo and one for redo, and provides functions for updating, undoing, and redoing actions.

Architecture

The UndoStack component fits into the NodeLink MVC architecture as a supporting component that works closely with the I_Scene interface. It uses the sceneActiveRepo property to interact with the scene's repository.

Component Description

The UndoStack component is a QtObject that provides the following properties and functions:

Properties

Signals

Functions

Example Usage in QML

import QtQuick
import NodeLink

Item {
    UndoStack {
        id: undoStack
        scene: myScene
    }

    // Perform an undo operation
    function undo() {
        undoStack.undo();
    }

    // Perform a redo operation
    function redo() {
        undoStack.redo();
    }
}

Extension/Override Points

The UndoStack component provides several points for extension or override:

Caveats or Assumptions

AddContainerCommand.qml

Overview

The AddContainerCommand is a QtObject that represents a command to add a container to a scene in the NodeLink MVC architecture. It provides a way to encapsulate the addition of a container as a command that can be executed, undone, and redone.

In the NodeLink MVC architecture, AddContainerCommand serves as a command object that interacts with the I_Scene interface and a Container object. It is typically used in conjunction with a CommandHistory to manage the execution, undoing, and redoing of commands.

Component Description

The AddContainerCommand QtObject has the following properties and functions:

Properties

Signals

None

Functions

Example Usage in QML

import QtQuick
import NodeLink

// Create a scene that implements I_Scene
I_Scene {
    id: myScene
}

// Create a container
Container {
    id: myContainer
}

// Create an AddContainerCommand
AddContainerCommand {
    id: addCommand
    scene: myScene
    container: myContainer
}

// Execute the command
addCommand.redo()

// Undo the command
addCommand.undo()

Extension/Override Points

To extend or override the behavior of AddContainerCommand, you can:

Caveats or Assumptions

AddNodeCommand.qml

Overview

The AddNodeCommand QML component is a concrete implementation of the Command pattern, specifically designed to add a node to a scene within the NodeLink MVC architecture. This component is essential for managing changes in the scene graph, allowing for easy undo and redo functionality.

In the NodeLink MVC (Model-View-Controller) architecture, AddNodeCommand serves as a Controller component. It encapsulates the logic required to add a node to the scene model and provides a way to reverse this action (undo). This fits into the broader architecture by enabling the management of scene state changes in a structured and reversible manner.

Component Description

The AddNodeCommand component is a QtObject that holds references to a scene and a node. It provides redo and undo functions to add and remove the specified node from the scene, respectively.

Properties

Signals

This component does not emit any signals.

Functions

Example Usage in QML

import QtQuick
import NodeLink

// Assuming you have a scene and a node defined
Item {
    id: root
    property var myScene: // Initialize your scene here
    property var myNode: // Initialize your node here

    Component.onCompleted: {
        var addCommand = Qt.createQmlObject('import NodeLink; AddNodeCommand { scene: root.myScene; node: root.myNode }', root, "AddNodeCommand");
        addCommand.redo(); // Adds the node to the scene
        addCommand.undo(); // Removes the node from the scene
    }
}

Extension/Override Points

To extend or customize the behavior of AddNodeCommand, you could:

Caveats or Assumptions

CreateLinkCommand.qml

Overview

The CreateLinkCommand is a QtObject that represents a command to create a link between two nodes in the NodeLink MVC architecture. It provides a way to encapsulate the creation and deletion of links, making it easier to manage the history of changes in a scene.

In the NodeLink MVC architecture, the CreateLinkCommand serves as a command object that interacts with the I_Scene interface. It is responsible for creating and deleting links between nodes, and it notifies the scene about these changes.

Component Description

The CreateLinkCommand is a QtObject that has the following properties:

Properties

Property Type Description
scene var The scene in which the link will be created.
inputPortUuid string The UUID of the input port of the link.
outputPortUuid string The UUID of the output port of the link.
createdLink var The link that was created, set after the redo function is called.

Signals

None.

Functions

redo()

The redo function creates a link between the input and output ports in the scene. It checks if the scene, input port UUID, and output port UUID are valid before creating the link.

undo()

The undo function deletes the link between the input and output ports in the scene. It checks if the scene, input port UUID, and output port UUID are valid before deleting the link.

Example Usage in QML

import QtQuick
import NodeLink

Item {
    id: root

    // Create a scene
    Scene {
        id: scene
    }

    // Create a CreateLinkCommand
    CreateLinkCommand {
        id: createLinkCommand
        scene: scene
        inputPortUuid: "inputPortUuid"
        outputPortUuid: "outputPortUuid"
    }

    // Create a link
    Button {
        text: "Create Link"
        onClicked: createLinkCommand.redo()
    }

    // Undo the link creation
    Button {
        text: "Undo"
        onClicked: createLinkCommand.undo()
    }
}

Extension/Override Points

To extend or override the behavior of the CreateLinkCommand, you can:

Caveats or Assumptions

PropertyCommand.qml

Overview

The PropertyCommand QML component is a fundamental building block in the NodeLink MVC architecture, designed to manage property changes on target objects. It encapsulates the logic for setting, undoing, and redoing property modifications, making it an essential part of the application's command history and undo/redo functionality.

Architecture Integration

In the NodeLink MVC architecture, PropertyCommand serves as a command object that encapsulates a specific property change on a model object. It acts as a bridge between the Model and Controller components, allowing for easy tracking and reversal of changes made to the model's properties.

Component Description

PropertyCommand is a QtObject that represents a single command to change a property on a target object. It maintains references to the target object, the property key, and the old and new values of the property. Optionally, a custom applier function can be provided to handle complex property setting logic.

Properties

Signals

None.

Functions

Example Usage in QML

import QtQuick 2.0

Item {
    id: root
    property int myProperty: 0

    PropertyCommand {
        id: command
        target: root
        key: "myProperty"
        oldValue: 0
        newValue: 10
    }

    Component.onCompleted: {
        console.log(myProperty) // prints 0
        command.setProp(5)
        console.log(myProperty) // prints 5
        command.undo()
        console.log(myProperty) // prints 0
        command.redo()
        console.log(myProperty) // prints 10
    }
}

Extension/Override Points

To extend or customize the behavior of PropertyCommand, you can:

Caveats or Assumptions

RemoveContainerCommand.qml

Overview

The RemoveContainerCommand is a QtObject designed to manage the removal and restoration of a container within a scene in the context of the NodeLink MVC architecture. It encapsulates the logic for deleting a container from a scene and provides methods to redo and undo this action.

In the NodeLink MVC (Model-View-Controller) architecture, RemoveContainerCommand serves as a command object that can be used to manage changes to the model (in this case, the scene and its containers). It acts as a bridge between the controller (which initiates actions) and the model (which manages the data and state), allowing for easy implementation of undo/redo functionality.

Component Description

The RemoveContainerCommand QtObject has the following key characteristics:

Properties

Property Type Description
scene var A reference to the scene (I_Scene) where the container will be removed.
container var The container (Container) to be removed from the scene.

Signals

None.

Functions

redo()

undo()

Example Usage in QML

import QtQuick
import NodeLink

// Assuming you have a scene and a container
Item {
    NodeLinkScene {
        id: myScene
    }

    // Create a RemoveContainerCommand
    RemoveContainerCommand {
        id: removeCommand
        scene: myScene
        container: myScene.getContainer("containerId")
    }

    // To remove the container
    Button {
        text: "Remove Container"
        onClicked: removeCommand.redo()
    }

    // To undo the removal
    Button {
        text: "Undo Removal"
        onClicked: removeCommand.undo()
    }
}

Extension/Override Points

To extend or customize the behavior of RemoveContainerCommand, you could:

Caveats or Assumptions

RemoveNodeCommand.qml

Overview

The RemoveNodeCommand is a QtObject designed to manage the removal and restoration of nodes within a scene in the NodeLink MVC architecture. It encapsulates the logic for deleting a node and its associated links, as well as undoing these actions to restore the node and its connections.

In the NodeLink MVC (Model-View-Controller) architecture, RemoveNodeCommand serves as a command object that can be used to execute or undo changes to the model (in this case, the scene and its nodes). It interacts with the I_Scene interface to perform node deletion and addition, and it maintains a reference to the affected Node and its associated links.

Component Description

The RemoveNodeCommand QtObject has the following key features:

Properties

Property Type Description
scene var Reference to the scene (I_Scene) where the node exists.
node var Reference to the node (Node) to be removed.
links var A list of objects containing inputPortUuid and outputPortUuid to restore connections.

Signals

None.

Functions

redo()

Deletes the specified node from the scene.

undo()

Adds the removed node back to the scene and restores its connections.

Example Usage in QML

import QtQuick
import NodeLink

// Assuming you have a scene and a node
NodeLink.I_Scene {
    id: myScene
}

NodeLink.Node {
    id: myNode
    scene: myScene
}

// Create RemoveNodeCommand instance
RemoveNodeCommand {
    id: removeCommand
    scene: myScene
    node: myNode
    links: [{inputPortUuid: myNode.inputPorts[0].uuid, outputPortUuid: myNode.outputPorts[0].uuid}]
}

// To remove the node
removeCommand.redo()

// To undo and add the node back
removeCommand.undo()

Extension/Override Points

To extend or customize the behavior of RemoveNodeCommand, you could:

Caveats or Assumptions

UnlinkCommand.qml

Overview

The UnlinkCommand QML component is a part of the NodeLink framework, designed to encapsulate the logic for unlinking two nodes within a scene. It adheres to the Command pattern, allowing for the action of unlinking nodes to be executed and then undone.

In the NodeLink Model-View-Controller (MVC) architecture, UnlinkCommand serves as a Controller component. It acts as an intermediary between the Model (I_Scene) and the View, encapsulating the business logic required to unlink nodes. This component is pivotal in managing the state changes within the scene, specifically when nodes are to be disconnected.

Component Description

The UnlinkCommand component is a QtObject that encapsulates the functionality to unlink two nodes, identified by their input and output port UUIDs, within a given scene. It maintains references to the scene and the UUIDs of the input and output ports involved in the unlinking process.

Properties

Signals

This component does not emit any signals.

Functions

Example Usage in QML

import QtQuick
import NodeLink

Item {
    id: root

    // Assuming scene is an I_Scene instance
    property var scene: // initialize your scene here

    UnlinkCommand {
        id: unlinkCmd
        scene: root.scene
        inputPortUuid: "input-port-uuid"
        outputPortUuid: "output-port-uuid"
    }

    // To unlink nodes
    function unlinkNodes() {
        unlinkCmd.redo()
    }

    // To undo unlinking
    function undoUnlink() {
        unlinkCmd.undo()
    }
}

Extension/Override Points

To extend or customize the behavior of UnlinkCommand, you can:

Caveats or Assumptions

ContainerOverview.qml

Overview

The ContainerOverview.qml file provides a QML component for displaying a container's overview in a NodeLink scene. This component is part of the NodeLink MVC architecture and plays a crucial role in visualizing container nodes in an overview.

In the NodeLink MVC (Model-View-Controller) architecture, ContainerOverview.qml serves as a view component. It is responsible for rendering the visual representation of a container node in the overview. The model provides the data (e.g., node properties), and this component uses that data to display the container's overview. The controller would handle interactions and updates to the model.

Component Description

The ContainerOverview component is a specialized ContainerView that displays a container node in an overview. It takes into account the node's properties, such as position, size, and selection state, to render the overview accurately.

Properties

Object Properties

The component's visual properties are dynamically calculated based on the node's GUI configuration and the overview scale factor:

Signals

This component does not emit any custom signals.

Functions

This component does not have any custom functions.

Example Usage in QML

import NodeLink

// Assuming you have a NodeLink scene set up
NodeLinkScene {
    id: scene

    // Create a container node
    ContainerNode {
        id: containerNode
        guiConfig: NodeGuiConfig {
            position: Qt.point(100, 100)
            width: 200
            height: 100
            color: "blue"
        }
    }

    // Use the ContainerOverview component
    ContainerOverview {
        node: containerNode
        scene: scene
        viewProperties: ViewProperties {
            nodeRectTopLeft: Qt.point(0, 0)
            overviewScaleFactor: 0.5
        }
    }
}

Extension/Override Points

To extend or customize the behavior of this component, you can:

Caveats or Assumptions

ContainerView.qml

Overview

The ContainerView component is a visual representation of a container in the NodeLink MVC architecture. It provides an interactive node view that can be resized, dragged, and dropped. The component is responsible for managing the container's position, size, and child nodes.

In the NodeLink MVC architecture, the ContainerView component corresponds to the View layer. It receives data from the Model layer (represented by the Container object) and updates the UI accordingly. The component also sends user interactions (e.g., drag and drop) to the Controller layer for processing.

Component Description

The ContainerView component is an instance of InteractiveNodeView and provides the following features:

Properties

The following properties are exposed by the ContainerView component:

Signals

The ContainerView component does not emit any custom signals. However, it inherits signals from its parent InteractiveNodeView component.

Functions

The following functions are provided by the ContainerView component:

Example Usage in QML

import NodeLink

ContainerView {
    id: myContainerView
    container: myContainer
}

Extension/Override Points

To extend or override the behavior of the ContainerView component, you can:

Caveats or Assumptions

The following caveats or assumptions apply to the ContainerView component:

The following components are related to the ContainerView component:

ImagesFlickable.qml

Overview

The ImagesFlickable component is a QML component designed to display a horizontal list of images associated with a node in the NodeLink MVC architecture. It appears on top of each node that has images.

Architecture

In the NodeLink MVC architecture, ImagesFlickable is a view component that complements the I_Node model. It is used to visualize and interact with the images of a node.

Component Description

The ImagesFlickable component is a Rectangle that contains a ListView with a horizontal orientation. The list displays images from the imagesModel of the associated node. Each image is represented by a Rectangle delegate that contains an Image and several interactive elements.

Properties

Signals

None.

Functions

None.

Example Usage in QML

import NodeLink

// Assuming 'node' is an I_Node object
ImagesFlickable {
    node: node
    scene: myScene
    sceneSession: mySceneSession
}

Extension/Override Points

To extend or customize the behavior of ImagesFlickable, you can:

Caveats or Assumptions

Properties and Explanations

The following properties are used in the component:

The ListView has the following properties:

Each image delegate has the following properties:

The Image component has the following properties:

The interactive elements (e.g., MouseArea, NLIconButtonRound) have their own properties and signals, which are not listed here for brevity.

ImageViewer.qml

Overview

The ImageViewer.qml component is a popup dialog that displays an image with navigation capabilities to view multiple images. It is designed to be used within the NodeLink MVC architecture.

In the NodeLink MVC architecture, the ImageViewer.qml component serves as a view that can be triggered by a controller to display an image or a series of images. The images are passed to the component as a list of URLs or image sources.

Component Description

The ImageViewer.qml component is a subclass of NLPopUp, which provides a basic popup dialog functionality. It contains the following key elements:

Properties

The ImageViewer.qml component has the following properties:

Signals

The ImageViewer.qml component does not emit any signals.

Functions

The ImageViewer.qml component does not have any explicit functions. However, it uses the following functions:

Example Usage in QML

import QtQuick
import NodeLink

ApplicationWindow {
    id: window
    visible: true

    // Create a list of image sources
    property var imageSources: [
        "image1.jpg",
        "image2.png",
        "image3.bmp"
    ]

    // Create an instance of the ImageViewer component
    ImageViewer {
        id: imageViewer
        images: window.imageSources
        shownImage: imageSources[0]
    }

    // Button to trigger the image viewer
    Button {
        text: "Show Image Viewer"
        onClicked: {
            imageViewer.open()
        }
    }
}

Extension/Override Points

To extend or override the behavior of the ImageViewer.qml component, you can:

Caveats or Assumptions

The ImageViewer.qml component assumes that the image sources provided in the images list are valid and can be displayed. It also assumes that the NLPopUp and NLIconButtonRound components are available in the NodeLink library.

The ImageViewer.qml component is related to the following components:

InteractiveNodeView.qml

Overview

The InteractiveNodeView component is a QML view that represents a node in a NodeLink scene. It provides interactive functionality for node resizing, selection, and deletion.

In the NodeLink MVC architecture, InteractiveNodeView serves as the View component. It is responsible for rendering the node's visual representation and handling user interactions. The Model component is represented by the node property, which provides access to the node's data and configuration. The Controller component is not explicitly defined in this QML file, but it is assumed to be handled by the surrounding NodeLink framework.

Component Description

The InteractiveNodeView component is a QML component that extends the I_NodeView interface. It provides a visual representation of a node in a NodeLink scene, complete with interactive resizing, selection, and deletion functionality.

Properties

The following properties are exposed by the InteractiveNodeView component:

Signals

The InteractiveNodeView component does not emit any signals.

Functions

The InteractiveNodeView component provides the following functions:

Example Usage in QML

import NodeLink

NodeLinkScene {
    id: scene

    // ...

    InteractiveNodeView {
        node: myNode
        scene: scene
        sceneSession: scene.session
    }
}

Extension/Override Points

To extend or override the behavior of the InteractiveNodeView component, you can:

Caveats or Assumptions

The InteractiveNodeView component assumes that it is used within a NodeLink scene, and that the node property is set to a valid node object. It also assumes that the scene and sceneSession properties are set to valid objects.

The following components are related to InteractiveNodeView:

I_LinkView.qml

Overview

The I_LinkView component is a QML interface class that displays links as Bezier curves. It is a crucial part of the NodeLink MVC architecture, responsible for rendering the visual representation of links between nodes.

In the NodeLink MVC architecture, I_LinkView serves as the View component for links. It receives data from the Model (Link) and updates its visual representation accordingly. The component interacts with the Controller through the scene, sceneSession, and viewProperties properties.

Component Description

I_LinkView is a Canvas component that displays a link as a Bezier curve. It provides a customizable visual representation of a link, including its color, style, and type.

Properties

Signals

None.

Functions

Example Usage in QML

import QtQuick 2.15
import NodeLink 1.0

Item {
    I_LinkView {
        id: linkView
        scene: myScene
        sceneSession: mySceneSession
        link: myLink
    }

    // ...
}

Extension/Override Points

To extend or override the behavior of I_LinkView, you can:

Caveats or Assumptions

Usage Guidelines

I_NodesRect.qml

Overview

The I_NodesRect QML component is an interface class responsible for displaying nodes within a scene. It plays a crucial role in the NodeLink MVC (Model-View-Controller) architecture by acting as the view for nodes, links, and containers.

In the NodeLink MVC architecture, I_NodesRect serves as the view component. It receives updates from the model (scene and scene session) and displays the nodes, links, and containers accordingly. The controller (not shown in this component) would typically interact with the model and update the view through signals and properties.

Component Description

The I_NodesRect component is an Item that fills its parent area. It has several properties that define its behavior and child components.

Properties

Signals

This component does not emit any signals. Instead, it listens to signals from the scene object to update its child components.

Functions

There are no explicit functions defined in this component. However, it uses several functions from the ObjectCreator class to create node, link, and container views.

Example Usage in QML

import NodeLink

I_NodesRect {
    id: nodesRect
    scene: myScene
    sceneSession: mySceneSession
    viewProperties: myViewProperties
}

Extension/Override Points

To extend or customize the behavior of I_NodesRect, you can:

Caveats or Assumptions

I_NodesScene.qml

Overview

The I_NodesScene QML component represents the abstract base for a nodes scene in the NodeLink architecture. It provides a Flickable area for displaying nodes and links, along with properties and functionality for managing the scene's layout and user interactions.

In the NodeLink Model-View-Controller (MVC) architecture, I_NodesScene serves as the View component. It is responsible for rendering the scene's contents, handling user input, and updating the scene's layout in response to user interactions.

Component Description

The I_NodesScene component is a Flickable area that displays the scene's contents, including nodes and links. It provides properties for customizing the scene's background, foreground, and contents.

Properties

Signals

Functions

Example Usage in QML

import NodeLink

I_NodesScene {
    id: nodesScene
    scene: myScene // assuming myScene is an I_Scene object
    sceneSession: mySceneSession // assuming mySceneSession is a SceneSession object
    background: Rectangle { color: "lightgray" }
    sceneContent: MySceneContentComponent {} // assuming MySceneContentComponent is a QML component
    foreground: MyForegroundComponent {} // assuming MyForegroundComponent is a QML component
}

Extension/Override Points

Caveats or Assumptions

I_NodeView.qml

Overview

The I_NodeView QML component is an abstract representation of a node view in the NodeLink MVC architecture. It provides a basic structure for displaying node properties and serves as a base for more specific node view implementations.

In the NodeLink MVC architecture, I_NodeView is a view component that corresponds to a node in the model. It receives updates from the model through the node and scene properties and displays the node's properties accordingly. The I_NodeView component is part of the view layer, while the I_Scene and SceneSession components are part of the model layer.

Component Description

The I_NodeView component is a Rectangle that displays the node's properties, such as position, size, color, and border style. It also provides a content area for displaying custom node content.

Properties

The following properties are exposed by the I_NodeView component:

Signals

The I_NodeView component does not emit any signals.

Functions

The I_NodeView component does not provide any functions.

Example Usage in QML

import NodeLink

I_NodeView {
    node: myNode
    scene: myScene
    sceneSession: mySceneSession
    contentItem: MyNodeContent {}
}

Extension/Override Points

To create a custom node view, you can override the I_NodeView component and provide a custom implementation for the contentItem property. You can also bind to the node and scene properties to access additional node and scene data.

Caveats or Assumptions

The I_NodeView component assumes that the node and scene properties are valid and non-null. It also assumes that the contentItem property is a valid QML component.

The following components are related to I_NodeView:

Usage Guidelines

When using the I_NodeView component, ensure that you provide valid node and scene properties. You can customize the appearance of the node view by binding to the node and scene properties and providing a custom contentItem component.

Best Practices

LinkView.qml

Overview

The LinkView component is a QML view that manages link children using the I_LinkView interface, specifically implementing a Bezier curve. It provides a visual representation of a link in the NodeLink MVC architecture.

In the NodeLink MVC architecture, LinkView is a view component that corresponds to a link model. It receives updates from the model and displays the link's properties, such as its description and color.

Component Description

The LinkView component is an instance of I_LinkView and provides the following features:

Properties

The following properties are exposed by the LinkView component:

Signals

The LinkView component emits the following signals:

Functions

The LinkView component provides the following functions:

Example Usage in QML

To use the LinkView component in QML, you can simply import the NodeLink module and create an instance of LinkView:

import NodeLink

LinkView {
    id: linkView
    link: someLinkModel
    sceneSession: someSceneSession
}

Extension/Override Points

To extend or override the behavior of the LinkView component, you can:

Caveats or Assumptions

The following caveats or assumptions apply to the LinkView component:

The following components are related to LinkView:

Properties Reference

Object Properties

Property Name Type Description
z int The z-order of the link view
isSelected bool A boolean indicating whether the link is currently selected

Child Components

The following child components are used by LinkView:

Key Handling

The LinkView component handles the following key presses:

Focus Handling

The LinkView component handles focus as follows:

Visual Feedback

The LinkView component provides visual feedback as follows:

NodesRectOverview.qml

Overview

The NodesRectOverview.qml component provides a user interface for an overview of a node rectangle in the NodeLink architecture. It serves as a visual representation of a node rectangle in a reduced scale, allowing users to navigate and understand the overall layout of nodes and links.

In the NodeLink Model-View-Controller (MVC) architecture, NodesRectOverview.qml acts as a View component. It displays the overview of node rectangles and provides properties and signals to interact with the Model and Controller. Specifically, it fits into the architecture as follows:

Component Description

The NodesRectOverview.qml component inherits from I_NodesRect, an interface or base component provided by the NodeLink architecture. It defines an overview of a node rectangle, including its top-left position and a scale factor for mapping between the scene and overview.

Properties

The following properties are exposed by the NodesRectOverview.qml component:

Additionally, the component exposes the following object properties:

Signals

No signals are explicitly declared in the provided code.

Functions

No functions are explicitly declared in the provided code.

Example Usage in QML

To use the NodesRectOverview.qml component in a QML file, you can import it and create an instance:

import NodeLink

NodesRectOverview {
    id: overview

    nodeRectTopLeft: Qt.vector2d(100, 100)
    overviewScaleFactor: 1.5

    // Access viewProperties
    console.log(overview.viewProperties.nodeRectTopLeft)
    console.log(overview.viewProperties.overviewScaleFactor)
}

Extension/Override Points

To extend or override the behavior of NodesRectOverview.qml, you can:

Caveats or Assumptions

The following assumptions are made:

The following components are related to NodesRectOverview.qml:

SceneViewBackground.qml

Overview

The SceneViewBackground.qml component is responsible for drawing the background of a scene in the NodeLink application. It utilizes a C++-based background grid component, BackgroundGridsCPP, to render a customizable grid pattern.

In the NodeLink Model-View-Controller (MVC) architecture, SceneViewBackground.qml serves as a view component that displays the background of the scene. It interacts with the application's style and configuration to determine the appearance of the background grid.

Component Description

The SceneViewBackground.qml component is a QML-based view that leverages the BackgroundGridsCPP C++ component to draw the background grid. It exposes properties to customize the grid's appearance and inherits behavior from its C++ counterpart.

Properties

The following properties are exposed by the SceneViewBackground.qml component:

Signals

None.

Functions

None.

Example Usage in QML

To use the SceneViewBackground.qml component in a QML file, simply import the necessary modules and instantiate the component:

import QtQuick
import NodeLink

SceneViewBackground {
    // No additional properties need to be set, as they are retrieved from the application's style
}

Extension/Override Points

To customize the appearance or behavior of the SceneViewBackground.qml component, you can:

Caveats or Assumptions

HorizontalScrollBar.qml

Overview

The HorizontalScrollBar component is a customized ScrollBar designed to provide a horizontal scrolling indicator with a specific visual style. It is intended to be used within the NodeLink MVC architecture to provide a consistent user experience.

Architecture Integration

In the NodeLink MVC architecture, the HorizontalScrollBar component serves as a view component, responsible for rendering the horizontal scrolling indicator. It can be used in conjunction with NodeLink views to provide a seamless user experience.

Component Description

The HorizontalScrollBar component is a subclass of ScrollBar from QtQuick.Controls. It customizes the appearance of the scroll bar to have a horizontal orientation with a specific height and opacity.

Properties

The following properties are inherited from ScrollBar and can be used to customize the behavior of the HorizontalScrollBar component:

The component also defines the following properties:

Signals

The HorizontalScrollBar component does not define any custom signals. It inherits signals from ScrollBar, including:

Functions

The HorizontalScrollBar component does not define any custom functions. It inherits functions from ScrollBar, including:

Example Usage in QML

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    id: window
    width: 800
    height: 600

    Flickable {
        id: flickable
        width: window.width
        height: window.height
        contentWidth: 1000
        HorizontalScrollBar {
            parent: flickable
            anchors.bottom: flickable.bottom
            anchors.left: flickable.left
            anchors.right: flickable.right
        }
    }
}

Extension/Override Points

To customize the appearance or behavior of the HorizontalScrollBar component, you can:

Caveats or Assumptions

NLRepeater.qml

Overview

The NLRepeater component is a custom QML component designed to manage model overlays by converting JavaScript arrays to a ListModel. This enables intelligent control of model changes, making it a crucial part of the NodeLink MVC architecture.

In the NodeLink MVC (Model-View-Controller) architecture, NLRepeater serves as a bridge between the model and view components. It allows for dynamic management of model data, enabling efficient updates and manipulation of the data displayed in the view.

Component Description

NLRepeater is a QML component that extends the built-in Repeater component. It provides a temporary model (repeaterModel) for connection to the repeater, enabling intelligent control of model changes.

Properties

Signals

None

Functions

Example Usage in QML

import NodeLink 1.0

NLRepeater {
    id: repeater

    // Example usage of addElement function
    Component.onCompleted: {
        repeater.addElement({"name": "Item 1", "_qsUuid": "uuid1"});
        repeater.addElement({"name": "Item 2", "_qsUuid": "uuid2"});
    }

    // Example usage of removeElement function
    Button {
        text: "Remove Item 1"
        onClicked: repeater.removeElement({"name": "Item 1", "_qsUuid": "uuid1"});
    }

    // Delegate example
    delegate: Rectangle {
        text: qsObj.name
    }
}

Extension/Override Points

To extend or customize the behavior of NLRepeater, you can:

Caveats or Assumptions

NLToolTip.qml

Overview

The NLToolTip component is a custom tooltip designed for the NodeLink framework. It provides a customizable tooltip with a sleek and modern design.

In the NodeLink MVC architecture, NLToolTip is a view component that can be used to display additional information about nodes or other graphical elements. It does not interact directly with the model or controller, but rather serves as a visual aid to enhance the user experience.

Component Description

NLToolTip is a QML component that extends the built-in ToolTip control from Qt Quick Controls. It provides a custom background and content item, allowing for a high degree of customization.

Properties

The following properties are available:

Signals

None

Functions

None

Example Usage in QML

import QtQuick
import NodeLink

Item {
    ToolTip {
        text: "Hello World"
    }
    NLToolTip {
        text: "This is a custom tooltip"
        delay: 500
        visible: true
    }
}

Extension/Override Points

To extend or customize the behavior of NLToolTip, you can:

Caveats or Assumptions

Best Practices

SideMenuButtonGroup.qml

Overview

The SideMenuButtonGroup component is a customizable button group designed for the side menu in the NodeLink application. It provides a container for a collection of buttons, allowing for a flexible and organized way to present menu options.

In the NodeLink Model-View-Controller (MVC) architecture, the SideMenuButtonGroup component serves as a View component. It is responsible for rendering a group of buttons in the side menu, which is likely controlled by a Controller that manages the application's menu logic.

Component Description

The SideMenuButtonGroup component is a Rectangle that contains a ColumnLayout of buttons. It provides a default property contents to add buttons or other components to the group.

Properties

Object Properties

Signals

None.

Functions

None.

Example Usage in QML

import QtQuick
import NodeLink

SideMenuButtonGroup {
    // Add buttons to the group
    contents: [
        Button { text: "Button 1" },
        Button { text: "Button 2" },
        Button { text: "Button 3" }
    ]
}

Extension/Override Points

To extend or customize the SideMenuButtonGroup component, you can:

Caveats or Assumptions

TextIcon.qml

Overview

The TextIcon component is a simple QML text component designed to display FontAwesome icons. It provides a straightforward way to show icons in a NodeLink application, leveraging the power of FontAwesome for consistent and scalable iconography.

Architecture Integration

In the context of the NodeLink MVC (Model-View-Controller) architecture, the TextIcon component primarily serves as a view component. It can be used within various views to display icons, enhancing the user interface with visual elements that are consistent across the application.

Component Description

The TextIcon component extends the base Text component from QtQuick, customizing it to display FontAwesome icons. It sets specific properties to ensure that the text is rendered as a FontAwesome icon, centered both horizontally and vertically.

Properties

The following properties are defined or inherited by the TextIcon component:

Additionally, it inherits all properties from the Text component, such as text, font, color, etc., which can be used to customize the appearance and content of the icon.

Signals

The TextIcon component does not define any custom signals. It inherits signals from the Text component, such as onLinkActivated.

Functions

No custom functions are defined in the TextIcon component. It relies on the functions provided by the Text component.

Example Usage in QML

import QtQuick 2.15
import NodeLink 1.0

Item {
    TextIcon {
        text: "\uF0A8" // Example FontAwesome icon code
        font.pixelSize: 24
        color: "blue"
    }
}

Extension/Override Points

To extend or customize the TextIcon component, you can:

Caveats or Assumptions

By using the TextIcon component, you can easily integrate FontAwesome icons into your NodeLink application, enhancing the user interface with a wide range of scalable and customizable icons.

VerticalScrollBar.qml

Overview

The VerticalScrollBar component is a customized vertical scrollbar designed for use in NodeLink-based user interfaces. It provides a compact and translucent scrollbar that can be easily integrated into various QML-based applications.

Architecture

In the context of the NodeLink MVC architecture, the VerticalScrollBar component serves as a view component, primarily responsible for rendering the scrollbar and handling user interactions. It does not directly interact with the model or controller but can be used in conjunction with NodeLink's view components to provide a seamless user experience.

Component Description

The VerticalScrollBar component is a subclass of QtQuick.Controls.ScrollBar, customized to provide a vertical scrollbar with a specific appearance.

Properties

The following properties are relevant to the VerticalScrollBar component:

The background property is also used, which is a child component of type Rectangle:

Signals

The VerticalScrollBar component does not emit any custom signals. It inherits signals from the base ScrollBar component, such as onPositionChanged.

Functions

No custom functions are provided by the VerticalScrollBar component. It relies on the functions inherited from the base ScrollBar component.

Example Usage in QML

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    id: window
    width: 800
    height: 600

    Flickable {
        id: flickable
        width: parent.width
        height: parent.height
        contentWidth: width
        contentHeight: 1000 // Example content height

        VerticalScrollBar {
            id: scrollBar
            parent: flickable
            anchors.right: parent.right
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            orientation: Qt.Vertical
            policy: ScrollBar.AlwaysOn
        }

        // Your content here
        Rectangle {
            anchors.fill: parent
            color: "lightblue"
        }
    }
}

Extension/Override Points

To extend or customize the behavior of the VerticalScrollBar component, you can:

Caveats or Assumptions

NLButton.qml

Overview

The NLButton component is a customizable button designed for use within the NodeLink MVC architecture. It extends the NLBaseButton component, adding a simple text object as its content.

Architecture Integration

In the NodeLink MVC architecture, NLButton serves as a view component, primarily used in the presentation layer. It can be utilized in various contexts where a standard button with text content is required.

Component Description

NLButton is a QML component that inherits from NLBaseButton. It features a text-based content item, making it suitable for scenarios where a button with textual content is needed.

Properties

The following properties are available:

Signals

NLButton does not emit any custom signals. It inherits signals from NLBaseButton and QtQuick.Controls.Button.

Functions

No custom functions are provided by NLButton. It relies on the functions inherited from NLBaseButton and QtQuick.Controls.Button.

Example Usage in QML

import QtQuick 2.0

Row {
    NLButton {
        text: "Click Me"
        onClicked: console.log("Button clicked")
    }
    NLButton {
        text: "Disabled Button"
        enabled: false
    }
}

Extension/Override Points

To extend or customize NLButton, consider the following:

Caveats or Assumptions

NLIconButtonRound.qml

Overview

The NLIconButtonRound is a QML component that represents a round button with an icon as its content. It extends the NLIconButton component and provides a simplified way to create round buttons with a uniform size.

The NLIconButtonRound component fits into the NodeLink MVC architecture as a view component, specifically designed to display a round button with an icon. It can be used in various parts of the application where a round button is required.

Component Description

The NLIconButtonRound component is a round button with an icon as its content. It has a single property size that determines both the width and height of the button, making it easy to use and size.

Properties

Signals

The NLIconButtonRound component does not emit any custom signals. It inherits signals from its parent NLIconButton component.

Functions

The NLIconButtonRound component does not have any custom functions. It inherits functions from its parent NLIconButton component.

Example Usage in QML

import QtQuick
import QtQuick.Controls

Item {
    NLIconButtonRound {
        id: button
        size: 50
        anchors.centerIn: parent
    }
}

Extension/Override Points

To extend or customize the NLIconButtonRound component, you can:

Caveats or Assumptions

NLSideMenuButton.qml

Overview

The NLSideMenuButton component is a specialized button designed for use in side menus, providing a consistent and visually appealing interface element. It extends the NLIconButton component, inheriting its core functionality while adding specific properties tailored for side menu buttons.

In the NodeLink MVC (Model-View-Controller) architecture, NLSideMenuButton serves as a view component. It is intended to be used within the application's UI, specifically in the side menu section, to provide users with interactive buttons that can be used for navigation or actions.

Component Description

NLSideMenuButton is a QML component that represents a side menu button. It inherits from NLIconButton and customizes its appearance and behavior for a side menu context. The component is designed to display an icon and text, with a hover and checked state that changes its text color.

Properties

The following properties are exposed by NLSideMenuButton:

Signals

NLSideMenuButton does not emit any custom signals. It relies on the signals inherited from NLIconButton and possibly its base QML types.

Functions

This component does not introduce any new functions beyond those inherited from NLIconButton and its base types.

Example Usage in QML

import QtQuick 2.0

Row {
    NLSideMenuButton {
        icon.source: "icon1.png"
        text: "Menu Item 1"
    }
    NLSideMenuButton {
        icon.source: "icon2.png"
        text: "Menu Item 2"
    }
}

Extension/Override Points

To extend or customize the behavior of NLSideMenuButton, you can:

Caveats or Assumptions

HelpersView.qml

Overview

The HelpersView component is a crucial part of the NodeLink MVC architecture, responsible for managing and displaying helper classes. It acts as a container for various helper views, providing a centralized location for rendering scene-related assistance.

Architecture Integration

In the NodeLink MVC architecture, HelpersView fits into the View layer, working closely with the I_Scene and SceneSession models. It receives updates from these models and reacts accordingly, ensuring a seamless user experience.

Component Description

The HelpersView component is an Item that fills its parent area. It serves as a host for two primary helper views:

Properties

The following properties are exposed by HelpersView:

Signals

None.

Functions

None.

Example Usage in QML

To use HelpersView in a QML file, simply import the necessary modules and create an instance of the component:

import QtQuick
import NodeLink

Item {
    // ...
    HelpersView {
        scene: myScene
        sceneSession: mySceneSession
        anchors.fill: parent
    }
    // ...
}

Extension/Override Points

To extend or customize the behavior of HelpersView, consider the following:

Caveats or Assumptions

The following components are closely related to HelpersView:

ConfirmPopup.qml

Overview

The ConfirmPopup.qml component is a customizable popup dialog used to confirm user actions. It provides a simple way to display a confirmation message to the user, allowing them to either accept or cancel the action.

In the NodeLink MVC architecture, the ConfirmPopup.qml component serves as a view, responsible for displaying the confirmation dialog to the user. It receives input from the controller and emits signals to notify the controller of the user's response.

Component Description

The ConfirmPopup.qml component is an Item that displays a confirmation dialog with a message, two buttons (accept and cancel), and optional custom content.

Properties

Property Name Type Description
message string The confirmation message to display to the user.
acceptButtonText string The text to display on the accept button. Default is "OK".
cancelButtonText string The text to display on the cancel button. Default is "Cancel".
showAcceptButton bool Whether to show the accept button. Default is true.
showCancelButton bool Whether to show the cancel button. Default is true.

Signals

Signal Name Description
accepted() Emitted when the user accepts the confirmation.
canceled() Emitted when the user cancels the confirmation.

Functions

Function Name Description
open() Opens the confirmation dialog.
close() Closes the confirmation dialog.

Example Usage in QML

import QtQuick 2.12

ApplicationWindow {
    id: window
    visible: true

    ConfirmPopup {
        id: confirmPopup
        message: "Are you sure you want to delete this item?"
        onAccepted: console.log("Confirmed")
        onCanceled: console.log("Canceled")
    }

    Button {
        text: "Show Confirm Popup"
        onClicked: confirmPopup.open()
    }
}

Extension/Override Points

To customize the appearance or behavior of the ConfirmPopup.qml component, you can override the following:

Caveats or Assumptions

HashCompareStringCPP.cpp

Overview

The HashCompareStringCPP class is a Qt-based utility class designed to compare two string models by generating their hash values using the MD5 algorithm. This class fits into the NodeLink MVC architecture as a supporting utility component, providing a specific functionality that can be used across various models.

Purpose and Architecture Fit

The primary purpose of HashCompareStringCPP is to enable efficient and secure comparison of string models. By utilizing hash values, it avoids direct string-to-string comparisons, which can be beneficial in scenarios where strings are large or when performance is critical. In the context of the NodeLink MVC architecture, this class serves as a tool that can be leveraged by model components to compare string data securely and efficiently.

Class Description

The HashCompareStringCPP class is a subclass of QObject, allowing it to integrate seamlessly into Qt and QML applications. It provides a single public function for comparing two string models.

Properties

None.

Signals

None.

Functions

bool compareStringModels(QString strModelFirst, QString strModelSecound)

Compares two string models by generating their MD5 hash values and checking if they are equal.

Example Usage in QML

To use HashCompareStringCPP in QML, you need to create an instance of it in C++ and expose it to the QML context. Here's a basic example:

// In main.cpp or where your QML context is set up
#include "HashCompareStringCPP.h"

int main(int argc, char *argv[]) {
    // ...
    HashCompareStringCPP hashComparator;
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("hashComparator", &hashComparator);
    // ...
}

Then, in your QML:

import QtQuick 2.15

Item {
    function compareStrings(str1, str2) {
        return hashComparator.compareStringModels(str1, str2)
    }

    // Example usage
    onCompleted: console.log("Are strings equal?", compareStrings("Hello", "Hello"))
}

Extension/Override Points

Caveats or Assumptions

NLUtilsCPP.cpp

Overview

The NLUtilsCPP class provides a set of utility functions for common tasks, such as converting image URLs to base64-encoded strings and key sequences to human-readable strings. This class is part of the NodeLink MVC architecture and serves as a supporting component for various NodeLink features.

In the NodeLink MVC architecture, NLUtilsCPP acts as a utility class that provides helper functions for other components. It does not directly interact with the Model or View but instead offers a set of reusable functions that can be used throughout the application.

Class Description

The NLUtilsCPP class is a QObject-based class that provides the following functionality:

Properties

The NLUtilsCPP class does not have any properties.

Signals

The NLUtilsCPP class does not emit any signals.

Functions

imageURLToImageString(QString url)

keySequenceToString(int keySequence)

Example Usage in QML

To use the NLUtilsCPP class in QML, you need to create an instance of the class and expose it to the QML context. Here's an example:

import QtQuick 2.12
import NodeLink 1.0

Item {
    id: root

    // Create an instance of NLUtilsCPP
    property NLUtilsCPP utils: NLUtilsCPP {}

    // Convert an image URL to a base64-encoded string
    property string imageString: utils.imageURLToImageString("path/to/image.png")

    // Convert a key sequence to a human-readable string
    property string keySequenceString: utils.keySequenceToString(QKeySequence.Copy)
}

Extension/Override Points

To extend or override the functionality of the NLUtilsCPP class, you can:

Caveats or Assumptions

The NLUtilsCPP class is related to other NodeLink components, such as:

NLUtilsCPP.h

Overview

The NLUtilsCPP class provides a set of utility functions for converting and processing data in the NodeLink application. It is designed to be used in conjunction with the NodeLink MVC architecture, providing a bridge between C++ and QML.

In the NodeLink MVC architecture, NLUtilsCPP serves as a utility class that provides functionality to the Model or Controller components. It does not directly interact with the View, but rather provides functions that can be used by the Model or Controller to process data.

Class Description

The NLUtilsCPP class is a QObject-based class that provides two utility functions: imageURLToImageString and keySequenceToString. These functions can be invoked from QML using the Q_INVOKABLE macro.

Properties

None.

Signals

None.

Functions

imageURLToImageString(QString url)

keySequenceToString(int keySequence)

Example Usage in QML

import QtQuick 2.15
import NodeLink 1.0

Item {
    id: root

    // Create an instance of NLUtilsCPP
    property NLUtilsCPP utils: NLUtilsCPP {}

    // Convert an image URL to a string
    property string imageString: utils.imageURLToImageString("path/to/image.png")

    // Convert a key sequence to a string
    property string keySequenceString: utils.keySequenceToString(Qt.Key_A)
}

Extension/Override Points

To extend or override the functionality of NLUtilsCPP, you can:

Caveats or Assumptions

Registration

The NLUtilsCPP class is registered with the QML engine using the QML_ELEMENT macro. To use this class in QML, ensure that the NodeLink module is imported.

Notes

Styling Guide: Customizing Appearance

Overview

This guide provides comprehensive instructions for customizing the appearance of NodeLink applications. You'll learn how to modify colors, fonts, sizes, and other visual properties to create a custom look and feel that matches your application's design requirements.

Understanding the Styling System

NodeLink uses a hierarchical styling system:

  1. NLStyle - Global style singleton with default values
  2. GUI Config Objects - Per-object configuration (NodeGuiConfig, LinkGUIConfig, etc.)
  3. Node Registry - Node type-specific colors and icons
  4. View Components - QML components that render the visual elements

Styling Layers

NLStyle (Global Defaults)
    ↓
Node Registry (Type-specific defaults)
    ↓
GUI Config (Per-object configuration)
    ↓
View Components (Rendering)

Customizing Global Theme

Modifying NLStyle

Note: NLStyle properties are readonly, so you have two options:

  1. Modify the source file (resources/View/NLStyle.qml) for permanent changes
  2. Create a custom style object for application-specific styling

Option 1: Modifying NLStyle Source

Edit resources/View/NLStyle.qml:

QtObject {
    // Change primary color
    readonly property string primaryColor: "#FF5733"  // Orange theme

    // Change background colors
    readonly property string primaryBackgroundColor: "#0a0a0a"  // Darker background
    readonly property string primaryTextColor: "#ffffff"  // White text

    // Change node defaults
    readonly property QtObject node: QtObject {
        property real width: 250  // Larger default width
        property real height: 180  // Larger default height
        property string color: "#4A90E2"  // Blue default
    }

    // Change port view size
    readonly property QtObject portView: QtObject {
        property int size: 20  // Larger ports
        property int borderSize: 3
    }
}

Option 2: Creating a Custom Style Object

Create a custom style object in your application:

// MyCustomStyle.qml
pragma Singleton
import QtQuick

QtObject {
    // Override or extend NLStyle
    property string customPrimaryColor: "#FF5733"
    property string customAccentColor: "#33C3F0"

    // Custom color palette
    readonly property var colorPalette: [
        "#FF5733",  // Orange
        "#33C3F0",  // Blue
        "#9B59B6",  // Purple
        "#2ECC71"   // Green
    ]
}

Setting Window Theme

In your main.qml:

import QtQuick.Controls

Window {
    id: window

    // Set window background
    color: NLStyle.primaryBackgroundColor

    // Set Material theme (if using Material style)
    Material.theme: Material.Dark
    Material.accent: NLStyle.primaryColor

    // Or use custom colors
    Material.accent: "#FF5733"  // Custom accent
}

Customizing Node Appearance

Setting Node Colors

Method 1: In Node Definition

// SourceNode.qml
import QtQuick
import NodeLink

Node {
    type: 0

    // Set default appearance
    guiConfig.width: 200
    guiConfig.height: 150
    guiConfig.color: "#4A90E2"  // Blue
    guiConfig.opacity: 0.9

    Component.onCompleted: addPorts();
}

Method 2: In Node Registry

// main.qml
Component.onCompleted: {
    // Register node with custom color
    nodeRegistry.nodeTypes[0] = "SourceNode";
    nodeRegistry.nodeNames[0] = "Source";
    nodeRegistry.nodeIcons[0] = "\uf1c0";
    nodeRegistry.nodeColors[0] = "#4A90E2";  // Custom color
}

Method 3: Programmatically

// Create and style node
var node = NLCore.createNode();
node.type = 0;
node.guiConfig.color = "#FF5733";  // Orange
node.guiConfig.width = 250;
node.guiConfig.height = 180;
node.guiConfig.opacity = 0.95;

Node Size Configuration

Node {
    // Fixed size
    guiConfig.width: 250
    guiConfig.height: 180
    guiConfig.autoSize: false

    // OR auto-sizing with constraints
    guiConfig.autoSize: true
    guiConfig.minWidth: 150
    guiConfig.minHeight: 100
    guiConfig.baseContentWidth: 120
}

Node Opacity and Locking

Node {
    // Set opacity
    guiConfig.opacity: 0.8  // 80% opacity

    // Lock node (prevents movement)
    guiConfig.locked: true
}

Custom Node Icons

// In node registry
nodeRegistry.nodeIcons[0] = "\uf1c0";  // Font Awesome file icon
nodeRegistry.nodeIcons[1] = "\uf0ad";  // Font Awesome cog icon
nodeRegistry.nodeIcons[2] = "\uf1b3";  // Font Awesome cube icon

// Or use custom image
Node {
    guiConfig.logoUrl: "qrc:/icons/my-custom-icon.png"
}

Node Border Styling

Node borders are controlled by NLStyle.node.borderWidth and NLStyle.node.borderLockColor:

// In NLStyle.qml
readonly property QtObject node: QtObject {
    property int borderWidth: 3  // Thicker border
    property string borderLockColor: "#FF0000"  // Red for locked nodes
}

Customizing Container Appearance

Container Colors and Sizes

// Create and style container
var container = scene.createContainer();
container.title = "My Container";
container.guiConfig.width = 600;
container.guiConfig.height = 400;
container.guiConfig.color = "#2d2d2d";  // Dark gray
container.guiConfig.position = Qt.vector2d(100, 100);

Container Locking

// Lock container
container.guiConfig.locked = true;

Container Text Height

// Adjust title area height
container.guiConfig.containerTextHeight = 40;

Customizing Scene Appearance

Scene Background

// In main.qml
Window {
    color: NLStyle.primaryBackgroundColor  // Dark background

    // Or custom color
    color: "#0a0a0a"  // Very dark
}

Background Grid

// In SceneViewBackground.qml or custom component
BackgroundGridsCPP {
    spacing: 25  // Larger grid spacing
    opacity: 0.5  // More transparent
}

// Or use NLStyle defaults
BackgroundGridsCPP {
    spacing: NLStyle.backgroundGrid.spacing  // 20
    opacity: NLStyle.backgroundGrid.opacity  // 0.65
}

Scene Dimensions

// Configure scene view size
scene.sceneGuiConfig.contentWidth = 6000;
scene.sceneGuiConfig.contentHeight = 6000;
scene.sceneGuiConfig.contentX = 2000;
scene.sceneGuiConfig.contentY = 2000;

Snap to Grid

// Enable snap to grid globally
NLStyle.snapEnabled = true;

// Use in node positioning
if (NLStyle.snapEnabled) {
    var snappedPos = scene.snappedPosition(position);
    node.guiConfig.position = snappedPos;
}

Customizing Port Appearance

Port Colors

// Create port with custom color
var port = NLCore.createPort();
port.portType = NLSpec.PortType.Input;
port.portSide = NLSpec.PortPositionSide.Left;
port.title = "Input";
port.color = "#4A90E2";  // Blue port

Port Size

Port size is controlled globally by NLStyle.portView.size:

// In NLStyle.qml
readonly property QtObject portView: QtObject {
    property int size: 20  // Larger ports (default: 18)
    property int borderSize: 3  // Thicker border (default: 2)
    property double fontSize: 12  // Larger font (default: 10)
}

Port Enable/Disable

// Disable port (grayed out, cannot connect)
port.enable = false;

// Enable port
port.enable = true;

Color Schemes and Themes

Dark Theme (Default)

// Dark theme colors
readonly property string primaryBackgroundColor: "#1e1e1e"
readonly property string primaryTextColor: "white"
readonly property string primaryBorderColor: "#363636"
readonly property string backgroundGray: "#2A2A2A"

Light Theme

// Light theme colors
readonly property string primaryBackgroundColor: "#f5f5f5"
readonly property string primaryTextColor: "#333333"
readonly property string primaryBorderColor: "#cccccc"
readonly property string backgroundGray: "#e0e0e0"

Custom Color Palette

Create a color palette for your application:

// ColorPalette.qml
pragma Singleton
import QtQuick

QtObject {
    // Primary colors
    readonly property string primary: "#4890e2"
    readonly property string secondary: "#FF5733"
    readonly property string accent: "#33C3F0"

    // Semantic colors
    readonly property string success: "#2ECC71"
    readonly property string warning: "#F39C12"
    readonly property string error: "#E74C3C"
    readonly property string info: "#3498DB"

    // Neutral colors
    readonly property string dark: "#1e1e1e"
    readonly property string light: "#f5f5f5"
    readonly property string gray: "#95a5a6"

    // Node type colors
    readonly property var nodeColors: [
        "#4A90E2",  // Source nodes
        "#F5A623",  // Operation nodes
        "#7ED321",  // Result nodes
        "#BD10E0",  // Special nodes
        "#50E3C2"   // Utility nodes
    ]
}

Using Color Palette

// In node definitions
Node {
    guiConfig.color: ColorPalette.nodeColors[0]  // Use palette color
}

// In links
link.guiConfig.color = ColorPalette.primary;

// In containers
container.guiConfig.color = ColorPalette.secondary;

Color by Node Type

// Register nodes with type-specific colors
Component.onCompleted: {
    // Source nodes - Blue
    nodeRegistry.nodeColors[CSpecs.NodeType.Source] = "#4A90E2";

    // Operation nodes - Orange
    nodeRegistry.nodeColors[CSpecs.NodeType.Operation] = "#F5A623";

    // Result nodes - Green
    nodeRegistry.nodeColors[CSpecs.NodeType.Result] = "#7ED321";
}

Font Customization

Available Fonts

NodeLink uses two main font families:

  1. Roboto - For text content
  2. Font Awesome 6 Pro - For icons

Using Fonts

// Text with Roboto
Text {
    font.family: NLStyle.fontType.roboto
    font.pointSize: 12
    text: "Node Title"
}

// Icon with Font Awesome
Text {
    font.family: NLStyle.fontType.font6Pro
    font.pointSize: 16
    text: "\uf04b"  // Font Awesome icon
}

Font Sizes

Node font sizes are controlled by NLStyle.node:

// In NLStyle.qml
readonly property QtObject node: QtObject {
    property int fontSizeTitle: 12  // Title font size (default: 10)
    property int fontSize: 11  // Content font size (default: 9)
}

Custom Fonts

To use custom fonts:

  1. Add font file to your resources:

    // In main.qml
    FontLoader {
        source: "qrc:/fonts/MyCustomFont.ttf"
    }
  2. Use in NLStyle:

    readonly property QtObject fontType: QtObject {
        readonly property string roboto: "Roboto"
        readonly property string font6Pro: "Font Awesome 6 Pro"
        readonly property string custom: "MyCustomFont"  // Your font
    }
  3. Use in components:

    Text {
        font.family: NLStyle.fontType.custom
        text: "Custom Font Text"
    }

Best Practices

1. Consistency

2. Accessibility

3. Performance

4. Theming

5. Customization Levels

6. Color Selection

Complete Styling Example

Here's a complete example of a styled NodeLink application:

// main.qml
import QtQuick
import QtQuick.Controls
import NodeLink

Window {
    id: window

    // Window styling
    width: 1280
    height: 960
    color: "#1e1e1e"  // Dark background
    Material.theme: Material.Dark
    Material.accent: "#4890e2"

    property Scene scene: Scene { }
    property NLNodeRegistry nodeRegistry: NLNodeRegistry {
        _qsRepo: NLCore.defaultRepo
        imports: ["MyApp", "NodeLink"]
        defaultNode: 0
    }

    Component.onCompleted: {
        // Register nodes with custom colors
        nodeRegistry.nodeTypes[0] = "SourceNode";
        nodeRegistry.nodeNames[0] = "Source";
        nodeRegistry.nodeIcons[0] = "\uf1c0";
        nodeRegistry.nodeColors[0] = "#4A90E2";  // Blue

        nodeRegistry.nodeTypes[1] = "OperationNode";
        nodeRegistry.nodeNames[1] = "Operation";
        nodeRegistry.nodeIcons[1] = "\uf0ad";
        nodeRegistry.nodeColors[1] = "#F5A623";  // Orange

        // Initialize scene
        NLCore.defaultRepo = NLCore.createDefaultRepo(["QtQuickStream", "NodeLink", "MyApp"])
        NLCore.defaultRepo.initRootObject("Scene");
        window.scene = Qt.binding(function() {
            return NLCore.defaultRepo.qsRootObject;
        });
        window.scene.nodeRegistry = nodeRegistry;
    }

    // Main view
    NLView {
        id: view
        scene: window.scene
        anchors.fill: parent
    }
}
// SourceNode.qml
import QtQuick
import NodeLink

Node {
    type: 0

    // Custom styling
    guiConfig.width: 200
    guiConfig.height: 150
    guiConfig.color: "#4A90E2"  // Blue
    guiConfig.opacity: 0.9
    guiConfig.autoSize: true
    guiConfig.minWidth: 150
    guiConfig.minHeight: 100

    Component.onCompleted: addPorts();

    function addPorts() {
        let port = NLCore.createPort();
        port.portType = NLSpec.PortType.Output;
        port.portSide = NLSpec.PortPositionSide.Right;
        port.title = "Output";
        port.color = "#4A90E2";  // Match node color
        addPort(port);
    }
}

Troubleshooting

Colors Not Applying

Fonts Not Loading

Styles Not Updating

QML components

Overview

This document provides a comprehensive reference for all QML components, properties, functions, and signals available in the NodeLink framework. Use this reference to understand the API structure and how to interact with NodeLink components programmatically.

QML Components Reference

Scene

Location: resources/Core/Scene.qml
Inherits: I_Scene
Purpose: The main scene component that manages all nodes, links, and containers in the visual programming environment.

Where to Use

Scene is used in the following contexts:

  1. Main Application Window (main.qml):
    • Declare as a property in your main Window
    • Initialize with QtQuickStream repository
    • Connect to NLView for rendering
// examples/simpleNodeLink/main.qml
Window {
    property Scene scene: Scene { }

    Component.onCompleted: {
        NLCore.defaultRepo = NLCore.createDefaultRepo([
            "QtQuickStream",
            "NodeLink",
            "MyApp"
        ]);
        NLCore.defaultRepo.initRootObject("Scene");
        window.scene = Qt.binding(function() {
            return NLCore.defaultRepo.qsRootObject;
        });
    }
}
  1. Custom Scene Classes:
    • Inherit from Scene or I_Scene for custom behavior
    • Override createCustomizeNode() for custom node creation
    • Override linkNodes() and canLinkNodes() for custom validation
    • Implement data flow logic (e.g., updateData())
// examples/calculator/resources/Core/CalculatorScene.qml
I_Scene {
    function createCustomizeNode(nodeType: int, xPos: real, yPos: real): string {
        // Custom node creation logic
    }

    function updateData() {
        // Data flow calculation logic
    }
}
  1. Event Handlers:
    • Connect to signals (nodeAdded, linkAdded, etc.) for UI updates
    • Handle data flow when connections change
    • Manage undo/redo operations
Connections {
    target: scene
    function onLinkAdded(link) {
        updateData();  // Recalculate data flow
    }
}
  1. Node Management:
    • Create, delete, and clone nodes programmatically
    • Manage node selection
    • Organize nodes with containers

Use Cases:

Properties

title: string

Scene title/name.

Default: "<Untitled>"

Example:

scene.title = "My Calculator Scene"
nodes: var

Map of all nodes in the scene. Key is node UUID, value is Node object.

Type: map<UUID, Node>

Example:

// Iterate over all nodes
Object.values(scene.nodes).forEach(function(node) {
    console.log("Node:", node.title);
});

// Get node by UUID
var myNode = scene.nodes[nodeUuid];

Map of all links in the scene. Key is link UUID, value is Link object.

Type: map<UUID, Link>

Example:

// Iterate over all links
Object.values(scene.links).forEach(function(link) {
    console.log("Link from", link.inputPort.title, "to", link.outputPort.title);
});
containers: var

Map of all containers in the scene. Key is container UUID, value is Container object.

Type: map<UUID, Container>

Example:

// Get all containers
Object.values(scene.containers).forEach(function(container) {
    console.log("Container:", container.title);
});
selectionModel: SelectionModel

Selection model for managing selected objects in the scene.

Example:

// Select a node
scene.selectionModel.selectNode(node);

// Clear selection
scene.selectionModel.clear();

// Get selected objects
var selected = scene.selectionModel.selectedModel;
nodeRegistry: NLNodeRegistry

Registry containing all registered node types, names, icons, and colors.

Example:

// Access node registry
var nodeType = scene.nodeRegistry.nodeTypes[0];
var nodeName = scene.nodeRegistry.nodeNames[0];
sceneGuiConfig: SceneGuiConfig

GUI configuration for the scene (background, grid, etc.).

Example:

scene.sceneGuiConfig.backgroundColor = "#1e1e1e";

Signals

nodeAdded(Node node)

Emitted when a node is added to the scene.

Example:

Connections {
    target: scene
    function onNodeAdded(node) {
        console.log("Node added:", node.title);
    }
}
nodesAdded(var nodes)

Emitted when multiple nodes are added at once.

Parameter: Array of Node objects

nodeRemoved(Node node)

Emitted when a node is removed from the scene.

Emitted when a link is created between two ports.

Example:

Connections {
    target: scene
    function onLinkAdded(link) {
        console.log("Link created from", link.inputPort.title, "to", link.outputPort.title);
        // Update node data
        updateData();
    }
}

Emitted when multiple links are added at once.

Emitted when a link is removed from the scene.

containerAdded(Container container)

Emitted when a container is added to the scene.

containerRemoved(Container container)

Emitted when a container is removed from the scene.

copyCalled()

Emitted when copy operation is triggered (e.g., Ctrl+C).

pasteCalled()

Emitted when paste operation is triggered (e.g., Ctrl+V).

Functions

createCustomizeNode(nodeType: int, xPos: real, yPos: real): string

Creates a new node of the specified type at the given position.

Parameters:

Returns: UUID string of the created node, or null if creation failed

Example:

// Create a Source node at position (100, 200)
var nodeUuid = scene.createCustomizeNode(CSpecs.NodeType.Source, 100, 200);

Note: This function should be overridden in custom Scene implementations to customize node creation logic.

addNode(node: Node, autoSelect: bool): Node

Adds an existing node to the scene.

Parameters:

Returns: The added Node object

Example:

var newNode = NLCore.createNode();
newNode.title = "My Node";
newNode.type = 0;
scene.addNode(newNode, true);  // Add and select
addNodes(nodeArray: list<Node>, autoSelect: bool)

Adds multiple nodes to the scene at once.

Parameters:

Example:

var nodes = [node1, node2, node3];
scene.addNodes(nodes, false);
deleteNode(nodeUUId: string)

Deletes a node from the scene by UUID.

Parameters:

Example:

scene.deleteNode(node._qsUuid);

Note: This also removes all links connected to the node.

deleteNodes(nodeUUIds: list<string>)

Deletes multiple nodes from the scene.

Parameters:

Creates a link between two ports.

Parameters:

Returns: Created Link object

Example:

// Get ports from nodes
var outputPort = sourceNode.findPortByPortSide(NLSpec.PortPositionSide.Right);
var inputPort = targetNode.findPortByPortSide(NLSpec.PortPositionSide.Left);

// Create link
var link = scene.createLink(outputPort._qsUuid, inputPort._qsUuid);
linkNodes(portA: string, portB: string)

Links two nodes via their ports with validation.

Parameters:

Example:

scene.linkNodes(outputPortUuid, inputPortUuid);

Note: This function validates the link using canLinkNodes() before creating it. Override this function in custom Scene implementations to add custom validation logic.

canLinkNodes(portA: string, portB: string): bool

Checks if two ports can be linked.

Parameters:

Returns: true if the ports can be linked, false otherwise

Validation Rules:

Example:

if (scene.canLinkNodes(portA, portB)) {
    scene.linkNodes(portA, portB);
} else {
    console.error("Cannot link these ports");
}
unlinkNodes(portA: string, portB: string)

Removes a link between two ports.

Parameters:

Example:

scene.unlinkNodes(outputPortUuid, inputPortUuid);
findNode(portId: string): Node

Finds the node that contains the specified port.

Parameters:

Returns: Node object, or null if not found

Example:

var node = scene.findNode(portUuid);
if (node) {
    console.log("Found node:", node.title);
}
findNodeId(portId: string): string

Finds the UUID of the node that contains the specified port.

Parameters:

Returns: Node UUID string, or empty string if not found

findNodeByItsId(nodeId: string): Node

Finds a node by its UUID.

Parameters:

Returns: Node object, or undefined if not found

Example:

var node = scene.findNodeByItsId(nodeUuid);
findPort(portId: string): Port

Finds a port object by its UUID.

Parameters:

Returns: Port object, or null if not found

Example:

var port = scene.findPort(portUuid);
if (port) {
    console.log("Port title:", port.title);
}
cloneNode(nodeUuid: string): Node

Clones (duplicates) a node.

Parameters:

Returns: Cloned Node object

Example:

var clonedNode = scene.cloneNode(originalNode._qsUuid);
// Cloned node is positioned 50 pixels to the right and down

Note: The cloned node is automatically positioned 50 pixels offset from the original.

createContainer(): Container

Creates a new empty container.

Returns: New Container object

Example:

var container = scene.createContainer();
container.title = "My Container";
scene.addContainer(container);
addContainer(container: Container): Container

Adds a container to the scene.

Parameters:

Returns: The added Container object

deleteContainer(containerUUId: string)

Deletes a container from the scene.

Parameters:

deleteSelectedObjects()

Deletes all currently selected objects (nodes, links, containers).

Example:

// Select some objects
scene.selectionModel.selectNode(node1);
scene.selectionModel.selectNode(node2);

// Delete all selected
scene.deleteSelectedObjects();
isSceneEmpty(): bool

Checks if the scene is empty (no nodes, links, or containers).

Returns: true if scene is empty, false otherwise

Example:

if (scene.isSceneEmpty()) {
    console.log("Scene is empty");
}
snappedPosition(position: vector2d): vector2d

Calculates a snapped position based on grid spacing.

Parameters:

Returns: Snapped position vector

Example:

var snapped = scene.snappedPosition(Qt.vector2d(123, 456));
// Returns position snapped to grid
snapAllNodesToGrid()

Snaps all nodes and containers to the grid.

Example:

scene.snapAllNodesToGrid();
automaticNodeReorder(nodes: var, rootId: string, keepRootPosition: bool)

Automatically reorders nodes based on their connections.

Parameters:

Example:

// Reorder selected nodes
var selectedNodes = {};
Object.keys(scene.selectionModel.selectedModel).forEach(function(uuid) {
    if (scene.nodes[uuid]) {
        selectedNodes[uuid] = scene.nodes[uuid];
    }
});

var rootId = Object.keys(selectedNodes)[0];
scene.automaticNodeReorder(selectedNodes, rootId, true);
copyScene(): Scene

Creates a copy of the entire scene with all nodes, links, and containers.

Returns: New Scene object with copied content

Example:

var copiedScene = scene.copyScene();
// Use copiedScene for paste operation
findNodesInContainerItem(containerItem): var

Finds all nodes and containers that are inside a container's bounds.

Parameters:

Returns: Array of Node and Container objects

Example:

var items = scene.findNodesInContainerItem({
    x: 100,
    y: 100,
    width: 300,
    height: 200
});

findNodesInLasso(points): var

Finds all nodes and containers that are inside a selected polygon (in Lasso mode).

Parameters:

Returns:

Example:

var items = scene.findNodesInLasso(lassoSelection.pathPoints);

Node

Location: resources/Core/Node.qml
Inherits: I_Node
Purpose: Represents a node in the visual programming scene. Nodes are the main building blocks that can be connected via ports.

Where to Use

Node is used in the following contexts:

  1. Custom Node Definitions (.qml files):
    • Create custom node types by inheriting from Node
    • Define node-specific properties and behavior
    • Add ports in Component.onCompleted
// examples/calculator/resources/Core/SourceNode.qml
Node {
    type: CSpecs.NodeType.Source
    nodeData: I_NodeData {}

    Component.onCompleted: addPorts();

    function addPorts() {
        let port = NLCore.createPort();
        port.portType = NLSpec.PortType.Output;
        port.portSide = NLSpec.PortPositionSide.Right;
        addPort(port);
    }
}
  1. Scene Node Management:
    • Access nodes through scene.nodes map
    • Iterate over nodes for data processing
    • Find nodes by UUID or port
// In Scene or custom logic
Object.values(scene.nodes).forEach(function(node) {
    if (node.type === CSpecs.NodeType.Source) {
        // Process source nodes
    }
});
  1. Data Flow Processing:
    • Access node data through node.nodeData
    • Read from parent nodes via node.parents
    • Write to child nodes via node.children
// examples/calculator/resources/Core/CalculatorScene.qml
function updateData() {
    Object.values(scene.nodes).forEach(function(node) {
        // Get data from parent nodes
        Object.values(node.parents).forEach(function(parent) {
            node.nodeData.input = parent.nodeData.data;
        });
        // Process node data
        node.calculate();
    });
}
  1. Node Creation:
    • Create nodes programmatically using NLCore.createNode()
    • Create nodes via scene's createCustomizeNode()
    • Clone existing nodes
// Programmatic node creation
var node = NLCore.createNode();
node.type = 0;
node.title = "My Node";
scene.addNode(node, true);

Use Cases:

Properties

type: int

Unique integer identifier for the node type. Must match a type registered in nodeRegistry.

Example:

Node {
    type: CSpecs.NodeType.Source  // 0
}
title: string

Display name of the node.

Default: "<No Title>"

Example:

node.title = "Source Node 1"
guiConfig: NodeGuiConfig

GUI configuration object containing visual properties (position, size, color, etc.).

See: NodeGuiConfig

Example:

node.guiConfig.position = Qt.vector2d(100, 200);
node.guiConfig.width = 200;
node.guiConfig.height = 150;
node.guiConfig.color = "#4A90E2";
nodeData: I_NodeData

Data storage object for the node. Can be I_NodeData or a custom subclass.

See: NodeData

Example:

node.nodeData = I_NodeData {}
node.nodeData.data = "some value"
ports: var

Map of all ports belonging to this node. Key is port UUID, value is Port object.

Type: map<UUID, Port>

Example:

// Iterate over ports
Object.values(node.ports).forEach(function(port) {
    console.log("Port:", port.title, "Type:", port.portType);
});

// Get port by UUID
var port = node.ports[portUuid];
children: var

Map of child nodes (nodes connected to this node's output ports). Key is node UUID, value is Node object.

Type: map<UUID, Node>

Example:

// Iterate over children
Object.values(node.children).forEach(function(child) {
    console.log("Child node:", child.title);
});
parents: var

Map of parent nodes (nodes connected to this node's input ports). Key is node UUID, value is Node object.

Type: map<UUID, Node>

Example:

// Check if node has parents
if (Object.keys(node.parents).length > 0) {
    console.log("Node has", Object.keys(node.parents).length, "parents");
}
imagesModel: ImagesModel

Model for managing node images/icons.

Example:

node.imagesModel.addImage("qrc:/icons/my-icon.png");

Signals

portAdded(var portId)

Emitted when a port is added to the node.

Parameter: UUID string of the added port

Example:

Connections {
    target: node
    function onPortAdded(portId) {
        console.log("Port added:", portId);
    }
}
nodeCompleted()

Emitted after the node's Component.onCompleted signal. Indicates that all node properties have been set.

Example:

Connections {
    target: node
    function onNodeCompleted() {
        console.log("Node setup complete:", node.title);
        // Perform initialization tasks
    }
}
cloneFrom(baseNode: I_Node)

Signal emitted when the node is being cloned. Handle this signal to customize cloning behavior.

Example:

Node {
    onCloneFrom: function(baseNode) {
        // Copy base properties (done automatically)
        title = baseNode.title;
        type = baseNode.type;

        // Custom cloning logic
        myCustomProperty = baseNode.myCustomProperty;

        // Reset node-specific data
        nodeData.data = null;
    }
}

Functions

addPort(port: Port)

Adds a port to the node.

Parameters:

Example:

function addPorts() {
    let inputPort = NLCore.createPort();
    inputPort.portType = NLSpec.PortType.Input;
    inputPort.portSide = NLSpec.PortPositionSide.Left;
    inputPort.title = "Input";
    addPort(inputPort);

    let outputPort = NLCore.createPort();
    outputPort.portType = NLSpec.PortType.Output;
    outputPort.portSide = NLSpec.PortPositionSide.Right;
    outputPort.title = "Output";
    addPort(outputPort);
}
deletePort(port: Port)

Removes a port from the node.

Parameters:

Example:

var port = node.findPort(portUuid);
if (port) {
    node.deletePort(port);
}
findPort(portId: string): Port

Finds a port by its UUID.

Parameters:

Returns: Port object, or null if not found

Example:

var port = node.findPort(portUuid);
if (port) {
    console.log("Found port:", port.title);
}
findPortByPortSide(portSide: int): Port

Finds a port by its side position.

Parameters:

Returns: Port object, or null if not found

Example:

// Find left input port
var inputPort = node.findPortByPortSide(NLSpec.PortPositionSide.Left);

// Find right output port
var outputPort = node.findPortByPortSide(NLSpec.PortPositionSide.Right);

Port

Location: resources/Core/Port.qml
Inherits: QSObject
Purpose: Represents a connection point on a node. Ports allow nodes to send and receive data through links.

Where to Use

Port is used in the following contexts:

  1. Node Port Definition (addPorts() function):
    • Create ports when node is initialized
    • Define port types (Input, Output, InAndOut)
    • Set port positions (Top, Bottom, Left, Right)
    • Configure port appearance (title, color)
// examples/simpleNodeLink/NodeExample.qml
Node {
    Component.onCompleted: addPorts();

    function addPorts() {
        let inputPort = NLCore.createPort();
        inputPort.portType = NLSpec.PortType.Input;
        inputPort.portSide = NLSpec.PortPositionSide.Left;
        inputPort.title = "Input";
        addPort(inputPort);
    }
}
  1. Link Creation:
    • Get ports from nodes to create links
    • Validate port compatibility before linking
    • Access port UUIDs for link creation
// Create link between two ports
var sourcePort = sourceNode.findPortByPortSide(NLSpec.PortPositionSide.Right);
var targetPort = targetNode.findPortByPortSide(NLSpec.PortPositionSide.Left);
scene.createLink(sourcePort._qsUuid, targetPort._qsUuid);
  1. Data Flow:
    • Identify which ports are connected
    • Determine data flow direction
    • Access connected nodes through ports
// Find connected nodes through ports
Object.values(node.ports).forEach(function(port) {
    if (port.portType === NLSpec.PortType.Input) {
        // Find links connected to this input port
        var link = findLinkByInputPort(port._qsUuid);
        if (link) {
            var sourceNode = scene.findNode(link.inputPort._qsUuid);
            // Get data from source node
        }
    }
});
  1. Port Validation:
    • Check port types before linking
    • Validate port compatibility
    • Ensure proper data flow direction
// examples/calculator/resources/Core/CalculatorScene.qml
function canLinkNodes(portA: string, portB: string): bool {
    var portAObj = findPort(portA);
    var portBObj = findPort(portB);

    // Input port cannot be source
    if (portAObj.portType === NLSpec.PortType.Input)
        return false;
    // Output port cannot be destination
    if (portBObj.portType === NLSpec.PortType.Output)
        return false;

    return true;
}

Use Cases:

Properties

node: var

Reference to the parent node that owns this port.

Example:

var parentNode = port.node;
console.log("Port belongs to:", parentNode.title);
portType: int

Type of the port (Input, Output, or InAndOut).

Values:

Example:

port.portType = NLSpec.PortType.Input;
portSide: int

Position of the port on the node (Top, Bottom, Left, Right).

Values:

Example:

port.portSide = NLSpec.PortPositionSide.Left;  // Left side
title: string

Display name of the port.

Default: ""

Example:

port.title = "Input Value";
color: string

Color of the port (hex format).

Default: "white"

Example:

port.color = "#4A90E2";  // Blue
enable: bool

Whether the port is enabled (can be connected).

Default: true

Example:

port.enable = false;  // Disable port (grayed out)
_position: vector2d

Cached global position of the port in the UI. Set by the view layer.

Default: Qt.vector2d(-1, -1)

Note: This is an internal property used by the view for rendering. Do not set manually.

_measuredTitleWidth: real

Measured width of the port title for auto-sizing. Set by the view layer.

Default: -1

Note: Internal property used for layout calculations.

Usage Example

// Create and configure a port
function addPorts() {
    let inputPort = NLCore.createPort();
    inputPort.portType = NLSpec.PortType.Input;
    inputPort.portSide = NLSpec.PortPositionSide.Left;
    inputPort.title = "Input";
    inputPort.color = "#4A90E2";
    inputPort.enable = true;
    addPort(inputPort);

    let outputPort = NLCore.createPort();
    outputPort.portType = NLSpec.PortType.Output;
    outputPort.portSide = NLSpec.PortPositionSide.Right;
    outputPort.title = "Output";
    outputPort.color = "#7ED321";
    addPort(outputPort);
}

Location: resources/Core/Link.qml
Inherits: I_Node
Purpose: Represents a connection between two ports, allowing data to flow from one node to another.

Where to Use

Link is used in the following contexts:

  1. Link Creation (via Scene):
    • Create links between nodes programmatically
    • Handle link creation in custom scenes
    • Validate links before creation
// examples/calculator/resources/Core/CalculatorScene.qml
function linkNodes(portA: string, portB: string) {
    if (!canLinkNodes(portA, portB)) {
        return;
    }
    createLink(portA, portB);
}
  1. Data Flow Processing:
    • Iterate over links to process data flow
    • Access source and target nodes through links
    • Update node data based on connections
// Process data through links
Object.values(scene.links).forEach(function(link) {
    var sourceNode = scene.findNode(link.inputPort._qsUuid);
    var targetNode = scene.findNode(link.outputPort._qsUuid);

    // Transfer data from source to target
    targetNode.nodeData.input = sourceNode.nodeData.data;
});
  1. Link Management:
    • Remove links when nodes are deleted
    • Clone links when copying nodes
    • Validate link existence
// Remove link
scene.unlinkNodes(outputPortUuid, inputPortUuid);

// Check if link exists
var existingLink = Object.values(scene.links).find(function(link) {
    return link.inputPort._qsUuid === portA &&
           link.outputPort._qsUuid === portB;
});
  1. Link Visualization:
    • Configure link appearance (color, style, type)
    • Set link direction (unidirectional, bidirectional)
    • Customize link path (Bezier, L-shape, straight)
// Configure link appearance
link.guiConfig.color = "#4890e2";
link.guiConfig.linkType = NLSpec.LinkType.Bezier;
link.direction = NLSpec.LinkDirection.Unidirectional;

Use Cases:

Properties

inputPort: Port

The input port (destination) of the link. This is the port that receives data.

Note: Despite the name, inputPort in a Link actually refers to the output port (source) of the source node. This is a naming convention from the link's perspective.

Example:

var sourcePort = link.inputPort;  // Actually the output port of source node
console.log("Source port:", sourcePort.title);
outputPort: Port

The output port (source) of the link. This is the port that sends data.

Note: Despite the name, outputPort in a Link actually refers to the input port (destination) of the target node.

Example:

var targetPort = link.outputPort;  // Actually the input port of target node
console.log("Target port:", targetPort.title);

Important: The naming can be confusing. In practice:

controlPoints: var

Array of control points for the link's path (for Bezier curves, L-shapes, etc.).

Type: array<vector2d>

Example:

link.controlPoints = [
    Qt.vector2d(100, 100),
    Qt.vector2d(200, 150),
    Qt.vector2d(300, 100)
];
direction: int

Direction of the link (Unidirectional, Bidirectional, Nondirectional).

Values:

Default: NLSpec.LinkDirection.Unidirectional

Example:

link.direction = NLSpec.LinkDirection.Unidirectional;
guiConfig: LinkGUIConfig

GUI configuration for the link (color, style, width, etc.).

Example:

link.guiConfig.color = "#4890e2";
link.guiConfig.width = 2;
link.guiConfig.linkType = NLSpec.LinkType.Bezier;

Signals

Signal emitted when the link is being cloned.

Example:

Link {
    onCloneFrom: function(baseLink) {
        // Copy GUI config
        guiConfig.setProperties(baseLink.guiConfig);
    }
}

Usage Example

// Create a link between two ports
var link = scene.createLink(outputPortUuid, inputPortUuid);

// Configure link appearance
link.guiConfig.color = "#4890e2";
link.guiConfig.width = 2;
link.guiConfig.linkType = NLSpec.LinkType.Bezier;
link.direction = NLSpec.LinkDirection.Unidirectional;

Container

Location: resources/Core/Container.qml
Inherits: I_Node
Purpose: A container that can group multiple nodes and containers together visually.

Where to Use

Container is used in the following contexts:

  1. Node Organization:
    • Group related nodes together visually
    • Organize complex scenes into logical sections
    • Create hierarchical structures with nested containers
// Create container for grouping nodes
var container = scene.createContainer();
container.title = "Math Operations";
container.guiConfig.position = Qt.vector2d(100, 100);
container.guiConfig.width = 500;
container.guiConfig.height = 300;
scene.addContainer(container);
  1. Scene Management:
    • Add nodes to containers for organization
    • Find nodes within container bounds
    • Manage container hierarchy
// Find nodes inside container
var items = scene.findNodesInContainerItem({
    x: container.guiConfig.position.x,
    y: container.guiConfig.position.y,
    width: container.guiConfig.width,
    height: container.guiConfig.height
});

// Add nodes to container
items.forEach(function(node) {
    container.addNode(node);
});

(You can also use the findNodesInLasso function here for this.)

  1. UI Organization:
    • Visually group related functionality
    • Create collapsible sections
    • Organize large node graphs
// examples/simpleNodeLink/main.qml
// Container is registered as a node type
nodeRegistry.nodeTypes[1] = "Container";
nodeRegistry.nodeNames[1] = "Container";
  1. Nested Structures:
    • Create containers inside containers
    • Build hierarchical organization
    • Manage complex scene structures
// Add nested container
var parentContainer = scene.createContainer();
var childContainer = scene.createContainer();
parentContainer.addContainerInside(childContainer);

Use Cases:

Properties

title: string

Display name of the container.

Default: "Untitled"

Example:

container.title = "My Container Group";
nodes: var

Map of nodes inside this container.

Type: map<UUID, Node>

Example:

// Add node to container
container.addNode(node);

// Iterate over nodes in container
Object.values(container.nodes).forEach(function(node) {
    console.log("Node in container:", node.title);
});
containersInside: var

Map of containers inside this container (nested containers).

Type: map<UUID, Container>

Example:

// Add nested container
container.addContainerInside(nestedContainer);
guiConfig: ContainerGuiConfig

GUI configuration for the container (position, size, color, etc.).

Example:

container.guiConfig.position = Qt.vector2d(100, 100);
container.guiConfig.width = 500;
container.guiConfig.height = 300;
container.guiConfig.color = "#2d2d2d";

Functions

addNode(node: Node)

Adds a node to the container.

Parameters:

Example:

container.addNode(node);
removeNode(node: Node)

Removes a node from the container.

Parameters:

addContainerInside(container: Container)

Adds a nested container inside this container.

Parameters:

removeContainerInside(container: Container)

Removes a nested container from this container.

Usage Example

// Create container
var container = scene.createContainer();
container.title = "Math Operations";
container.guiConfig.position = Qt.vector2d(100, 100);
container.guiConfig.width = 400;
container.guiConfig.height = 300;

// Add nodes to container
container.addNode(addNode);
container.addNode(multiplyNode);
container.addNode(subtractNode);

// Add to scene
scene.addContainer(container);

Core Utilities

NLCore

Location: resources/Core/NLCore.qml
Type: Singleton (pragma Singleton)
Purpose: Factory functions for creating NodeLink objects and managing the default repository.

Where to Use

NLCore is used in the following contexts:

  1. Object Creation (Factory Functions):
    • Create nodes, ports, and links programmatically
    • Use in custom node definitions
    • Create objects for testing or batch operations
// examples/calculator/resources/Core/SourceNode.qml
function addPorts() {
    let port = NLCore.createPort();  // Create port
    port.portType = NLSpec.PortType.Output;
    addPort(port);
}

// examples/PerformanceAnalyzer - Batch node creation
var startNode = NLCore.createNode();
startNode.type = CSpecs.NodeType.StartNode;
  1. Repository Initialization (main.qml):
    • Initialize QtQuickStream repository
    • Set up default repository for serialization
    • Configure repository with required imports
// examples/simpleNodeLink/main.qml
Component.onCompleted: {
    NLCore.defaultRepo = NLCore.createDefaultRepo([
        "QtQuickStream",
        "NodeLink",
        "SimpleNodeLink"
    ]);
    NLCore.defaultRepo.initRootObject("Scene");
}
  1. Scene Creation:
    • Create new scenes programmatically
    • Initialize scene objects
    • Set up scene hierarchy
// Create scene
var newScene = NLCore.createScene();
newScene.title = "New Scene";
  1. Link Creation:
    • Create links between ports
    • Programmatically connect nodes
// Create link object
var link = NLCore.createLink();
link.inputPort = sourcePort;
link.outputPort = targetPort;

Use Cases:

Properties

defaultRepo: QSRepo

Default QtQuickStream repository used for serialization/deserialization.

Example:

// Initialize default repo
NLCore.defaultRepo = NLCore.createDefaultRepo(["QtQuickStream", "NodeLink", "MyApp"]);
NLCore.defaultRepo.initRootObject("Scene");

Functions

createScene(): Scene

Creates a new Scene object.

Returns: New Scene object

Example:

var scene = NLCore.createScene();
scene.title = "My Scene";
createNode(): Node

Creates a new Node object.

Returns: New Node object

Example:

var node = NLCore.createNode();
node.title = "My Node";
node.type = 0;
createPort(qsRepo: QSRepo = null): Port

Creates a new Port object.

Parameters:

Returns: New Port object

Example:

var port = NLCore.createPort();
port.portType = NLSpec.PortType.Input;
port.portSide = NLSpec.PortPositionSide.Left;
port.title = "Input";

Creates a new Link object.

Returns: New Link object

Example:

var link = NLCore.createLink();
link.inputPort = sourcePort;
link.outputPort = targetPort;

NLSpec

Location: resources/Core/NLSpec.qml
Type: Singleton (pragma Singleton)
Purpose: Contains enums and constants used throughout NodeLink.

Where to Use

NLSpec is used throughout NodeLink for type checking and configuration:

  1. Port Configuration:
    • Set port types (Input, Output, InAndOut)
    • Set port positions (Top, Bottom, Left, Right)
// examples/simpleNodeLink/NodeExample.qml
port.portType = NLSpec.PortType.InAndOut;
port.portSide = NLSpec.PortPositionSide.Left;
  1. Link Configuration:
    • Set link types (Bezier, LLine, Straight)
    • Set link directions (Unidirectional, Bidirectional)
    • Set link styles (Solid, Dash, Dot)
link.guiConfig.linkType = NLSpec.LinkType.Bezier;
link.direction = NLSpec.LinkDirection.Unidirectional;
link.guiConfig.linkStyle = NLSpec.LinkStyle.Solid;
  1. Object Type Checking:
    • Identify object types (Node, Link, Container)
    • Filter objects by type
    • Validate object types
// examples/calculator/resources/Core/CalculatorScene.qml
if (item.objectType === NLSpec.ObjectType.Node) {
    // Handle node
} else if (item.objectType === NLSpec.ObjectType.Link) {
    // Handle link
}
  1. Undo/Redo Control:
    • Block observers during undo/redo operations
    • Prevent re-recording of changes
// Block observers during undo
NLSpec.undo.blockObservers = true;
// Perform undo operation
undoStack.undo();
NLSpec.undo.blockObservers = false;
  1. Selection Tool Configuration:
    • Configure selection behavior
    • Set selection tool types
selectionTool.toolType = NLSpec.SelectionSpecificToolType.Node;

Use Cases:

Enums

ObjectType

Type of object in the scene.

Example:

if (item.objectType === NLSpec.ObjectType.Node) {
    console.log("This is a node");
}
PortPositionSide

Position of a port on a node.

Example:

port.portSide = NLSpec.PortPositionSide.Left;
PortType

Type of port (data flow direction).

Example:

port.portType = NLSpec.PortType.Input;
LinkType

Visual style of the link.

Example:

link.guiConfig.linkType = NLSpec.LinkType.Bezier;
LinkDirection

Direction of data flow in the link.

Example:

link.direction = NLSpec.LinkDirection.Unidirectional;
LinkStyle

Line style of the link.

Example:

link.guiConfig.linkStyle = NLSpec.LinkStyle.Dash;
SelectionSpecificToolType

Type of selection tool.

Properties

undo.blockObservers: bool

Flag to block observers during undo/redo operations.

Default: false

Example:

// Block observers during undo
NLSpec.undo.blockObservers = true;
// Perform undo operation
NLSpec.undo.blockObservers = false;

Supporting Components

NodeGuiConfig

Location: resources/Core/NodeGuiConfig.qml
Purpose: Stores GUI-related properties for nodes.

Where to Use

NodeGuiConfig is used in the following contexts:

  1. Node Definition (Custom Node .qml files):
    • Set initial node size and position
    • Configure node appearance (color, opacity)
    • Enable/disable auto-sizing
// examples/calculator/resources/Core/SourceNode.qml
Node {
    guiConfig.width: 150
    guiConfig.height: 100
    guiConfig.color: "#4A90E2"
    guiConfig.autoSize: true
}
  1. Node Creation (Programmatic):
    • Set node position when creating
    • Configure appearance
    • Set locked state
// examples/PerformanceAnalyzer
var node = NLCore.createNode();
node.guiConfig.position = Qt.vector2d(100, 200);
node.guiConfig.width = 200;
node.guiConfig.height = 150;
node.guiConfig.color = "#444444";
  1. Node Manipulation:
    • Move nodes programmatically
    • Resize nodes
    • Change appearance dynamically
// Move node
node.guiConfig.position = Qt.vector2d(newX, newY);

// Resize node
node.guiConfig.width = 300;
node.guiConfig.height = 200;

// Change color
node.guiConfig.color = "#FF5733";
  1. Auto-Sizing Configuration:
    • Enable automatic sizing based on content
    • Set minimum dimensions
    • Configure base content width
// examples/calculator/resources/Core/OperationNode.qml
guiConfig.autoSize: false
guiConfig.minWidth: 150
guiConfig.minHeight: 80
guiConfig.baseContentWidth: 120
  1. Node Locking:
    • Lock nodes to prevent movement
    • Protect important nodes from accidental changes
// Lock node
node.guiConfig.locked = true;

Use Cases:

Properties

position: vector2d

Position of the node in scene coordinates.

Default: Qt.vector2d(0.0, 0.0)

Example:

node.guiConfig.position = Qt.vector2d(100, 200);
width: int

Width of the node in pixels.

Default: NLStyle.node.width

Example:

node.guiConfig.width = 200;
height: int

Height of the node in pixels.

Default: NLStyle.node.height

Example:

node.guiConfig.height = 150;
color: string

Background color of the node (hex format).

Default: NLStyle.node.color

Example:

node.guiConfig.color = "#4A90E2";
opacity: real

Opacity of the node (0.0 to 1.0).

Default: NLStyle.node.opacity

Example:

node.guiConfig.opacity = 0.8;
locked: bool

Whether the node is locked (cannot be moved).

Default: false

Example:

node.guiConfig.locked = true;  // Lock node
autoSize: bool

Whether the node automatically sizes based on content and port titles.

Default: true

Example:

node.guiConfig.autoSize = false;  // Fixed size
minWidth: int

Minimum width when auto-sizing.

Default: 120

minHeight: int

Minimum height when auto-sizing.

Default: 80

baseContentWidth: int

Base content width for auto-sizing calculations.

Default: 100

description: string

Description text for the node.

Default: "<No Description>"

Example:

node.guiConfig.description = "This node performs addition";
logoUrl: string

URL or path to the node's icon/logo.

Default: ""

Example:

node.guiConfig.logoUrl = "qrc:/icons/add-icon.png";
colorIndex: int

Index for color selection (used with color palettes).

Default: -1


NodeData

Location: resources/Core/NodeData.qml
Inherits: I_NodeData
Purpose: Base class for storing node data.

Where to Use

NodeData is used in the following contexts:

  1. Node Definition (Custom Node .qml files):
    • Assign nodeData to nodes
    • Use base I_NodeData or create custom subclasses
    • Store node-specific data
// examples/calculator/resources/Core/SourceNode.qml
Node {
    nodeData: I_NodeData {}

    property real value: 0.0
    onValueChanged: {
        nodeData.data = value;
    }
}
  1. Custom NodeData Classes:
    • Create type-safe data storage
    • Define specific properties for node types
    • Implement data validation
// examples/calculator/resources/Core/OperationNodeData.qml
I_NodeData {
    property var input1: null
    property var input2: null
    property var output: null
}

// Usage in OperationNode
Node {
    nodeData: OperationNodeData {}

    function calculate() {
        if (nodeData.input1 && nodeData.input2) {
            nodeData.output = nodeData.input1 + nodeData.input2;
            nodeData.data = nodeData.output;
        }
    }
}
  1. Data Flow Processing (Scene):
    • Read data from source nodes
    • Write data to target nodes
    • Process data through node graph
// examples/calculator/resources/Core/CalculatorScene.qml
function updateData() {
    Object.values(scene.nodes).forEach(function(node) {
        // Get input from connected nodes
        Object.values(node.parents).forEach(function(parent) {
            node.nodeData.input = parent.nodeData.data;
        });
        // Process node
        if (node.calculate) {
            node.calculate();
        }
    });
}
  1. Data Storage:
    • Store calculation results
    • Store input values
    • Store intermediate processing data
// Store data
node.nodeData.data = "result";

// Store complex objects
node.nodeData.data = {
    value: 100,
    timestamp: Date.now()
};

Use Cases:

Properties

data: var

Generic data storage property. Can hold any type of data.

Default: null

Example:

// Store simple value
node.nodeData.data = 42;

// Store object
node.nodeData.data = {
    value: 100,
    name: "test"
};

// Store array
node.nodeData.data = [1, 2, 3];

Custom NodeData

You can create custom NodeData classes for type-safe data handling:

// MyNodeData.qml
import QtQuick
import NodeLink

I_NodeData {
    property var input1: null
    property var input2: null
    property var output: null
    property int operation: 0
}

// Usage in Node
Node {
    nodeData: MyNodeData {}

    function calculate() {
        if (nodeData.input1 && nodeData.input2) {
            nodeData.output = nodeData.input1 + nodeData.input2;
            nodeData.data = nodeData.output;
        }
    }
}

NLNodeRegistry

Location: resources/Core/NLNodeRegistry.qml
Purpose: Registry for managing all available node types in the application.

Where to Use

NLNodeRegistry is used in the following contexts:

  1. Main Application Initialization (main.qml):
    • Declare as a property in main Window
    • Register all node types in Component.onCompleted
    • Assign to scene after initialization
// examples/simpleNodeLink/main.qml
Window {
    property NLNodeRegistry nodeRegistry: NLNodeRegistry {
        _qsRepo: NLCore.defaultRepo
        imports: ["SimpleNodeLink", "NodeLink"]
        defaultNode: 0
    }

    Component.onCompleted: {
        // Register node types
        nodeRegistry.nodeTypes[0] = "NodeExample";
        nodeRegistry.nodeNames[0] = "NodeExample";
        nodeRegistry.nodeIcons[0] = "\ue4e2";
        nodeRegistry.nodeColors[0] = "#444";

        // Assign to scene
        scene.nodeRegistry = nodeRegistry;
    }
}
  1. Custom Scene Classes:
    • Define registry as part of scene
    • Register node types specific to the scene
    • Configure default node type
// examples/calculator/resources/Core/CalculatorScene.qml
I_Scene {
    nodeRegistry: NLNodeRegistry {
        _qsRepo: scene._qsRepo
        imports: ["Calculator"]
        defaultNode: CSpecs.NodeType.Source

        nodeTypes: [
            CSpecs.NodeType.Source = "SourceNode",
            CSpecs.NodeType.Operation = "OperationNode"
        ];
        // ... nodeNames, nodeIcons, nodeColors
    }
}
  1. Node Creation:
    • Scene uses registry to create nodes
    • Registry maps node type IDs to QML component names
    • Registry provides metadata (name, icon, color)
// Scene uses registry to create nodes
function createCustomizeNode(nodeType: int, xPos: real, yPos: real): string {
    var qsType = nodeRegistry.nodeTypes[nodeType];  // Get component name
    var nodeColor = nodeRegistry.nodeColors[nodeType];  // Get color
    // Create node using registry information
}
  1. UI Display:
    • Side menu uses registry to show available node types
    • Context menu uses registry for node creation
    • Node palette displays registered nodes
// Side menu iterates over registry
Object.keys(nodeRegistry.nodeTypes).forEach(function(typeId) {
    var nodeName = nodeRegistry.nodeNames[typeId];
    var nodeIcon = nodeRegistry.nodeIcons[typeId];
    // Display in menu
});

Use Cases:

Properties

imports: var

Array of QML module imports required to create nodes.

Type: array<string>

Example:

nodeRegistry.imports = ["MyApp", "NodeLink"];
nodeTypes: var

Map of node type IDs to QML component names.

Type: map<int, string>

Example:

nodeRegistry.nodeTypes[0] = "SourceNode";
nodeRegistry.nodeTypes[1] = "OperationNode";
nodeNames: var

Map of node type IDs to display names.

Type: map<int, string>

Example:

nodeRegistry.nodeNames[0] = "Source";
nodeRegistry.nodeNames[1] = "Operation";
nodeIcons: var

Map of node type IDs to icon characters (Font Awesome Unicode).

Type: map<int, string>

Example:

nodeRegistry.nodeIcons[0] = "\uf1c0";  // Font Awesome file icon
nodeRegistry.nodeIcons[1] = "\uf0ad";  // Font Awesome cog icon
nodeColors: var

Map of node type IDs to color strings (hex format).

Type: map<int, string>

Example:

nodeRegistry.nodeColors[0] = "#4A90E2";  // Blue
nodeRegistry.nodeColors[1] = "#F5A623";  // Orange
defaultNode: int

Default node type to create when no type is specified.

Default: 0

Example:

nodeRegistry.defaultNode = CSpecs.NodeType.Source;
nodeView: string

Path to custom node view component (optional).

Default: "NodeView.qml"

linkView: string

Path to custom link view component (optional).

Default: "LinkView.qml"

containerView: string

Path to custom container view component (optional).

Default: "ContainerView.qml"

Usage Example

property NLNodeRegistry nodeRegistry: NLNodeRegistry {
    _qsRepo: NLCore.defaultRepo
    imports: ["MyApp", "NodeLink"]
    defaultNode: 0
}

Component.onCompleted: {
    // Register Source Node
    nodeRegistry.nodeTypes[0] = "SourceNode";
    nodeRegistry.nodeNames[0] = "Source";
    nodeRegistry.nodeIcons[0] = "\uf1c0";
    nodeRegistry.nodeColors[0] = "#4A90E2";

    // Register Operation Node
    nodeRegistry.nodeTypes[1] = "OperationNode";
    nodeRegistry.nodeNames[1] = "Operation";
    nodeRegistry.nodeIcons[1] = "\uf0ad";
    nodeRegistry.nodeColors[1] = "#F5A623";

    // Assign to scene
    scene.nodeRegistry = nodeRegistry;
}

Common Usage Patterns

Creating and Adding a Node

// 1. Create node
var node = NLCore.createNode();
node.title = "My Node";
node.type = 0;
node.guiConfig.position = Qt.vector2d(100, 200);
node.guiConfig.width = 200;
node.guiConfig.height = 150;
node.guiConfig.color = "#4A90E2";

// 2. Add ports
let inputPort = NLCore.createPort();
inputPort.portType = NLSpec.PortType.Input;
inputPort.portSide = NLSpec.PortPositionSide.Left;
inputPort.title = "Input";
node.addPort(inputPort);

let outputPort = NLCore.createPort();
outputPort.portType = NLSpec.PortType.Output;
outputPort.portSide = NLSpec.PortPositionSide.Right;
outputPort.title = "Output";
node.addPort(outputPort);

// 3. Add to scene
scene.addNode(node, true);  // true = auto-select
// Get ports
var sourceNode = scene.nodes[sourceNodeUuid];
var targetNode = scene.nodes[targetNodeUuid];

var outputPort = sourceNode.findPortByPortSide(NLSpec.PortPositionSide.Right);
var inputPort = targetNode.findPortByPortSide(NLSpec.PortPositionSide.Left);

// Validate and create link
if (scene.canLinkNodes(outputPort._qsUuid, inputPort._qsUuid)) {
    scene.linkNodes(outputPort._qsUuid, inputPort._qsUuid);
}

Iterating Over Scene Objects

// Iterate over all nodes
Object.values(scene.nodes).forEach(function(node) {
    console.log("Node:", node.title, "Type:", node.type);

    // Iterate over node's ports
    Object.values(node.ports).forEach(function(port) {
        console.log("  Port:", port.title, "Type:", port.portType);
    });

    // Check children
    if (Object.keys(node.children).length > 0) {
        console.log("  Has", Object.keys(node.children).length, "children");
    }
});

// Iterate over all links
Object.values(scene.links).forEach(function(link) {
    var sourceNode = scene.findNode(link.inputPort._qsUuid);
    var targetNode = scene.findNode(link.outputPort._qsUuid);
    console.log("Link:", sourceNode.title, "->", targetNode.title);
});

Handling Scene Events

Connections {
    target: scene

    function onNodeAdded(node) {
        console.log("Node added:", node.title);
        // Update UI, validate, etc.
    }

    function onLinkAdded(link) {
        console.log("Link created");
        // Update data flow, recalculate, etc.
        updateData();
    }

    function onNodeRemoved(node) {
        console.log("Node removed:", node.title);
        // Cleanup, update UI, etc.
    }
}

Type Definitions

UUID

String identifier used throughout NodeLink for uniquely identifying objects.

Format: Generated by QtQuickStream (QSObject)

Example: "550e8400-e29b-41d4-a716-446655440000"

vector2d

Qt vector2d type representing 2D coordinates.

Example: Qt.vector2d(100, 200)

Notes

  1. UUIDs: All objects in NodeLink have a _qsUuid property (from QtQuickStream) that uniquely identifies them.

  2. Repository: NodeLink uses QtQuickStream for serialization. Objects must be created with a repository (_qsRepo) to be serializable.

  3. Signals: Most operations emit signals that can be connected to for UI updates and data flow management.

  4. Undo/Redo: NodeLink has built-in undo/redo support. Operations are automatically recorded when not in replay mode.

  5. Thread Safety: NodeLink components are not thread-safe. All operations should be performed on the main UI thread.

C++ Classes

Overview

This document provides a comprehensive reference for all C++ classes available in the NodeLink framework. These classes are registered with the QML engine and can be used directly from QML code. They provide performance-critical operations, utility functions, and advanced features that benefit from C++ implementation.

ObjectCreator

Location: include/NodeLink/Core/objectcreator.h
Source: Source/Core/objectcreator.cpp
QML Name: ObjectCreator
Type: QML Singleton
Inherits: QObject
Purpose: Factory class for creating QML components programmatically with caching for improved performance.

Where to Use

ObjectCreator is used in the following contexts:

  1. Dynamic Component Creation (View Layer):
    • Create node views dynamically from QML components
    • Create link views dynamically
    • Create container views dynamically
    • Batch creation of multiple items for performance
// resources/View/I_NodesRect.qml
// Create multiple node views at once
var result = ObjectCreator.createItems(
    "node",           // Property name
    nodeArray,        // Array of node objects
    root,             // Parent item
    nodeViewUrl,      // Component URL
    baseProperties    // Base properties for all items
);

var createdItems = result.items;
var needsPropertySet = result.needsPropertySet;
  1. Single Item Creation:
    • Create individual QML items programmatically
    • Set initial properties during creation
// Create a single item
var result = ObjectCreator.createItem(
    parentItem,       // Parent QQuickItem
    componentUrl,     // URL to QML component
    properties        // Initial properties map
);

var item = result.item;
var needsPropertySet = result.needsPropertySet;
  1. Performance Optimization:
    • Component caching for faster subsequent creations
    • Batch operations for creating multiple items efficiently
    • Asynchronous component loading

Use Cases:
- Node View Creation: Dynamically create NodeView components for each node in the scene
- Link View Creation: Create LinkView components for connections
- Container View Creation: Create ContainerView components for containers
- Batch Operations: Create multiple views at once for better performance

Public Methods

createItem(parentItem: QQuickItem, componentUrl: string, properties: QVariantMap): QVariantMap

Creates a single QML item from a component URL.

Parameters:
- parentItem: Parent QQuickItem for the created item
- componentUrl: URL string to the QML component file
- properties: QVariantMap of initial properties to set on the created item

Returns: QVariantMap with:
- item: QVariant containing the created QQuickItem (or null if creation failed)
- needsPropertySet: Boolean indicating if properties need to be set manually (Qt < 6.2.4)

Example:

var result = ObjectCreator.createItem(
    parentItem,
    "qrc:/MyApp/MyComponent.qml",
    {
        "property1": value1,
        "property2": value2
    }
);

if (result.item) {
    var createdItem = result.item;
    // Use created item
}

Note: For Qt 6.2.4+, properties are set automatically during creation. For older versions, you may need to set properties manually if needsPropertySet is true.

createItems(name: string, itemArray: QVariantList, parentItem: QQuickItem, componentUrl: string, baseProperties: QVariantMap): QVariantMap

Creates multiple QML items from the same component in a batch operation.

Parameters:
- name: Property name to set for each item in the array
- itemArray: QVariantList of objects to create items for
- parentItem: Parent QQuickItem for all created items
- componentUrl: URL string to the QML component file
- baseProperties: QVariantMap of base properties applied to all items

Returns: QVariantMap with:
- items: QVariantList of created QQuickItem objects
- needsPropertySet: Boolean indicating if properties need to be set manually

Example:

// Create node views for multiple nodes
var nodeArray = Object.values(scene.nodes);
var result = ObjectCreator.createItems(
    "node",                    // Property name
    nodeArray,                 // Array of node objects
    nodesRect,                 // Parent item
    "qrc:/NodeLink/NodeView.qml",  // Component URL
    {                          // Base properties
        "scene": scene,
        "sceneSession": sceneSession
    }
);

var createdViews = result.items;
createdViews.forEach(function(view) {
    // Process each created view
});

Performance: This method is optimized for batch creation and includes component caching. Use this instead of multiple createItem() calls for better performance.

Private Methods

getOrCreateComponent(componentUrl: string): QQmlComponent*

Internal method that caches components for reuse. Components are cached in a QHash for fast subsequent access.

Note: This is a private method and cannot be called directly from QML.

Implementation Details

HashCompareStringCPP

Location: include/NodeLink/Core/HashCompareStringCPP.h
Source: Source/Core/HashCompareStringCPP.cpp
QML Name: HashCompareString
Type: QML Singleton
Inherits: QObject
Purpose: Provides efficient string comparison using MD5 hashing for comparing UUID strings and other identifiers.

Where to Use

HashCompareStringCPP is used in the following contexts:

  1. UUID Comparison (Scene Link Validation):
    • Compare port UUIDs when checking for duplicate links
    • Compare node UUIDs when validating connections
    • Efficient comparison of string identifiers
// resources/Core/Scene.qml
function canLinkNodes(portA: string, portB: string): bool {
    // Check for duplicate links
    var sameLinks = Object.values(links).filter(link =>
        HashCompareString.compareStringModels(link.inputPort._qsUuid, portA) &&
        HashCompareString.compareStringModels(link.outputPort._qsUuid, portB));

    if (sameLinks.length > 0)
        return false;

    // Compare node UUIDs
    var nodeA = findNodeId(portA);
    var nodeB = findNodeId(portB);
    if (HashCompareString.compareStringModels(nodeA, nodeB))
        return false;  // Same node
}
  1. Link Validation:
    • Check if a link already exists between two ports
    • Validate port connections
    • Prevent duplicate connections
// Check for existing link
var existingLink = Object.values(links).find(link =>
    HashCompareString.compareStringModels(link.inputPort._qsUuid, portA) &&
    HashCompareString.compareStringModels(link.outputPort._qsUuid, portB)
);
  1. Performance-Critical Comparisons:
    • When comparing many strings (e.g., iterating over all links)
    • UUID comparisons in validation loops
    • String identity checks

Use Cases:
- Calculator Example: Validates links and prevents duplicates
- Logic Circuit Example: Validates gate connections
- VisionLink Example: Validates image processing pipeline connections
- Chatbot Example: Validates conversation flow connections
- All Scene Implementations: Link validation and UUID comparison

Public Methods

compareStringModels(strModelFirst: string, strModelSecond: string): bool

Compares two strings using MD5 hash comparison for efficient equality checking.

Parameters:
- strModelFirst: First string to compare
- strModelSecond: Second string to compare

Returns: true if strings are equal (same MD5 hash), false otherwise

Example:

// Compare UUIDs
var port1Uuid = port1._qsUuid;
var port2Uuid = port2._qsUuid;

if (HashCompareString.compareStringModels(port1Uuid, port2Uuid)) {
    console.log("Ports have the same UUID");
}

// Check for duplicate link
var isDuplicate = Object.values(links).some(function(link) {
    return HashCompareString.compareStringModels(link.inputPort._qsUuid, portA) &&
           HashCompareString.compareStringModels(link.outputPort._qsUuid, portB);
});

Performance: Uses MD5 hashing for efficient comparison, especially useful when comparing many strings in loops.

Algorithm:
1. Computes MD5 hash of both strings
2. Compares the hash values
3. Returns true if hashes match (strings are equal)

BackgroundGridsCPP

Location: include/NodeLink/View/BackgroundGridsCPP.h
Source: Source/View/BackgroundGridsCPP.cpp
QML Name: BackgroundGridsCPP
Type: QML Element
Inherits: QQuickItem
Purpose: High-performance background grid rendering using Qt's Scene Graph (QSG) for GPU-accelerated rendering.

Where to Use

BackgroundGridsCPP is used in the following contexts:

  1. Scene Background (View Layer):
    • Render background grid in the scene view
    • Provide visual reference for node positioning
    • Support snap-to-grid functionality
// resources/View/SceneViewBackground.qml
BackgroundGridsCPP {
    anchors.fill: parent
    spacing: NLStyle.backgroundGrid.spacing
}
  1. Grid Visualization:
    • Display grid points or lines in the scene
    • Provide visual feedback for alignment
    • Support zoom-aware grid rendering
// Custom grid configuration
BackgroundGridsCPP {
    id: backgroundGrid
    anchors.fill: parent
    spacing: 20  // Grid spacing in pixels

    onSpacingChanged: {
        console.log("Grid spacing changed to:", spacing);
    }
}
  1. Performance-Critical Rendering:
    • When rendering large scenes with many grid points
    • When grid needs to update frequently
    • When smooth scrolling/zooming is required

Use Cases:
- All NodeLink Views: Background grid in scene canvas
- Zoom Operations: Grid updates when zooming
- Snap-to-Grid: Visual feedback for grid snapping
- Large Scenes: Efficient rendering of grid for large canvases

Properties

spacing: int

Grid spacing in pixels. Determines the distance between grid points.

Default: 0 (grid disabled)

Access: Read/Write

Signal: spacingChanged() emitted when spacing changes

Example:

BackgroundGridsCPP {
    spacing: 20  // 20 pixels between grid points
}

Note: Setting spacing to 0 or negative value disables grid rendering.

Signals

spacingChanged()

Emitted when the spacing property changes.

Example:

BackgroundGridsCPP {
    onSpacingChanged: {
        console.log("Grid spacing:", spacing);
    }
}

Implementation Details

Rendering Algorithm:
1. Calculates number of grid points based on spacing and item size
2. Creates triangle geometry for each grid point (2x2 pixel squares)
3. Uses QSGFlatColorMaterial for rendering
4. Updates geometry only when spacing or size changes

NLUtilsCPP

Location: Utils/NLUtilsCPP.h
Source: Utils/NLUtilsCPP.cpp
QML Name: NLUtilsCPP
Type: QML Element
Inherits: QObject
Purpose: Utility functions for common operations like image conversion and key sequence formatting.

Where to Use

NLUtilsCPP is used in the following contexts:

  1. Image Processing:
    • Convert image files to base64 strings
    • Convert image URLs to data strings
    • Prepare images for QML Image components
// Convert image file to base64 string
var imageString = NLUtilsCPP.imageURLToImageString("file:///path/to/image.png");
// Use in Image component
Image {
    source: "data:image/png;base64," + imageString
}
  1. Key Sequence Formatting:
    • Format keyboard shortcuts for display
    • Convert QKeySequence::StandardKey to readable string
    • Display shortcuts in UI
// Format key sequence for display
var keyString = NLUtilsCPP.keySequenceToString(QKeySequence.Copy);
// Result: "Ctrl + C" (platform-specific)

Use Cases:
- Image Handling: Converting images for display in QML
- UI Display: Formatting keyboard shortcuts in menus and tooltips
- File Operations: Reading and converting image files

Public Methods

imageURLToImageString(url: string): string

Reads an image file and converts it to a base64-encoded string.

Parameters:
- url: File path or URL to the image file (supports file:// URLs)

Returns: Base64-encoded string of the image data, or empty string on failure

Example:

// Load image from file
var imagePath = fileDialog.selectedFile;
var base64String = NLUtilsCPP.imageURLToImageString(imagePath);

// Use in Image component
Image {
    source: "data:image/png;base64," + base64String
}

Error Handling: Returns empty string if file cannot be opened or read.

keySequenceToString(keySequence: int): string

Converts a QKeySequence::StandardKey integer to a formatted string representation.

Parameters:
- keySequence: Integer value from QKeySequence::StandardKey enum

Returns: Formatted string with platform-specific key sequence (e.g., "Ctrl + C", "Cmd + C")

Example:

// Format standard key sequences
var copyKey = NLUtilsCPP.keySequenceToString(QKeySequence.Copy);
// Result: "Ctrl + C" (Windows/Linux) or "Cmd + C" (macOS)

var pasteKey = NLUtilsCPP.keySequenceToString(QKeySequence.Paste);
// Result: "Ctrl + V" (Windows/Linux) or "Cmd + V" (macOS)

// Display in UI
Text {
    text: "Copy: " + copyKey
}

Format: Keys are separated by " + " (space, plus, space) for readability.

Platform Support: Returns platform-specific key representations (Ctrl on Windows/Linux, Cmd on macOS).

Common Usage Patterns

Creating Multiple Node Views

// Batch create node views for better performance
function onNodesAdded(nodeArray) {
    var result = ObjectCreator.createItems(
        "node",
        nodeArray,
        nodesRect,
        "qrc:/NodeLink/NodeView.qml",
        {
            "scene": scene,
            "sceneSession": sceneSession
        }
    );

    // Handle property setting for older Qt versions
    if (result.needsPropertySet) {
        for (var i = 0; i < result.items.length; i++) {
            result.items[i].node = nodeArray[i];
        }
    }
}
// Check if link already exists
function linkExists(portA, portB) {
    return Object.values(links).some(function(link) {
        return HashCompareString.compareStringModels(link.inputPort._qsUuid, portA) &&
               HashCompareString.compareStringModels(link.outputPort._qsUuid, portB);
    });
}

Custom Grid Configuration

BackgroundGridsCPP {
    id: grid
    anchors.fill: parent
    spacing: 25

    onSpacingChanged: {
        console.log("Grid spacing:", spacing);
    }
}

// Change grid spacing dynamically
function setGridSpacing(newSpacing) {
    grid.spacing = newSpacing;
}

Image Loading and Display

// Load and display image
function loadImage(imagePath) {
    var base64 = NLUtilsCPP.imageURLToImageString(imagePath);
    if (base64) {
        imageComponent.source = "data:image/png;base64," + base64;
    } else {
        console.error("Failed to load image:", imagePath);
    }
}

Performance Considerations

ObjectCreator

HashCompareStringCPP

BackgroundGridsCPP

NLUtilsCPP

Thread Safety

Important: All C++ classes in NodeLink are not thread-safe. All operations should be performed on the main UI thread (QML thread). Attempting to use these classes from background threads will result in undefined behavior.

Configuration Options

Overview

This document provides a comprehensive reference for all configuration options available in the NodeLink framework. These options allow you to customize the appearance, behavior, and styling of nodes, links, containers, and scenes.

NLStyle - Global Style Settings

Location: resources/View/NLStyle.qml
Type: Singleton (pragma Singleton)
Purpose: Global style settings and theme configuration for the entire NodeLink application.

Where to Use

NLStyle is used throughout NodeLink for consistent styling:

  1. View Components: All view components reference NLStyle for colors, sizes, fonts
  2. Default Values: GUI config objects use NLStyle for default values
  3. Theme Customization: Modify NLStyle to change the entire application theme

Example:

// Accessing style properties
Rectangle {
    color: NLStyle.primaryBackgroundColor
    border.color: NLStyle.primaryBorderColor
    Text {
        color: NLStyle.primaryTextColor
        font.family: NLStyle.fontType.roboto
    }
}

Color Properties

primaryColor: string

Primary accent color used throughout the application.

Default: "#4890e2" (Blue)

Example:

Rectangle {
    color: NLStyle.primaryColor
}

primaryTextColor: string

Primary text color for readable text on dark backgrounds.

Default: "white"

Example:

Text {
    color: NLStyle.primaryTextColor
}

primaryBackgroundColor: string

Primary background color for the application.

Default: "#1e1e1e" (Dark gray)

Example:

Window {
    color: NLStyle.primaryBackgroundColor
}

primaryBorderColor: string

Primary border color for UI elements.

Default: "#363636" (Medium gray)

Example:

Rectangle {
    border.color: NLStyle.primaryBorderColor
    border.width: 1
}

backgroundGray: string

Secondary background color for panels and containers.

Default: "#2A2A2A"

Example:

Rectangle {
    color: NLStyle.backgroundGray
}

primaryRed: string

Primary red color for errors and warnings.

Default: "#8b0000" (Dark red)

Example:

Rectangle {
    color: NLStyle.primaryRed  // Error indicator
}

Node Style Properties

node: QtObject

Object containing default node styling properties.

Properties:
- width: real - Default node width (default: 200)
- height: real - Default node height (default: 150)
- opacity: real - Default node opacity (default: 1.0)
- defaultOpacity: real - Opacity for unselected nodes (default: 0.8)
- selectedOpacity: real - Opacity for selected nodes (default: 0.8)
- overviewFontSize: int - Font size in overview mode (default: 60)
- borderWidth: int - Node border width (default: 2)
- fontSizeTitle: int - Font size for node title (default: 10)
- fontSize: int - Font size for node content (default: 9)
- color: string - Default node color (default: "pink")
- borderLockColor: string - Border color for locked nodes (default: "gray")

Example:

Node {
    guiConfig.width: NLStyle.node.width
    guiConfig.height: NLStyle.node.height
    guiConfig.color: NLStyle.node.color
}

Port View Properties

portView: QtObject

Object containing port view styling properties.

Properties:
- size: int - Port size in pixels (default: 18)
- borderSize: int - Port border width (default: 2)
- fontSize: double - Port label font size (default: 10)

Example:

Rectangle {
    width: NLStyle.portView.size
    height: NLStyle.portView.size
    border.width: NLStyle.portView.borderSize
}

Scene Properties

scene: QtObject

Object containing scene dimension properties.

Properties:
- maximumContentWidth: real - Maximum scene width (default: 12000)
- maximumContentHeight: real - Maximum scene height (default: 12000)
- defaultContentWidth: real - Default scene width (default: 4000)
- defaultContentHeight: real - Default scene height (default: 4000)
- defaultContentX: real - Default scene X position (default: 1500)
- defaultContentY: real - Default scene Y position (default: 1500)

Example:

Flickable {
    contentWidth: NLStyle.scene.defaultContentWidth
    contentHeight: NLStyle.scene.defaultContentHeight
}

Background Grid Properties

backgroundGrid: QtObject

Object containing background grid styling properties.

Properties:
- spacing: int - Grid spacing in pixels (default: 20)
- opacity: double - Grid opacity (default: 0.65)

Example:

BackgroundGridsCPP {
    spacing: NLStyle.backgroundGrid.spacing
    opacity: NLStyle.backgroundGrid.opacity
}

Radius Properties

radiusAmount: QtObject

Object containing border radius values for different UI elements.

Properties:
- nodeOverview: double - Radius for node overview (default: 20)
- blockerNode: double - Radius for blocker nodes (default: 10)
- confirmPopup: double - Radius for confirmation popups (default: 10)
- nodeView: double - Radius for node views (default: 10)
- linkView: double - Radius for link views (default: 5)
- itemButton: double - Radius for item buttons (default: 5)
- toolTip: double - Radius for tooltips (default: 4)

Example:

Rectangle {
    radius: NLStyle.radiusAmount.nodeView
}

Font Properties

fontType: QtObject

Object containing font family names.

Properties:
- roboto: string - Roboto font family (default: "Roboto")
- font6Pro: string - Font Awesome 6 Pro font family (default: "Font Awesome 6 Pro")

Example:

Text {
    font.family: NLStyle.fontType.roboto
    text: "Hello"
}

Text {
    font.family: NLStyle.fontType.font6Pro
    text: "\uf04b"  // Font Awesome icon
}

Icon Arrays

nodeIcons: var

Array of Font Awesome icon characters for different node types.

Default: ["\ue4e2", "\uf04b", "\uf54b", "\ue57f", "\uf2db", "\uf04b"]

Example:

Text {
    font.family: NLStyle.fontType.font6Pro
    text: NLStyle.nodeIcons[0]  // General node icon
}

nodeColors: var

Array of color strings for different node types.

Default: ["#444", "#333", "#3D9798", "#625192", "#9D9E57", "#333"]

Example:

Rectangle {
    color: NLStyle.nodeColors[0]  // General node color
}

linkDirectionIcon: var

Array of Font Awesome icons for link directions.

Default: ["\ue404", "\ue4c1", "\uf07e"] (Nondirectional, Unidirectional, Bidirectional)

linkStyleIcon: var

Array of Font Awesome icons for link styles.

Default: ["\uf111", "\uf1ce", "\ue105"] (Solid, Dash, Dot)

linkTypeIcon: var

Array of icons/characters for link types.

Default: ["\uf899", "L", "/"] (Bezier, LLine, Straight)

Global Settings

snapEnabled: bool

Global flag to enable/disable snap-to-grid functionality.

Default: false

Access: Read/Write

Example:

// Enable snap to grid
NLStyle.snapEnabled = true;

// Check if snap is enabled
if (NLStyle.snapEnabled) {
    // Snap node to grid
    node.guiConfig.position = snapToGrid(position);
}

NodeGuiConfig - Node Configuration

Location: resources/Core/NodeGuiConfig.qml
Type: QSObject
Purpose: Stores GUI-related properties for individual nodes.

Where to Use

NodeGuiConfig is accessed through node.guiConfig:

// In node definition
Node {
    guiConfig.width: 200
    guiConfig.height: 150
    guiConfig.color: "#4A90E2"
}

// Programmatically
node.guiConfig.position = Qt.vector2d(100, 200);
node.guiConfig.width = 300;

Properties

description: string

Description text for the node.

Default: "<No Description>"

Example:

node.guiConfig.description = "This node performs addition";

logoUrl: string

URL or path to the node's icon/logo image.

Default: ""

Example:

node.guiConfig.logoUrl = "qrc:/icons/add-icon.png";

position: vector2d

Position of the node in scene coordinates.

Default: Qt.vector2d(0.0, 0.0)

Example:

node.guiConfig.position = Qt.vector2d(100, 200);

width: int

Width of the node in pixels.

Default: NLStyle.node.width (200)

Example:

node.guiConfig.width = 250;

height: int

Height of the node in pixels.

Default: NLStyle.node.height (150)

Example:

node.guiConfig.height = 180;

color: string

Background color of the node (hex format).

Default: NLStyle.node.color ("pink")

Example:

node.guiConfig.color = "#4A90E2";  // Blue

colorIndex: int

Index for color selection (used with color palettes).

Default: -1 (no color index)

Example:

node.guiConfig.colorIndex = 3;  // Use color from palette index 3

opacity: real

Opacity of the node (0.0 to 1.0).

Default: NLStyle.node.opacity (1.0)

Example:

node.guiConfig.opacity = 0.9;  // 90% opacity

locked: bool

Whether the node is locked (cannot be moved).

Default: false

Example:

node.guiConfig.locked = true;  // Lock node

autoSize: bool

Whether the node automatically sizes based on content and port titles.

Default: true

Example:

node.guiConfig.autoSize = false;  // Fixed size

minWidth: int

Minimum width when auto-sizing is enabled.

Default: 120

Example:

node.guiConfig.minWidth = 150;

minHeight: int

Minimum height when auto-sizing is enabled.

Default: 80

Example:

node.guiConfig.minHeight = 100;

baseContentWidth: int

Base content width for auto-sizing calculations (space for operation/image in the middle).

Default: 100

Example:

node.guiConfig.baseContentWidth = 120;

Usage Example

// Create and configure a node
var node = NLCore.createNode();
node.title = "My Node";
node.guiConfig.position = Qt.vector2d(100, 200);
node.guiConfig.width = 200;
node.guiConfig.height = 150;
node.guiConfig.color = "#4A90E2";
node.guiConfig.opacity = 0.9;
node.guiConfig.autoSize = true;
node.guiConfig.minWidth = 150;
node.guiConfig.minHeight = 100;
node.guiConfig.locked = false;

ContainerGuiConfig - Container Configuration

Location: resources/Core/ContainerGuiConfig.qml
Type: QSObject
Purpose: Stores GUI-related properties for containers.

Where to Use

ContainerGuiConfig is accessed through container.guiConfig:

// Configure container appearance
container.guiConfig.width = 500;
container.guiConfig.height = 300;
container.guiConfig.color = "#2d2d2d";

Properties

zoomFactor: real

Zoom factor for the container (used for nested zooming).

Default: 1.0

Example:

container.guiConfig.zoomFactor = 1.5;  // 150% zoom

width: real

Width of the container in pixels.

Default: 200

Example:

container.guiConfig.width = 500;

height: real

Height of the container in pixels.

Default: 200

Example:

container.guiConfig.height = 300;

color: string

Background color of the container (hex format).

Default: NLStyle.primaryColor ("#4890e2")

Example:

container.guiConfig.color = "#2d2d2d";  // Dark gray

colorIndex: int

Index for color selection (used with color palettes).

Default: -1 (no color index)

Example:

container.guiConfig.colorIndex = 1;

position: vector2d

Position of the container in scene coordinates.

Default: Qt.vector2d(0.0, 0.0)

Example:

container.guiConfig.position = Qt.vector2d(100, 100);

locked: bool

Whether the container is locked (cannot be moved).

Default: false

Example:

container.guiConfig.locked = true;

containerTextHeight: int

Height of the container title text area.

Default: 35

Example:

container.guiConfig.containerTextHeight = 40;

Usage Example

// Create and configure a container
var container = scene.createContainer();
container.title = "My Container";
container.guiConfig.position = Qt.vector2d(100, 100);
container.guiConfig.width = 500;
container.guiConfig.height = 300;
container.guiConfig.color = "#2d2d2d";
container.guiConfig.locked = false;

SceneGuiConfig - Scene Configuration

Location: resources/Core/SceneGuiConfig.qml
Type: QSObject
Purpose: Stores GUI-related properties for the scene view.

Where to Use

SceneGuiConfig is accessed through scene.sceneGuiConfig:

// Configure scene view
scene.sceneGuiConfig.contentWidth = 5000;
scene.sceneGuiConfig.contentHeight = 5000;
scene.sceneGuiConfig.zoomFactor = 1.0;

Properties

zoomFactor: real

Current zoom factor of the scene view.

Default: 1.0

Example:

scene.sceneGuiConfig.zoomFactor = 1.5;  // 150% zoom

contentWidth: real

Width of the scrollable content area.

Default: NLStyle.scene.defaultContentWidth (4000)

Example:

scene.sceneGuiConfig.contentWidth = 5000;

contentHeight: real

Height of the scrollable content area.

Default: NLStyle.scene.defaultContentHeight (4000)

Example:

scene.sceneGuiConfig.contentHeight = 5000;

contentX: real

Horizontal scroll position of the scene view.

Default: NLStyle.scene.defaultContentX (1500)

Example:

scene.sceneGuiConfig.contentX = 2000;

contentY: real

Vertical scroll position of the scene view.

Default: NLStyle.scene.defaultContentY (1500)

Example:

scene.sceneGuiConfig.contentY = 2000;

sceneViewWidth: real

Width of the visible scene view area.

Default: undefined (set by view)

Example:

var viewWidth = scene.sceneGuiConfig.sceneViewWidth;

sceneViewHeight: real

Height of the visible scene view area.

Default: undefined (set by view)

Example:

var viewHeight = scene.sceneGuiConfig.sceneViewHeight;

_mousePosition: vector2d

Internal property storing mouse position in scene coordinates (used for paste operations).

Default: Qt.vector2d(0.0, 0.0)

Note: This is an internal property and typically not set directly.

Usage Example

// Configure scene view
scene.sceneGuiConfig.contentWidth = 6000;
scene.sceneGuiConfig.contentHeight = 6000;
scene.sceneGuiConfig.contentX = 2000;
scene.sceneGuiConfig.contentY = 2000;
scene.sceneGuiConfig.zoomFactor = 1.0;

NLSpec - Enums and Constants

Location: resources/Core/NLSpec.qml
Type: Singleton (pragma Singleton)
Purpose: Contains enums and constants used throughout NodeLink.

Where to Use

NLSpec is used for type checking and configuration throughout NodeLink:

// Port configuration
port.portType = NLSpec.PortType.Input;
port.portSide = NLSpec.PortPositionSide.Left;

// Link configuration
link.guiConfig.type = NLSpec.LinkType.Bezier;
link.direction = NLSpec.LinkDirection.Unidirectional;

Enums

ObjectType

Type of object in the scene.

Values:
- Node (0) - Node object
- Link (1) - Link object
- Container (2) - Container object
- Unknown (99) - Unknown object type

Example:

if (item.objectType === NLSpec.ObjectType.Node) {
    console.log("This is a node");
}

SelectionSpecificToolType

Type of selection tool.

Values:
- Node (0) - Select single node
- Link (1) - Select single link
- Any (2) - Select single object of any type
- All (3) - Select multiple objects of any type
- Unknown (99) - Unknown tool type

Example:

selectionTool.toolType = NLSpec.SelectionSpecificToolType.Node;

SelectionType

Type of selection model.

Values:
- Rectangle (0) - Rectangle selection model
- Lasso (1) - Lasso selection model
- Unknown (99) - Unknown selection model

Example:

selectionTool.selectionType = NLSpec.SelectionType.Rectangle;

PortPositionSide

Position of a port on a node.

Values:
- Top (0) - Top side
- Bottom (1) - Bottom side
- Left (2) - Left side
- Right (3) - Right side
- Unknown (99) - Unknown position

Example:

port.portSide = NLSpec.PortPositionSide.Left;

PortType

Type of port (data flow direction).

Values:
- Input (0) - Can only receive connections
- Output (1) - Can only send connections
- InAndOut (2) - Can both send and receive

Example:

port.portType = NLSpec.PortType.Input;

LinkType

Visual style of the link.

Values:
- Bezier (0) - Bezier curve
- LLine (1) - L-shaped line with one control point
- Straight (2) - Straight line
- Unknown (99) - Unknown link type

Example:

link.guiConfig.type = NLSpec.LinkType.Bezier;

LinkDirection

Direction of data flow in the link.

Values:
- Nondirectional (0) - No specific direction
- Unidirectional (1) - One-way data flow (default)
- Bidirectional (2) - Two-way data flow

Example:

link.direction = NLSpec.LinkDirection.Unidirectional;

LinkStyle

Line style of the link.

Values:
- Solid (0) - Solid line
- Dash (1) - Dashed line
- Dot (2) - Dotted line

Example:

link.guiConfig.style = NLSpec.LinkStyle.Dash;

NodeType

Node type identifiers.

Values:
- CustomNode (98) - Custom node type
- Unknown (99) - Unknown node type

Note: Most node types are defined in application-specific spec files (e.g., CSpecs.qml).

Properties

undo.blockObservers: bool

Flag to block observers during undo/redo operations.

Default: false

Access: Read/Write

Example:

// Block observers during undo
NLSpec.undo.blockObservers = true;
// Perform undo operation
undoStack.undo();
// Unblock observers
NLSpec.undo.blockObservers = false;

Common Configuration Patterns

Customizing Node Appearance

// In node definition
Node {
    guiConfig.width: 250
    guiConfig.height: 180
    guiConfig.color: "#4A90E2"
    guiConfig.opacity: 0.9
    guiConfig.autoSize: true
    guiConfig.minWidth: 150
    guiConfig.minHeight: 100
}
// Configure link
link.guiConfig.color = "#4890e2";
link.guiConfig.style = NLSpec.LinkStyle.Solid;
link.guiConfig.type = NLSpec.LinkType.Bezier;
link.direction = NLSpec.LinkDirection.Unidirectional;

Customizing Container Appearance

// Configure container
container.guiConfig.width = 600;
container.guiConfig.height = 400;
container.guiConfig.color = "#2d2d2d";
container.guiConfig.position = Qt.vector2d(200, 200);

Theming the Application

// Modify NLStyle for custom theme
// Note: NLStyle properties are readonly, so you would need to modify the source file
// or create a custom style object

// Access style properties
Rectangle {
    color: NLStyle.primaryBackgroundColor
    border.color: NLStyle.primaryBorderColor
    Text {
        color: NLStyle.primaryTextColor
        font.family: NLStyle.fontType.roboto
    }
}

Port Configuration

// Create and configure port
var port = NLCore.createPort();
port.portType = NLSpec.PortType.Input;
port.portSide = NLSpec.PortPositionSide.Left;
port.title = "Input Value";
port.color = "#4A90E2";
port.enable = true;

Best Practices

  1. Use NLStyle for Defaults: Always use NLStyle properties for default values to maintain consistency.

  2. Consistent Colors: Use the color palette from NLStyle (primaryColor, primaryTextColor, etc.) for consistent theming.

  3. Auto-Sizing: Enable autoSize for nodes that need to adapt to content, but set appropriate minWidth and minHeight.

  4. Link Styling: Use Bezier curves for most links, L-lines for simple connections, and straight lines sparingly.

  5. Port Positioning: Follow conventions: Input ports on the left, Output ports on the right, Top/Bottom for special cases.

  6. Scene Dimensions: Use NLStyle.scene defaults for initial scene size, but allow users to expand as needed.

Advanced Topics

AQ

Custom Node Creation Guide

Overview

NodeLink provides a flexible and extensible framework for creating custom node types. This guide covers everything you need to know to create, register, and use custom nodes in your NodeLink application. Custom nodes are the building blocks of your visual programming interface, allowing users to create complex workflows by connecting different node types together.

Architecture Overview

The custom node creation system consists of several key components:




Node Registry
nodeTypes  nodeNames  nodeIcons
nodeColors  imports  defaultNode
β–Ό
Scene
createCustomizeNode()
createSpecificNode()
β–Ό
Custom Node
Node (Base)  Ports (I/O)  NodeData (Data)
GuiConfig (UI)  Properties (Custom)

Key Components

  1. Node Registry (NLNodeRegistry): Central registry for all node types
  2. Scene: Manages node creation and lifecycle
  3. Node: Base class for all custom nodes
  4. Port: Connection points for data flow
  5. NodeData: Data storage and processing
  6. NodeGuiConfig: Visual appearance configuration

Node Hierarchy

Class Hierarchy

QSObject (QtQuickStream)
    └── I_Node
        └── Node
            └── YourCustomNode

Interface Classes

I_Node

Location: resources/Core/I_Node.qml

Purpose: Base interface for all node-like objects.

Key Properties:
- objectType: Type identifier (Node, Link, Container)
- nodeData: Reference to node data object

Key Signals:
- cloneFrom(baseNode): Emitted when node is cloned

Key Functions:
- onCloneFrom(baseNode): Handler for cloning operation

Node

Location: resources/Core/Node.qml

Purpose: Base implementation for all custom nodes.

Key Properties:
- type: Unique integer identifier for node type
- title: Display name of the node
- guiConfig: NodeGuiConfig object for UI properties
- ports: Map of port UUIDs to Port objects
- children: Map of child node UUIDs to Node objects
- parents: Map of parent node UUIDs to Node objects
- imagesModel: ImagesModel for managing node images

Key Functions:
- addPort(port): Add a port to the node
- deletePort(port): Remove a port from the node
- findPort(portId): Find port by UUID
- findPortByPortSide(portSide): Find port by side position

Key Signals:
- portAdded(portId): Emitted when port is added
- nodeCompleted(): Emitted after Component.onCompleted

Step-by-Step Guide

Step 1: Create Node QML File

Create a new QML file for your custom node. The file should inherit from Node.

Example: MyCustomNode.qml

import QtQuick
import NodeLink

Node {
    // Set unique type identifier
    type: 1

    // Configure node data
    nodeData: I_NodeData {}

    // Configure GUI
    guiConfig.width: 200
    guiConfig.height: 150
    guiConfig.color: "#4A90E2"

    // Add ports when node is created
    Component.onCompleted: addPorts();

    // Handle cloning
    onCloneFrom: function(baseNode) {
        // Copy properties from base node
        // Customize as needed
    }

    // Add ports function
    function addPorts() {
        // Create and configure ports here
    }
}

Step 2: Register Node in Registry

Register your node in the NLNodeRegistry in your main QML file.

Example: main.qml

import QtQuick
import NodeLink

Window {
    property NLNodeRegistry nodeRegistry: NLNodeRegistry {
        _qsRepo: NLCore.defaultRepo
        imports: ["MyApp", "NodeLink"]
        defaultNode: 0
    }

    Component.onCompleted: {
        // Register your custom node
        var nodeType = 1;
        nodeRegistry.nodeTypes[nodeType] = "MyCustomNode";
        nodeRegistry.nodeNames[nodeType] = "My Custom Node";
        nodeRegistry.nodeIcons[nodeType] = "\uf123";  // Font Awesome icon
        nodeRegistry.nodeColors[nodeType] = "#4A90E2";

        // Set registry to scene
        scene.nodeRegistry = nodeRegistry;
    }
}

Step 3: Add to CMakeLists.txt

Add your node QML file to the CMake build configuration.

Example: CMakeLists.txt

qt_add_qml_module(MyApp
    URI "MyApp"
    VERSION 1.0
    QML_FILES
        main.qml
        MyCustomNode.qml
        # ... other files
)

Step 4: Override createCustomizeNode (Optional)

If you need custom node creation logic, override createCustomizeNode in your Scene.

Example: MyScene.qml

import QtQuick
import NodeLink

Scene {
    function createCustomizeNode(nodeType: int, xPos: real, yPos: real): string {
        var qsType = nodeRegistry.nodeTypes[nodeType];
        if (!qsType) {
            console.error("Unknown node type:", nodeType);
            return null;
        }

        // Generate unique title
        var title = nodeRegistry.nodeNames[nodeType] + "_" +
                   (Object.values(nodes).filter(node => node.type === nodeType).length + 1);

        // Apply snap if enabled
        if (NLStyle.snapEnabled) {
            var position = snappedPosition(Qt.vector2d(xPos, yPos));
            xPos = position.x;
            yPos = position.y;
        }

        // Create node
        return createSpecificNode(
            nodeRegistry.imports,
            nodeType,
            qsType,
            nodeRegistry.nodeColors[nodeType],
            title,
            xPos,
            yPos
        );
    }
}

Node Registry

NLNodeRegistry

Location: resources/Core/NLNodeRegistry.qml

Purpose: Central registry for managing all node types in the application.

Properties

imports: []

Array of QML module imports required to create nodes.

Example:

imports: ["MyApp", "NodeLink", "QtQuickStream"]

nodeTypes: {}

Map of node type IDs to QML component names.

Example:

nodeTypes: {
    0: "SourceNode",
    1: "OperationNode",
    2: "ResultNode"
}

nodeNames: {}

Map of node type IDs to display names.

Example:

nodeNames: {
    0: "Source",
    1: "Operation",
    2: "Result"
}

nodeIcons: {}

Map of node type IDs to icon characters (Font Awesome Unicode).

Example:

nodeIcons: {
    0: "\uf1c0",  // fa-file
    1: "\uf0ad",  // fa-cog
    2: "\uf00c"   // fa-check
}

Finding Font Awesome Icons:
- Visit Font Awesome Icons
- Copy the Unicode value (e.g., \uf1c0)
- Use in nodeIcons map

nodeColors: {}

Map of node type IDs to color strings (hex format).

Example:

nodeColors: {
    0: "#4A90E2",  // Blue
    1: "#F5A623",  // Orange
    2: "#7ED321"   // Green
}

defaultNode: int

Default node type to create when no type is specified.

Example:

defaultNode: 0  // Create SourceNode by default

nodeView: string

Path to custom node view component (optional).

Example:

nodeView: "qrc:/MyApp/NodeView.qml"

linkView: string

Path to custom link view component (optional).

Example:

linkView: "qrc:/MyApp/LinkView.qml"

containerView: string

Path to custom container view component (optional).

Example:

containerView: "qrc:/MyApp/ContainerView.qml"

Complete Registry Example

property NLNodeRegistry nodeRegistry: NLNodeRegistry {
    _qsRepo: NLCore.defaultRepo
    imports: ["Calculator", "NodeLink"]
    defaultNode: 0

    Component.onCompleted: {
        // Source Node
        nodeRegistry.nodeTypes[0] = "SourceNode";
        nodeRegistry.nodeNames[0] = "Source";
        nodeRegistry.nodeIcons[0] = "\uf1c0";
        nodeRegistry.nodeColors[0] = "#4A90E2";

        // Operation Node
        nodeRegistry.nodeTypes[1] = "OperationNode";
        nodeRegistry.nodeNames[1] = "Operation";
        nodeRegistry.nodeIcons[1] = "\uf0ad";
        nodeRegistry.nodeColors[1] = "#F5A623";

        // Result Node
        nodeRegistry.nodeTypes[2] = "ResultNode";
        nodeRegistry.nodeNames[2] = "Result";
        nodeRegistry.nodeIcons[2] = "\uf00c";
        nodeRegistry.nodeColors[2] = "#7ED321";
    }
}

Creating Custom Nodes

Basic Node Structure

import QtQuick
import NodeLink

Node {
    // 1. Set unique type identifier
    type: 1

    // 2. Configure node data
    nodeData: I_NodeData {}

    // 3. Configure GUI properties
    guiConfig.width: 200
    guiConfig.height: 150
    guiConfig.color: "#4A90E2"

    // 4. Add ports on creation
    Component.onCompleted: addPorts();

    // 5. Handle cloning (optional)
    onCloneFrom: function(baseNode) {
        // Copy properties if needed
    }

    // 6. Define port creation
    function addPorts() {
        // Create ports here
    }

    // 7. Add custom properties (optional)
    property string myProperty: "value"

    // 8. Add custom functions (optional)
    function myFunction() {
        // Custom logic
    }
}

Node Type Identifier

The type property is a unique integer that identifies your node type. It must match the key used in the node registry.

Node {
    type: 1  // Must match nodeRegistry.nodeTypes[1]
}

Best Practices:
- Use enum constants for type IDs (see examples)
- Start from 0 and increment sequentially
- Reserve 98 for CustomNode, 99 for Unknown
- Document your type IDs

Example with Enum:

// CSpecs.qml (Singleton)
pragma Singleton
import QtQuick

QtObject {
    enum NodeType {
        Source = 0,
        Operation = 1,
        Result = 2,
        CustomNode = 98,
        Unknown = 99
    }
}

// MyNode.qml
import QtQuick
import NodeLink
import MyApp

Node {
    type: CSpecs.NodeType.Operation
    // ...
}

Node Title

The title property is automatically set when the node is created, but you can customize it:

Node {
    type: 1
    title: "My Custom Node"  // Override default title
}

Default Title Format:

{nodeName}_{count}

Example: Source_1, Source_2, Operation_1

Node Data

Every node should have a nodeData property that stores the node's data.

Using I_NodeData

Node {
    nodeData: I_NodeData {}
}

Using Custom NodeData

// MyNodeData.qml
import QtQuick
import NodeLink

I_NodeData {
    property var input: null
    property var output: null
    property int count: 0
}

// MyNode.qml
Node {
    nodeData: MyNodeData {}
}

Accessing Node Data:

// In node
nodeData.data = "some value"
var value = nodeData.data

// In scene
node.nodeData.data = "some value"

Ports

Ports are connection points that allow nodes to send and receive data. Each port can be configured with type, position, and other properties.

Creating Ports

Use NLCore.createPort() to create new ports:

function addPorts() {
    let port = NLCore.createPort();
    port.portType = NLSpec.PortType.Output;
    port.portSide = NLSpec.PortPositionSide.Right;
    port.title = "Output";
    addPort(port);
}

Port Types

NLSpec.PortType.Input

Port can only receive connections (input only).

port.portType = NLSpec.PortType.Input

NLSpec.PortType.Output

Port can only send connections (output only).

port.portType = NLSpec.PortType.Output

NLSpec.PortType.InAndOut

Port can both send and receive connections (bidirectional).

port.portType = NLSpec.PortType.InAndOut

Port Positions

NLSpec.PortPositionSide.Top

Port appears on the top side of the node.

port.portSide = NLSpec.PortPositionSide.Top

NLSpec.PortPositionSide.Bottom

Port appears on the bottom side of the node.

port.portSide = NLSpec.PortPositionSide.Bottom

NLSpec.PortPositionSide.Left

Port appears on the left side of the node.

port.portSide = NLSpec.PortPositionSide.Left

NLSpec.PortPositionSide.Right

Port appears on the right side of the node.

port.portSide = NLSpec.PortPositionSide.Right

Port Properties

title: string

Display name for the port.

port.title = "Input Value"

color: string

Color of the port (hex format).

port.color = "#FF5733"

enable: bool

Whether the port is enabled (can be connected).

port.enable = true  // Enabled (default)
port.enable = false // Disabled (grayed out)

Complete Port Example

function addPorts() {
    // Input port (left side)
    let inputPort = NLCore.createPort();
    inputPort.portType = NLSpec.PortType.Input;
    inputPort.portSide = NLSpec.PortPositionSide.Left;
    inputPort.title = "Input";
    inputPort.color = "#4A90E2";
    addPort(inputPort);

    // Output port (right side)
    let outputPort = NLCore.createPort();
    outputPort.portType = NLSpec.PortType.Output;
    outputPort.portSide = NLSpec.PortPositionSide.Right;
    outputPort.title = "Output";
    outputPort.color = "#7ED321";
    addPort(outputPort);
}

Finding Ports

By UUID

var port = node.findPort(portUuid);

By Side

var port = node.findPortByPortSide(NLSpec.PortPositionSide.Left);

Port Management

Adding Ports

function addPorts() {
    let port = NLCore.createPort();
    // Configure port
    addPort(port);  // Add to node
}

Removing Ports

var port = node.findPort(portUuid);
if (port) {
    node.deletePort(port);
}

Node Data

Node data is stored in the nodeData property, which is an instance of I_NodeData or a custom subclass.

I_NodeData

Location: resources/Core/I_NodeData.qml

Base Properties:
- data: var: Generic data storage (can be any type)

Basic Usage:

Node {
    nodeData: I_NodeData {}

    function processData() {
        nodeData.data = "processed result";
    }
}

Custom NodeData

Create custom node data classes for type-safe data handling:

// OperationNodeData.qml
import QtQuick
import NodeLink

I_NodeData {
    property var input1: null
    property var input2: null
    property var output: null
    property int operation: 0
}

// OperationNode.qml
Node {
    nodeData: OperationNodeData {}

    function calculate() {
        if (nodeData.input1 && nodeData.input2) {
            switch (nodeData.operation) {
                case 0: // Add
                    nodeData.output = nodeData.input1 + nodeData.input2;
                    break;
                case 1: // Subtract
                    nodeData.output = nodeData.input1 - nodeData.input2;
                    break;
            }
            nodeData.data = nodeData.output;
        }
    }
}

Data Flow

Data flows through ports and is stored in node data:

Node A (Output) β†’ Link β†’ Node B (Input)
                      ↓
                 nodeData.input
                      ↓
              Process/Transform
                      ↓
                 nodeData.data
                      ↓
                 nodeData.output
                      ↓
Node B (Output) β†’ Link β†’ Node C (Input)

Accessing Connected Node Data

// In scene updateData function
function updateData() {
    Object.values(nodes).forEach(node => {
        // Get input from connected nodes
        Object.values(node.ports).forEach(port => {
            if (port.portType === NLSpec.PortType.Input) {
                // Find links connected to this port
                var link = findLinkByInputPort(port._qsUuid);
                if (link) {
                    var sourceNode = findNode(link.outputPort._qsUuid);
                    if (sourceNode) {
                        // Copy data from source
                        node.nodeData.input = sourceNode.nodeData.data;
                    }
                }
            }
        });

        // Process node data
        if (node.processData) {
            node.processData();
        }
    });
}

Node GUI Configuration

The guiConfig property controls the visual appearance and behavior of nodes.

NodeGuiConfig Properties

width: int and height: int

Node dimensions in pixels.

guiConfig.width: 200
guiConfig.height: 150

position: vector2d

Node position in the scene.

guiConfig.position: Qt.vector2d(100, 200)

color: string

Node background color (hex format).

guiConfig.color: "#4A90E2"

opacity: real

Node opacity (0.0 to 1.0).

guiConfig.opacity: 1.0

locked: bool

Whether the node is locked (cannot be moved).

guiConfig.locked: false

autoSize: bool

Automatically size node based on content and port titles.

guiConfig.autoSize: true  // Auto-size enabled (default)
guiConfig.autoSize: false // Fixed size

minWidth: int and minHeight: int

Minimum node dimensions when auto-sizing.

guiConfig.minWidth: 120
guiConfig.minHeight: 80

baseContentWidth: int

Base width for content area (for auto-sizing calculations).

guiConfig.baseContentWidth: 100

description: string

Node description text.

guiConfig.description: "This node performs calculations"

logoUrl: string

URL or path to node icon/logo.

guiConfig.logoUrl: "qrc:/icons/my-node-icon.png"

Complete GUI Config Example

Node {
    guiConfig.width: 250
    guiConfig.height: 200
    guiConfig.color: "#4A90E2"
    guiConfig.opacity: 1.0
    guiConfig.autoSize: false
    guiConfig.minWidth: 200
    guiConfig.minHeight: 150
    guiConfig.description: "Performs mathematical operations"
    guiConfig.logoUrl: "qrc:/icons/operation.png"
}

Advanced Topics

Cloning Nodes

Nodes can be cloned (copied) using the cloneFrom signal handler.

Basic Cloning

Node {
    onCloneFrom: function(baseNode) {
        // Base properties are copied automatically
        // Customize additional properties here
        myProperty = baseNode.myProperty;
    }
}

Advanced Cloning

Node {
    property var myData: null

    onCloneFrom: function(baseNode) {
        // Copy base properties
        title = baseNode.title;
        type = baseNode.type;

        // Copy custom properties
        if (baseNode.myData) {
            myData = JSON.parse(JSON.stringify(baseNode.myData));  // Deep copy
        }

        // Reset node data
        nodeData.data = null;
    }
}

Custom Properties

Add custom properties to your nodes:

Node {
    // Simple properties
    property string myString: "default"
    property int myNumber: 0
    property bool myBool: false

    // Complex properties
    property var myObject: ({})
    property var myArray: []

    // Custom objects
    property MyCustomObject myObject: MyCustomObject {}
}

Custom Functions

Add custom functions to your nodes:

Node {
    function processData() {
        // Custom processing logic
        if (nodeData.input) {
            nodeData.data = nodeData.input * 2;
        }
    }

    function validateInput() {
        return nodeData.input !== null && nodeData.input !== undefined;
    }

    function reset() {
        nodeData.data = null;
        nodeData.input = null;
    }
}

Dynamic Ports

Create ports dynamically based on conditions:

Node {
    property int inputCount: 2

    Component.onCompleted: {
        addPorts();
        updatePorts();
    }

    function addPorts() {
        // Create base ports
        let outputPort = NLCore.createPort();
        outputPort.portType = NLSpec.PortType.Output;
        outputPort.portSide = NLSpec.PortPositionSide.Right;
        addPort(outputPort);
    }

    function updatePorts() {
        // Remove old input ports
        Object.values(ports).forEach(port => {
            if (port.portType === NLSpec.PortType.Input) {
                deletePort(port);
            }
        });

        // Create new input ports
        for (var i = 0; i < inputCount; i++) {
            let inputPort = NLCore.createPort();
            inputPort.portType = NLSpec.PortType.Input;
            inputPort.portSide = NLSpec.PortPositionSide.Left;
            inputPort.title = "Input " + (i + 1);
            addPort(inputPort);
        }
    }

    onInputCountChanged: {
        updatePorts();
    }
}

Node Inheritance

Create base node classes and inherit from them:

// BaseOperationNode.qml
import QtQuick
import NodeLink

Node {
    property int operationType: 0
    nodeData: OperationNodeData {}

    function addPorts() {
        let inputPort = NLCore.createPort();
        inputPort.portType = NLSpec.PortType.Input;
        inputPort.portSide = NLSpec.PortPositionSide.Left;
        addPort(inputPort);

        let outputPort = NLCore.createPort();
        outputPort.portType = NLSpec.PortType.Output;
        outputPort.portSide = NLSpec.PortPositionSide.Right;
        addPort(outputPort);
    }
}

// AddNode.qml
BaseOperationNode {
    operationType: 0  // Add

    function calculate() {
        if (nodeData.input1 && nodeData.input2) {
            nodeData.data = nodeData.input1 + nodeData.input2;
        }
    }
}

// MultiplyNode.qml
BaseOperationNode {
    operationType: 1  // Multiply

    function calculate() {
        if (nodeData.input1 && nodeData.input2) {
            nodeData.data = nodeData.input1 * nodeData.input2;
        }
    }
}

Scene Integration

Override scene functions for custom node behavior:

// MyScene.qml
Scene {
    function createCustomizeNode(nodeType: int, xPos: real, yPos: real): string {
        // Custom creation logic
        var nodeId = createSpecificNode(
            nodeRegistry.imports,
            nodeType,
            nodeRegistry.nodeTypes[nodeType],
            nodeRegistry.nodeColors[nodeType],
            generateTitle(nodeType),
            xPos,
            yPos
        );

        // Post-creation setup
        var node = nodes[nodeId];
        if (node && node.initialize) {
            node.initialize();
        }

        return nodeId;
    }

    function generateTitle(nodeType: int): string {
        var baseName = nodeRegistry.nodeNames[nodeType];
        var count = Object.values(nodes).filter(n => n.type === nodeType).length;
        return baseName + "_" + (count + 1);
    }
}

Examples

Example 1: Simple Source Node

// SourceNode.qml
import QtQuick
import NodeLink

Node {
    type: 0
    nodeData: I_NodeData {}

    guiConfig.width: 150
    guiConfig.height: 100
    guiConfig.color: "#4A90E2"

    property real value: 0.0

    Component.onCompleted: addPorts();

    function addPorts() {
        let outputPort = NLCore.createPort();
        outputPort.portType = NLSpec.PortType.Output;
        outputPort.portSide = NLSpec.PortPositionSide.Right;
        outputPort.title = "value";
        addPort(outputPort);
    }

    onValueChanged: {
        nodeData.data = value;
    }
}

Example 2: Operation Node

// OperationNode.qml
import QtQuick
import NodeLink

Node {
    type: 1
    nodeData: OperationNodeData {}

    guiConfig.width: 200
    guiConfig.height: 120
    guiConfig.color: "#F5A623"

    property int operationType: 0  // 0=Add, 1=Subtract, 2=Multiply, 3=Divide

    Component.onCompleted: addPorts();

    function addPorts() {
        let input1Port = NLCore.createPort();
        input1Port.portType = NLSpec.PortType.Input;
        input1Port.portSide = NLSpec.PortPositionSide.Left;
        input1Port.title = "input 1";
        addPort(input1Port);

        let input2Port = NLCore.createPort();
        input2Port.portType = NLSpec.PortType.Input;
        input2Port.portSide = NLSpec.PortPositionSide.Left;
        input2Port.title = "input 2";
        addPort(input2Port);

        let outputPort = NLCore.createPort();
        outputPort.portType = NLSpec.PortType.Output;
        outputPort.portSide = NLSpec.PortPositionSide.Right;
        outputPort.title = "result";
        addPort(outputPort);
    }

    function calculate() {
        if (nodeData.input1 !== null && nodeData.input2 !== null) {
            switch (operationType) {
                case 0: nodeData.data = nodeData.input1 + nodeData.input2; break;
                case 1: nodeData.data = nodeData.input1 - nodeData.input2; break;
                case 2: nodeData.data = nodeData.input1 * nodeData.input2; break;
                case 3: nodeData.data = nodeData.input2 !== 0 ? nodeData.input1 / nodeData.input2 : 0; break;
            }
        } else {
            nodeData.data = null;
        }
    }
}

// OperationNodeData.qml
import QtQuick
import NodeLink

I_NodeData {
    property var input1: null
    property var input2: null
}

Example 3: Image Processing Node

// BlurNode.qml
import QtQuick
import NodeLink
import VisionLink

Node {
    type: 2
    nodeData: OperationNodeData {}

    guiConfig.width: 250
    guiConfig.height: 150
    guiConfig.color: "#9013FE"

    property real blurRadius: 5.0

    Component.onCompleted: addPorts();

    function addPorts() {
        let inputPort = NLCore.createPort();
        inputPort.portType = NLSpec.PortType.Input;
        inputPort.portSide = NLSpec.PortPositionSide.Left;
        inputPort.title = "image";
        addPort(inputPort);

        let outputPort = NLCore.createPort();
        outputPort.portType = NLSpec.PortType.Output;
        outputPort.portSide = NLSpec.PortPositionSide.Right;
        outputPort.title = "blurred";
        addPort(outputPort);
    }

    function processImage() {
        if (!nodeData.input) {
            nodeData.data = null;
            return;
        }

        var inputImage = nodeData.input;
        if (typeof inputImage === "string") {
            inputImage = ImageProcessor.loadImage(inputImage);
        }

        if (ImageProcessor.isValidImage(inputImage)) {
            nodeData.data = ImageProcessor.applyBlur(inputImage, blurRadius);
        } else {
            nodeData.data = null;
        }
    }

    onBlurRadiusChanged: {
        processImage();
    }
}

Best Practices

1. Use Enums for Type IDs

// CSpecs.qml
pragma Singleton
import QtQuick

QtObject {
    enum NodeType {
        Source = 0,
        Operation = 1,
        Result = 2
    }
}

// MyNode.qml
Node {
    type: CSpecs.NodeType.Operation
}

2. Initialize Ports in Component.onCompleted

Node {
    Component.onCompleted: addPorts();  // Always initialize ports here
}

3. Handle Cloning Properly

Node {
    onCloneFrom: function(baseNode) {
        // Reset node-specific data
        nodeData.data = null;
        // Copy only necessary properties
    }
}

4. Use Descriptive Port Titles

port.title = "Input Value"  // Good
port.title = "in"           // Bad

5. Set Appropriate Node Sizes

guiConfig.width: 200   // Reasonable default
guiConfig.height: 150
guiConfig.autoSize: true  // Let it adjust if needed

6. Validate Data Before Processing

function processData() {
    if (!nodeData.input || nodeData.input === null) {
        nodeData.data = null;
        return;
    }
    // Process data
}

7. Use Custom NodeData for Complex Data

// Instead of storing everything in nodeData.data
nodeData: MyCustomNodeData {
    property var input1: null
    property var input2: null
    property var output: null
}

8. Document Your Nodes

/*! ***********************************************************************************************
 * OperationNode performs mathematical operations on two inputs
 *
 * Properties:
 *   - operationType: Type of operation (0=Add, 1=Subtract, 2=Multiply, 3=Divide)
 *
 * Ports:
 *   - Input 1: Left side, receives first operand
 *   - Input 2: Left side, receives second operand
 *   - Output: Right side, outputs result
 * ************************************************************************************************/
Node {
    // ...
}

9. Use Consistent Naming

// Good
SourceNode.qml
OperationNode.qml
ResultNode.qml

// Bad
node1.qml
myNode.qml
test.qml

10. Register All Required Imports

nodeRegistry.imports: ["MyApp", "NodeLink", "QtQuickStream"]

Troubleshooting

Node Not Appearing

Problem: Node doesn't appear in the scene after creation.

Solutions:
1. Check node registry registration:

nodeRegistry.nodeTypes[nodeType] = "MyNode";  // Must match QML file name
  1. Verify imports:
nodeRegistry.imports: ["MyApp", "NodeLink"];  // Must include your module
  1. Check CMakeLists.txt:
QML_FILES
    MyNode.qml  # Must be included
  1. Verify QML file location matches import path.

Ports Not Showing

Problem: Ports don't appear on the node.

Solutions:
1. Ensure addPorts() is called:

Component.onCompleted: addPorts();
  1. Check port configuration:
port.portType = NLSpec.PortType.Input;  // Must be set
port.portSide = NLSpec.PortPositionSide.Left;  // Must be set
  1. Verify port is added:
addPort(port);  // Must call addPort

Data Not Flowing

Problem: Data doesn't flow between connected nodes.

Solutions:
1. Check scene's updateData() function is called:

onLinkAdded: updateData();
onLinkRemoved: updateData();
  1. Verify data is set in source node:
nodeData.data = value;  // Must set data property
  1. Check port types match:
// Source: Output port
// Destination: Input port

Node Not Cloning

Problem: Cloned node doesn't copy properties correctly.

Solutions:
1. Implement onCloneFrom:

onCloneFrom: function(baseNode) {
    // Copy properties
}
  1. Reset node-specific data:
onCloneFrom: function(baseNode) {
    nodeData.data = null;  // Reset data
}

Type Mismatch Errors

Problem: "Unknown node type" or type errors.

Solutions:
1. Verify type ID matches registry:

// Node
type: 1

// Registry
nodeRegistry.nodeTypes[1] = "MyNode";
  1. Check type is registered before use:
Component.onCompleted: {
    // Register types first
    nodeRegistry.nodeTypes[1] = "MyNode";
    // Then create scene
}

Import Errors

Problem: "module not found" or import errors.

Solutions:
1. Check CMakeLists.txt URI matches:

qt_add_qml_module(MyApp
    URI "MyApp"  # Must match import
)
  1. Verify imports in registry:
nodeRegistry.imports: ["MyApp", "NodeLink"];
  1. Check QML file import statements:
import MyApp  // Must match URI
import NodeLink

Conclusion

Creating custom nodes in NodeLink is a straightforward process that involves:

  1. Creating the Node QML File: Define your node structure, ports, and behavior
  2. Registering in Node Registry: Add node type, name, icon, and color
  3. Configuring Build System: Add QML files to CMakeLists.txt
  4. Implementing Data Flow: Handle data input, processing, and output

The framework provides all the necessary tools and infrastructure to create powerful, reusable node types for your visual programming applications.

Data Type Propagation

Overview

Data Type Propagation in NodeLink refers to the mechanism by which data flows through the node graph, from source nodes through processing nodes to result nodes. This document explains how data propagation works, different propagation algorithms, and how to implement custom data flow logic for your application.

Understanding Data Flow

Basic Concepts

Data Flow in NodeLink follows these principles:

  1. Source Nodes: Generate initial data (e.g., user input, file loading)
  2. Processing Nodes: Transform data from input to output
  3. Result Nodes: Display or consume final data
  4. Links: Connect nodes and define data flow direction

Data Flow Direction

Source Node β†’ Processing Node β†’ Result Node
     ↓              ↓                ↓
  Output        Input/Output      Input

Key Terms

Data Flow Architecture

Node Data Storage

All nodes store their data in the nodeData property, which is an instance of I_NodeData or a custom subclass:

// Base node data structure
I_NodeData {
    property var data: null  // Main data storage
}

Custom Node Data

You can create custom node data classes for type-safe data handling:

// OperationNodeData.qml
I_NodeData {
    property var inputFirst: null
    property var inputSecond: null
    property var output: null
    property var data: null  // Final result
}

Data Flow Triggers

Data propagation is triggered by:

  1. Link Added: When a new connection is created
  2. Link Removed: When a connection is deleted
  3. Node Removed: When a node is deleted
  4. Manual Update: When updateData() is called explicitly
  5. Parameter Change: When node parameters change (e.g., slider value)

Propagation Algorithms

NodeLink supports different propagation algorithms depending on your use case:

1. Sequential Propagation (Calculator Example)

Use Case: Simple data flow where nodes process data in order.

Algorithm:
1. Initialize all node data to null
2. Process links where upstream node has data
3. Update downstream nodes
4. Repeat until all nodes are processed

Example:

function updateData() {
    // Initialize node data
    Object.values(nodes).forEach(node => {
        if (node.type === CSpecs.NodeType.Operation) {
            node.nodeData.data = null;
            node.nodeData.inputFirst = null;
            node.nodeData.inputSecond = null;
        }
    });

    // Process links
    Object.values(links).forEach(link => {
        var upstreamNode = findNode(link.inputPort._qsUuid);
        var downstreamNode = findNode(link.outputPort._qsUuid);

        if (upstreamNode.nodeData.data) {
            upadateNodeData(upstreamNode, downstreamNode);
        }
    });

    // Handle nodes waiting for multiple inputs
    while (notReadyLinks.length > 0) {
        // Process remaining links
    }
}

2. Iterative Propagation (Logic Circuit Example)

Use Case: Circuits where signals propagate through gates until stable.

Algorithm:
1. Reset all node outputs
2. Iterate through links multiple times
3. Update nodes when all inputs are available
4. Continue until no more changes occur

Example:

function updateLogic() {
    // Reset all operation nodes
    Object.values(nodes).forEach(node => {
        if (node.type === LSpecs.NodeType.AND ||
            node.type === LSpecs.NodeType.OR ||
            node.type === LSpecs.NodeType.NOT) {
            node.nodeData.inputA = null;
            node.nodeData.inputB = null;
            node.nodeData.output = null;
        }
    });

    // Propagate values through the circuit
    var maxIterations = 999;
    var changed = true;

    for (var i = 0; i < maxIterations && changed; i++) {
        changed = false;

        Object.values(links).forEach(link => {
            var upstreamNode = findNode(link.inputPort._qsUuid);
            var downstreamNode = findNode(link.outputPort._qsUuid);

            if (upstreamNode && downstreamNode &&
                upstreamNode.nodeData.output !== null) {
                // Update downstream node
                if (downstreamNode.nodeData.inputA === null) {
                    downstreamNode.nodeData.inputA = upstreamNode.nodeData.output;
                    changed = true;
                }

                // Update node if all inputs are ready
                if (canUpdateNode(downstreamNode)) {
                    downstreamNode.updateData();
                }
            }
        });
    }
}

Use Case: Processing pipelines where order matters and dependencies must be resolved.

Algorithm:
1. Build dependency graph
2. Process nodes in topological order
3. Update downstream nodes only when upstream has data
4. Handle circular dependencies

Example:

function updateData() {
    var allLinks = Object.values(links);
    var processedLinks = [];
    var remainingLinks = allLinks.slice();
    var maxIterations = 100;
    var iteration = 0;

    while (remainingLinks.length > 0 && iteration < maxIterations) {
        iteration++;
        var linksProcessedThisIteration = [];
        var linksStillWaiting = [];

        remainingLinks.forEach(function(link) {
            var upstreamNode = findNode(link.inputPort._qsUuid);
            var downstreamNode = findNode(link.outputPort._qsUuid);

            // Check if upstream node has data
            var upstreamHasData = upstreamNode.nodeData.data !== null &&
                                 upstreamNode.nodeData.data !== undefined;

            if (upstreamHasData) {
                // Process this link now
                upadateNodeData(upstreamNode, downstreamNode);
                linksProcessedThisIteration.push(link);
            } else {
                // Can't process yet, upstream doesn't have data
                linksStillWaiting.push(link);
            }
        });

        // Update remaining links for next iteration
        remainingLinks = linksStillWaiting;
        processedLinks = processedLinks.concat(linksProcessedThisIteration);
    }
}

4. Incremental Propagation

Use Case: When only a specific node changes and you want to update only affected downstream nodes.

Algorithm:
1. Start from changed node
2. Find all downstream nodes
3. Update them in order
4. Stop when no more updates needed

Example:

function updateDataFromNode(startingNode) {
    // First, update the starting node itself
    if (startingNode.type === CSpecs.NodeType.Operation) {
        startingNode.updataData();
    }

    // Find all downstream nodes and update them
    var nodesToUpdate = [startingNode];
    var processedNodes = [];
    var maxIterations = 100;
    var iteration = 0;

    while (nodesToUpdate.length > 0 && iteration < maxIterations) {
        iteration++;
        var currentNode = nodesToUpdate.shift();
        processedNodes.push(currentNode._qsUuid);

        // Find all links where this node is upstream
        var downstreamLinks = Object.values(links).filter(function(link) {
            var upstreamNodeId = findNodeId(link.outputPort._qsUuid);
            return upstreamNodeId === currentNode._qsUuid;
        });

        // Process each downstream link
        downstreamLinks.forEach(function(link) {
            var downstreamNode = findNode(link.outputPort._qsUuid);
            upadateNodeData(currentNode, downstreamNode);

            // Add to queue if not already processed
            if (processedNodes.indexOf(downstreamNode._qsUuid) === -1) {
                nodesToUpdate.push(downstreamNode);
            }
        });
    }
}

Node Data Structure

Base Node Data

// I_NodeData.qml
QSObject {
    property var data: null  // Main data storage (can be any type)
}

Custom Node Data Examples

Calculator Node Data

// OperationNodeData.qml
I_NodeData {
    property var inputFirst: null   // First input value
    property var inputSecond: null  // Second input value
    property var data: null         // Calculated result
}

Logic Circuit Node Data

// LogicGateNodeData.qml
I_NodeData {
    property bool inputA: null      // First input (boolean)
    property bool inputB: null      // Second input (boolean, optional)
    property bool output: null      // Gate output (boolean)
}

Image Processing Node Data

// ImageNodeData.qml
I_NodeData {
    property var input: null        // Input image (QImage)
    property var data: null         // Processed image (QImage)
}

Data Type Examples

NodeLink supports any data type:

// Numbers
node.nodeData.data = 42;
node.nodeData.data = 3.14;

// Strings
node.nodeData.data = "Hello World";

// Booleans
node.nodeData.data = true;

// Objects
node.nodeData.data = {
    value: 100,
    name: "test",
    timestamp: Date.now()
};

// Arrays
node.nodeData.data = [1, 2, 3, 4, 5];

// QML Objects (e.g., QImage)
node.nodeData.data = imageObject;

Implementation Patterns

Pattern 1: Simple Data Transfer

For nodes that simply pass data through:

function upadateNodeData(upstreamNode, downstreamNode) {
    // Direct data transfer
    downstreamNode.nodeData.data = upstreamNode.nodeData.data;
}

Pattern 2: Single Input Processing

For nodes that process one input:

function upadateNodeData(upstreamNode, downstreamNode) {
    // Set input
    downstreamNode.nodeData.input = upstreamNode.nodeData.data;

    // Process
    downstreamNode.updataData();
}

Pattern 3: Multiple Input Processing

For nodes that need multiple inputs:

function upadateNodeData(upstreamNode, downstreamNode) {
    // Assign to first available input
    if (!downstreamNode.nodeData.inputFirst) {
        downstreamNode.nodeData.inputFirst = upstreamNode.nodeData.data;
    } else if (!downstreamNode.nodeData.inputSecond) {
        downstreamNode.nodeData.inputSecond = upstreamNode.nodeData.data;
    }

    // Update if all inputs are ready
    if (downstreamNode.nodeData.inputFirst &&
        downstreamNode.nodeData.inputSecond) {
        downstreamNode.updataData();
    }
}

Pattern 4: Type-Specific Processing

For nodes that handle different data types:

function upadateNodeData(upstreamNode, downstreamNode) {
    switch (downstreamNode.type) {
        case CSpecs.NodeType.Operation: {
            downstreamNode.nodeData.input = upstreamNode.nodeData.data;
            downstreamNode.updataData();
        } break;

        case CSpecs.NodeType.Result: {
            // Direct transfer for result nodes
            downstreamNode.nodeData.data = upstreamNode.nodeData.data;
        } break;

        default: {
            // Default behavior
        }
    }
}

Examples

Example 1: Calculator Data Flow

Source: examples/calculator/resources/Core/CalculatorScene.qml

I_Scene {
    // Update data when connections change
    onLinkRemoved: _upateDataTimer.start();
    onNodeRemoved: _upateDataTimer.start();
    onLinkAdded: updateData();

    function updateData() {
        var notReadyLinks = [];

        // Initialize node data
        Object.values(nodes).forEach(node => {
            switch (node.type) {
                case CSpecs.NodeType.Additive:
                case CSpecs.NodeType.Multiplier:
                case CSpecs.NodeType.Subtraction:
                case CSpecs.NodeType.Division: {
                    node.nodeData.data = null;
                    node.nodeData.inputFirst = null;
                    node.nodeData.inputSecond = null;
                } break;
                case CSpecs.NodeType.Result: {
                    node.nodeData.data = null;
                } break;
            }
        });

        // Process links
        Object.values(links).forEach(link => {
            var upstreamNode = findNode(link.inputPort._qsUuid);
            var downstreamNode = findNode(link.outputPort._qsUuid);

            if (!upstreamNode.nodeData.data &&
                upstreamNode.type !== CSpecs.NodeType.Source) {
                // Check if upstream needs multiple inputs
                var upstreamNodeLinks = Object.values(links).filter(linkObj =>
                    findNodeId(linkObj.outputPort._qsUuid) === upstreamNode._qsUuid);

                if (upstreamNodeLinks.length > 1) {
                    notReadyLinks.push(link);
                    return;
                }
            }

            upadateNodeData(upstreamNode, downstreamNode);
        });

        // Process remaining links
        while (notReadyLinks.length > 0) {
            notReadyLinks.forEach((link, index) => {
                var upstreamNode = findNode(link.inputPort._qsUuid);
                var downstreamNode = findNode(link.outputPort._qsUuid);

                if (upstreamNode.nodeData.data) {
                    notReadyLinks.splice(index, 1);
                    upadateNodeData(upstreamNode, downstreamNode);
                }
            });
        }
    }

    function upadateNodeData(upstreamNode, downstreamNode) {
        switch (downstreamNode.type) {
            case CSpecs.NodeType.Additive:
            case CSpecs.NodeType.Multiplier:
            case CSpecs.NodeType.Subtraction:
            case CSpecs.NodeType.Division: {
                // Assign to first available input
                if (!downstreamNode.nodeData.inputFirst) {
                    downstreamNode.nodeData.inputFirst = upstreamNode.nodeData.data;
                } else if (!downstreamNode.nodeData.inputSecond) {
                    downstreamNode.nodeData.inputSecond = upstreamNode.nodeData.data;
                }

                // Update if both inputs are ready
                downstreamNode.updataData();
            } break;

            case CSpecs.NodeType.Result: {
                // Direct transfer
                downstreamNode.nodeData.data = upstreamNode.nodeData.data;
            } break;
        }
    }
}

Example 2: Logic Circuit Signal Propagation

Source: examples/logicCircuit/resources/Core/LogicCircuitScene.qml

I_Scene {
    function updateLogic() {
        // Reset all operation nodes
        Object.values(nodes).forEach(node => {
            if (node.type === LSpecs.NodeType.AND ||
                node.type === LSpecs.NodeType.OR ||
                node.type === LSpecs.NodeType.NOT) {
                node.nodeData.inputA = null;
                node.nodeData.inputB = null;
                node.nodeData.output = null;
            }
            if (node.type === LSpecs.NodeType.Output) {
                node.nodeData.inputA = null;
                node.nodeData.displayValue = "UNDEFINED";
            }
        });

        // Track connections to prevent duplicate inputs
        var connectionMap = {};
        var maxIterations = 999;
        var changed = true;

        // Iterate until stable
        for (var i = 0; i < maxIterations && changed; i++) {
            changed = false;

            Object.values(links).forEach(link => {
                var upstreamNode = findNode(link.inputPort._qsUuid);
                var downstreamNode = findNode(link.outputPort._qsUuid);

                if (upstreamNode && downstreamNode &&
                    upstreamNode.nodeData.output !== null) {

                    var connectionKey = downstreamNode._qsUuid + "_" +
                                       upstreamNode._qsUuid;

                    // Handle 2-input gates (AND, OR)
                    if (downstreamNode.type === LSpecs.NodeType.AND ||
                        downstreamNode.type === LSpecs.NodeType.OR) {

                        if (downstreamNode.nodeData.inputA === null &&
                            !connectionMap[connectionKey + "_A"]) {
                            downstreamNode.nodeData.inputA = upstreamNode.nodeData.output;
                            connectionMap[connectionKey + "_A"] = true;
                            changed = true;
                        } else if (downstreamNode.nodeData.inputB === null &&
                                   !connectionMap[connectionKey + "_B"]) {
                            // Ensure different upstream nodes for A and B
                            var inputAUpstream = null;
                            Object.keys(connectionMap).forEach(key => {
                                if (key.startsWith(downstreamNode._qsUuid) &&
                                    key.endsWith("_A")) {
                                    inputAUpstream = key.split("_")[1];
                                }
                            });

                            if (inputAUpstream !== upstreamNode._qsUuid) {
                                downstreamNode.nodeData.inputB = upstreamNode.nodeData.output;
                                connectionMap[connectionKey + "_B"] = true;
                                changed = true;
                            }
                        }
                    }
                    // Handle single-input gates (NOT, Output)
                    else if (downstreamNode.type === LSpecs.NodeType.NOT ||
                             downstreamNode.type === LSpecs.NodeType.Output) {
                        if (downstreamNode.nodeData.inputA === null) {
                            downstreamNode.nodeData.inputA = upstreamNode.nodeData.output;
                            changed = true;
                        }
                    }

                    // Update node if all inputs are ready
                    if (downstreamNode.updateData) {
                        var canUpdate = false;
                        switch(downstreamNode.type) {
                            case LSpecs.NodeType.AND:
                            case LSpecs.NodeType.OR:
                                canUpdate = (downstreamNode.nodeData.inputA !== null &&
                                           downstreamNode.nodeData.inputB !== null);
                                break;
                            case LSpecs.NodeType.NOT:
                            case LSpecs.NodeType.Output:
                                canUpdate = (downstreamNode.nodeData.inputA !== null);
                                break;
                        }

                        if (canUpdate) {
                            downstreamNode.updateData();
                        }
                    }
                }
            });
        }
    }
}

Example 3: Image Processing Pipeline

Source: examples/visionLink/resources/Core/VisionLinkScene.qml

I_Scene {
    function updateData() {
        // Process links in topological order
        var allLinks = Object.values(links);
        var remainingLinks = allLinks.slice();
        var maxIterations = 100;
        var iteration = 0;

        while (remainingLinks.length > 0 && iteration < maxIterations) {
            iteration++;
            var linksProcessedThisIteration = [];
            var linksStillWaiting = [];

            remainingLinks.forEach(function(link) {
                var upstreamNode = findNode(link.inputPort._qsUuid);
                var downstreamNode = findNode(link.outputPort._qsUuid);

                // Check if upstream node has data
                var upstreamHasData = upstreamNode.nodeData.data !== null &&
                                     upstreamNode.nodeData.data !== undefined;

                // ImageInput always has data
                if (upstreamNode.type === CSpecs.NodeType.ImageInput) {
                    upstreamHasData = true;
                }

                if (upstreamHasData) {
                    // Process this link now
                    upadateNodeData(upstreamNode, downstreamNode);
                    linksProcessedThisIteration.push(link);
                } else {
                    // Can't process yet
                    linksStillWaiting.push(link);
                }
            });

            remainingLinks = linksStillWaiting;
        }
    }

    function updateDataFromNode(startingNode) {
        // Update starting node first
        if (startingNode.type === CSpecs.NodeType.Blur ||
            startingNode.type === CSpecs.NodeType.Brightness ||
            startingNode.type === CSpecs.NodeType.Contrast) {
            startingNode.updataData();
        }

        // Propagate to downstream nodes
        var nodesToUpdate = [startingNode];
        var processedNodes = [];
        var maxIterations = 100;
        var iteration = 0;

        while (nodesToUpdate.length > 0 && iteration < maxIterations) {
            iteration++;
            var currentNode = nodesToUpdate.shift();
            processedNodes.push(currentNode._qsUuid);

            // Find downstream links
            var downstreamLinks = Object.values(links).filter(function(link) {
                var upstreamNodeId = findNodeId(link.inputPort._qsUuid);
                return upstreamNodeId === currentNode._qsUuid;
            });

            // Process each downstream link
            downstreamLinks.forEach(function(link) {
                var downstreamNode = findNode(link.outputPort._qsUuid);
                upadateNodeData(currentNode, downstreamNode);

                if (processedNodes.indexOf(downstreamNode._qsUuid) === -1) {
                    nodesToUpdate.push(downstreamNode);
                }
            });
        }
    }

    function upadateNodeData(upstreamNode, downstreamNode) {
        switch (downstreamNode.type) {
            case CSpecs.NodeType.Blur:
            case CSpecs.NodeType.Brightness:
            case CSpecs.NodeType.Contrast: {
                downstreamNode.nodeData.input = upstreamNode.nodeData.data;
                downstreamNode.updataData();
            } break;

            case CSpecs.NodeType.ImageResult: {
                downstreamNode.nodeData.data = upstreamNode.nodeData.data;
            } break;
        }
    }
}

Best Practices

1. Initialize Node Data

Always initialize node data before processing:

function updateData() {
    // Reset all node data first
    Object.values(nodes).forEach(node => {
        if (node.type === CSpecs.NodeType.Operation) {
            node.nodeData.data = null;
            node.nodeData.inputFirst = null;
            node.nodeData.inputSecond = null;
        }
    });

    // Then process links
}

2. Handle Null/Undefined Data

Always check for null/undefined before processing:

function upadateNodeData(upstreamNode, downstreamNode) {
    if (!upstreamNode || !downstreamNode) {
        return;
    }

    if (upstreamNode.nodeData.data === null ||
        upstreamNode.nodeData.data === undefined) {
        return;
    }

    // Process data
}

3. Prevent Infinite Loops

Use iteration limits:

var maxIterations = 100;
var iteration = 0;

while (remainingLinks.length > 0 && iteration < maxIterations) {
    iteration++;
    // Process links
}

4. Track Processed Nodes

Prevent processing the same node multiple times:

var processedNodes = [];

if (processedNodes.indexOf(node._qsUuid) === -1) {
    processedNodes.push(node._qsUuid);
    // Process node
}

5. Handle Multiple Inputs

For nodes with multiple inputs, check all inputs before processing:

function upadateNodeData(upstreamNode, downstreamNode) {
    // Assign to first available input
    if (!downstreamNode.nodeData.inputFirst) {
        downstreamNode.nodeData.inputFirst = upstreamNode.nodeData.data;
    } else if (!downstreamNode.nodeData.inputSecond) {
        downstreamNode.nodeData.inputSecond = upstreamNode.nodeData.data;
    }

    // Only process when all inputs are ready
    if (downstreamNode.nodeData.inputFirst &&
        downstreamNode.nodeData.inputSecond) {
        downstreamNode.updataData();
    }
}

6. Use Incremental Updates

For performance, update only affected nodes:

// Instead of updating all nodes
function updateDataFromNode(startingNode) {
    // Only update downstream nodes from startingNode
    // More efficient for large graphs
}

7. Validate Data Types

Check data types before processing:

function upadateNodeData(upstreamNode, downstreamNode) {
    var data = upstreamNode.nodeData.data;

    // Type validation
    if (typeof data !== 'number') {
        console.warn("Expected number, got:", typeof data);
        return;
    }

    // Process data
}

8. Error Handling

Handle errors gracefully:

function upadateNodeData(upstreamNode, downstreamNode) {
    try {
        downstreamNode.nodeData.input = upstreamNode.nodeData.data;
        downstreamNode.updataData();
    } catch (error) {
        console.error("Error updating node data:", error);
        downstreamNode.nodeData.data = null;
    }
}

Common Patterns

Pattern: Source β†’ Operation β†’ Result

Source Node (data = 10)
    ↓
Operation Node (inputFirst = 10, inputSecond = 5, data = 15)
    ↓
Result Node (data = 15)

Pattern: Multiple Inputs

Source1 (data = 10) ──┐
                      β”œβ”€β†’ Operation (inputFirst = 10, inputSecond = 5)
Source2 (data = 5)  β”€β”€β”˜

Pattern: Processing Chain

Source β†’ Blur β†’ Brightness β†’ Contrast β†’ Result

Pattern: Branching

Source ──┬─→ Operation1 β†’ Result1
         └─→ Operation2 β†’ Result2

Troubleshooting

Data Not Propagating

Infinite Loops

Wrong Data Values

Performance Issues

Undo/Redo System Documentation

Overview

The Undo/Redo system in NodeLink is a comprehensive command-based architecture that allows users to undo and redo operations performed on the node graph. This system tracks all changes to nodes, links, containers, and their properties, providing a seamless way to revert or replay actions. The implementation uses the Command Pattern combined with Observer Pattern to automatically capture and record all modifications.

Architecture Overview

The Undo/Redo system consists of several key components working together:


        
UndoCore
CommandStack
Undo Stack [Commands]  Redo Stack [Commands]

UndoSceneObserver
Node Observers  Link Observers  Container Observers
β–Ό
Scene Operations
(Node / Link / Container Changes)

Key Design Principles

  1. Command Pattern: Each operation is encapsulated as a command object with undo() and redo() methods
  2. Observer Pattern: Automatic tracking of property changes through observers
  3. Batch Aggregation: Multiple rapid changes are grouped into single undo/redo operations
  4. Memory Management: Automatic cleanup of old commands to prevent memory leaks
  5. Observer Blocking: Prevents infinite loops during undo/redo execution

Core Components

1. UndoCore

Location: resources/Core/Undo/UndoCore.qml

Purpose: The central coordinator for the undo/redo system.

Responsibilities:

Properties:

Code Structure:

QtObject {
    required property I_Scene scene
    property CommandStack undoStack: CommandStack { }
    property UndoSceneObserver undoSceneObserver: UndoSceneObserver {
        scene: root.scene
        undoStack: root.undoStack
    }
}

2. CommandStack

Location: resources/Core/Undo/CommandStack.qml

Purpose: Manages the undo and redo stacks, handles command execution, and provides batch aggregation.

Key Features:

Properties:

push(cmd, appliedAlready = true)

Adds a command to the undo stack. Commands are batched together if they arrive within 200ms.

function push(cmd, appliedAlready = true) {
    if (!cmd || typeof cmd.redo !== "function" || typeof cmd.undo !== "function")
        return

    if (!appliedAlready) {
        // Execute the command if not already applied
        isReplaying = true
        NLSpec.undo.blockObservers = true
        try {
            cmd.redo()
        } finally {
            NLSpec.undo.blockObservers = false
            isReplaying = false
        }
    }

    // Add to pending batch
    _pendingCommands.push(cmd)
    _batchTimer.restart()  // 200ms delay for batching
}

undo()

Executes the most recent command's undo function and moves it to the redo stack.

function undo() {
    if (!isValidUndo)
        return

    const cmd = undoStack.shift()  // Get most recent command
    undoStackChanged()

    // Block observers to prevent creating new commands
    isReplaying = true
    NLSpec.undo.blockObservers = true
    try {
        cmd.undo()  // Execute undo
    } finally {
        NLSpec.undo.blockObservers = false
        isReplaying = false
    }

    redoStack.unshift(cmd)  // Move to redo stack
    redoStackChanged()
    undoRedoDone()
    stacksUpdated()
}

redo()

Executes the most recent redo command and moves it back to the undo stack.

function redo() {
    if (!isValidRedo)
        return

    const cmd = redoStack.shift()  // Get most recent redo command
    redoStackChanged()

    // Block observers to prevent creating new commands
    isReplaying = true
    NLSpec.undo.blockObservers = true
    try {
        cmd.redo()  // Execute redo
    } finally {
        NLSpec.undo.blockObservers = false
        isReplaying = false
    }

    undoStack.unshift(cmd)  // Move back to undo stack
    undoStackChanged()
    undoRedoDone()
    stacksUpdated()
}

Batch Aggregation

Commands arriving within 200ms are grouped into a macro command:

function _finalizePending() {
    if (_pendingCommands.length === 0)
        return

    const cmds = _pendingCommands.slice()
    _pendingCommands = []

    // Create macro command
    var macro = {
        subCommands: cmds,
        undo: function() {
            // Undo in reverse order
            for (var i = cmds.length - 1; i >= 0; --i) {
                cmds[i].undo()
            }
        },
        redo: function() {
            // Redo in forward order
            for (var i = 0; i < cmds.length; ++i) {
                cmds[i].redo()
            }
        }
    }

    undoStack.unshift(macro)
    _enforceStackLimit()
    clearRedo()  // Clear redo stack when new action occurs
    stacksUpdated()
}

Benefits of Batching:


3. UndoSceneObserver

Location: resources/Core/Undo/UndoSceneObserver.qml

Purpose: Creates and manages observers for all nodes, links, and containers in the scene.

Structure:

Item {
    required property I_Scene scene
    required property CommandStack undoStack

    // Node Observers
    Repeater {
        model: Object.values(root.scene.nodes)
        delegate: UndoNodeObserver {
            node: modelData
            undoStack: root.undoStack
        }
    }

    // Link Observers
    Repeater {
        model: Object.values(root.scene.links)
        delegate: UndoLinkObserver {
            link: modelData
            undoStack: root.undoStack
        }
    }

    // Container Observers
    Repeater {
        model: Object.values(root.scene.containers)
        delegate: UndoContainerObserver {
            container: modelData
            undoStack: root.undoStack
        }
    }
}

How It Works:


4. Property Observers

UndoNodeObserver

Location: resources/Core/Undo/UndoNodeObserver.qml

Purpose: Tracks changes to Node properties (title, type).

Monitored Properties:

How It Works:

  1. Caches initial property values on creation
  2. Listens to property change signals
  3. Creates PropertyCommand when values change
  4. Pushes command to CommandStack

Code Example:

Connections {
    target: node
    enabled: !NLSpec.undo.blockObservers

    function onTitleChanged() {
        _ensureCache()
        let oldV = _cache.title
        let newV = node.title
        pushProp(node, "title", oldV, newV)
        _cache.title = newV
    }
}

Also Includes: UndoNodeGuiObserver for tracking GUI properties (position, size, color, etc.)


UndoLinkObserver

Location: resources/Core/Undo/UndoLinkObserver.qml

Purpose: Tracks changes to Link properties.

Monitored Properties:

Also Includes: UndoLinkGuiObserver for tracking GUI properties


UndoContainerObserver

Location: resources/Core/Undo/UndoContainerObserver.qml

Purpose: Tracks changes to Container properties.

Monitored Properties:

Also Includes: UndoContainerGuiObserver for tracking GUI properties


GUI Observers

UndoNodeGuiObserver, UndoLinkGuiObserver, UndoContainerGuiObserver

Purpose: Track changes to GUI configuration properties.

Monitored Properties:

Special Handling:

How It Works

Operation Flow

1. User Action (e.g., Move Node)

User drags node
    ↓
NodeGuiConfig.position changes
    ↓
UndoNodeGuiObserver detects change
    ↓
Creates PropertyCommand with old/new position
    ↓
CommandStack.push(command)
    ↓
Command added to pending batch
    ↓
After 200ms: Batch finalized
    ↓
Macro command added to undoStack
    ↓
Redo stack cleared

2. Undo Operation

User presses Ctrl+Z
    ↓
CommandStack.undo() called
    ↓
Most recent command removed from undoStack
    ↓
NLSpec.undo.blockObservers = true
    ↓
Command.undo() executed
    ↓
Property restored to old value
    ↓
Observers blocked, so no new command created
    ↓
Command moved to redoStack
    ↓
NLSpec.undo.blockObservers = false
    ↓
undoRedoDone() signal emitted

3. Redo Operation

User presses Ctrl+Y
    ↓
CommandStack.redo() called
    ↓
Most recent command removed from redoStack
    ↓
NLSpec.undo.blockObservers = true
    ↓
Command.redo() executed
    ↓
Property restored to new value
    ↓
Observers blocked, so no new command created
    ↓
Command moved back to undoStack
    ↓
NLSpec.undo.blockObservers = false
    ↓
undoRedoDone() signal emitted

Command Pattern Implementation

Command Interface

All commands implement the I_Command interface:

Location: resources/Core/Undo/Commands/I_Command.qml

QtObject {
    property var scene: null

    function redo() {
        // Apply the command
    }

    function undo() {
        // Reverse the command
    }
}

Available Commands

1. AddNodeCommand

Purpose: Tracks node addition to the scene.

Properties:

Implementation:

function redo() {
    if (isValidScene() && isValidNode(node))
        scene.addNode(node)
}

function undo() {
    if (isValidScene() && isValidNode(node)) {
        scene.selectionModel.remove(node._qsUuid)
        scene.nodeRemoved(node)
        delete scene.nodes[node._qsUuid]
        scene.nodesChanged()
    }
}

Usage: Created automatically when scene.addNode() is called.


2. RemoveNodeCommand

Purpose: Tracks node removal from the scene.

Properties:

Implementation:

function redo() {
    if (isValidScene() && isValidNode(node))
        scene.deleteNode(node._qsUuid)
}

function undo() {
    if (isValidScene() && isValidNode(node)) {
        scene.addNode(node)
        // Restore connected links
        if (links && links.length) {
            scene.restoreLinks(links)
        }
    }
}

Key Feature: Preserves link objects for restoration during undo.


3. AddNodesCommand

Purpose: Tracks batch node addition (multiple nodes at once).

Properties:

Implementation: Similar to AddNodeCommand but handles multiple nodes.

Usage: Created when scene.addNodes() is called with multiple nodes.


4. RemoveNodesCommand

Purpose: Tracks batch node removal.

Properties:

Implementation: Handles multiple nodes and their links.


5. CreateLinkCommand

Purpose: Tracks link creation between two ports.

Properties:

Implementation:

function redo() {
    if (!isValidScene() || !isValidUuid(inputPortUuid) || !isValidUuid(outputPortUuid))
        return

    if (createdLink) {
        // Restore existing link
        scene.restoreLinks([createdLink])
    } else {
        // Create new link
        createdLink = scene.createLink(inputPortUuid, outputPortUuid)
    }
}

function undo() {
    if (!isValidScene() || !isValidLink(createdLink))
        return

    // Remove parent/children relationships
    // Remove link from scene (but don't destroy it)
    scene.linkRemoved(createdLink)
    scene.selectionModel.remove(createdLink._qsUuid)
    delete scene.links[createdLink._qsUuid]
    scene.linksChanged()
}

Key Feature: Preserves link object for redo (doesn't destroy it).


6. UnlinkCommand

Purpose: Tracks link removal.

Properties:

Implementation:

function redo() {
    if (!isValidScene() || !isValidUuid(inputPortUuid) || !isValidUuid(outputPortUuid))
        return
    scene.unlinkNodes(inputPortUuid, outputPortUuid)
}

function undo() {
    if (!isValidScene() || !isValidLink(removedLink))
        return
    scene.restoreLinks([removedLink])
}

7. AddContainerCommand

Purpose: Tracks container addition.

Properties:


8. RemoveContainerCommand

Purpose: Tracks container removal.

Properties:


9. PropertyCommand

Purpose: Generic command for property changes.

Properties:

Implementation:

function undo() {
    setProp(oldValue)
}

function redo() {
    setProp(newValue)
}

function setProp(value) {
    if (!target)
        return
    if (apply) {
        apply(target, value)
    } else {
        target[key] = value
    }
}

Usage: Created automatically by observers when properties change.

Observer System

How Observers Work

Observers use Qt's Connections component to listen to property change signals:

Connections {
    target: node
    enabled: !NLSpec.undo.blockObservers  // Disabled during undo/redo

    function onTitleChanged() {
        // Create command for this change
        pushProp(node, "title", oldValue, newValue)
    }
}

Observer Blocking

Why Block Observers?

How It Works:

// In CommandStack.undo()
NLSpec.undo.blockObservers = true  // Block all observers
try {
    cmd.undo()  // Change properties
} finally {
    NLSpec.undo.blockObservers = false  // Unblock
}

In Observers:

Connections {
    enabled: !NLSpec.undo.blockObservers  // Only active when not blocked
    // ...
}

Cache System

Observers cache previous values to detect changes:

property var _cache: ({})

Component.onCompleted: {
    _cache.title = node.title
    _cache._init = true
}

function onTitleChanged() {
    _ensureCache()
    let oldV = _cache.title  // Get cached value
    let newV = node.title     // Get new value
    pushProp(node, "title", oldV, newV)
    _cache.title = newV  // Update cache
}

Why Cache?

Command Stack Management

Stack Structure

Undo Stack (most recent first):
[Command3] ← Most recent
[Command2]
[Command1] ← Oldest

Redo Stack (most recent first):
[RedoCommand2]
[RedoCommand1]

Stack Operations

Adding Commands

  1. Command arrives via push()
  2. Added to _pendingCommands array
  3. Timer restarts (200ms delay)
  4. If no new commands arrive, batch is finalized
  5. Macro command created and added to undo stack
  6. Redo stack is cleared (new action invalidates redo)

Undo Operation

  1. Check if undo is valid (isValidUndo)
  2. Remove command from undo stack (shift())
  3. Block observers
  4. Execute command.undo()
  5. Move command to redo stack (unshift())
  6. Unblock observers
  7. Emit signals

Redo Operation

  1. Check if redo is valid (isValidRedo)
  2. Remove command from redo stack (shift())
  3. Block observers
  4. Execute command.redo()
  5. Move command back to undo stack (unshift())
  6. Unblock observers
  7. Emit signals

Memory Management

Stack Size Limiting

property int maxStackSize: 30

function _enforceStackLimit() {
    if (maxStackSize <= 0)
        return // Unlimited

    while (undoStack.length > maxStackSize) {
        var oldCmd = undoStack.pop()  // Remove oldest
        _cleanupCommand(oldCmd)       // Clean up resources
    }
}

Command Cleanup

function _cleanupCommand(cmd) {
    if (!cmd)
        return

    // If macro command, cleanup sub-commands
    if (cmd.subCommands && Array.isArray(cmd.subCommands)) {
        for (var i = 0; i < cmd.subCommands.length; i++) {
            _cleanupCommand(cmd.subCommands[i])
        }
        cmd.subCommands = []
    }

    // Cleanup QML objects
    if (cmd.destroy && typeof cmd.destroy === "function") {
        // Destroy objects no longer in scene
        if (cmd.node && !cmd.scene.nodes[cmd.node._qsUuid]) {
            cmd.node.destroy()
        }
        // ... cleanup other objects
        cmd.destroy()
    }

    // Clear references
    cmd.node = null
    cmd.scene = null
    // ...
}

Cleanup Strategy:

Implementation Details

Integration with Scene

The undo system is integrated into I_Scene:

I_Scene {
    property UndoCore _undoCore: UndoCore {
        scene: scene
    }

    function addNode(node) {
        // ... add node logic ...

        // Create undo command
        if (!_undoCore.undoStack.isReplaying) {
            var cmdAddNode = Qt.createQmlObject(
                'import QtQuick; import NodeLink; import "Undo/Commands"; AddNodeCommand { }',
                _undoCore.undoStack
            )
            cmdAddNode.scene = scene
            cmdAddNode.node = node
            _undoCore.undoStack.push(cmdAddNode)
        }
    }
}

Key Points:

Keyboard Shortcuts

Undo/Redo can be triggered via keyboard shortcuts:

Shortcut {
    sequence: "Ctrl+Z"
    onActivated: {
        if (scene?._undoCore?.undoStack?.isValidUndo) {
            scene._undoCore.undoStack.undo()
        }
    }
}

Shortcut {
    sequence: "Ctrl+Y"
    onActivated: {
        if (scene?._undoCore?.undoStack?.isValidRedo) {
            scene._undoCore.undoStack.redo()
        }
    }
}

Signal Flow

Property Change
    ↓
Observer Detects Change
    ↓
Creates PropertyCommand
    ↓
CommandStack.push(command)
    ↓
Command Batched (200ms delay)
    ↓
Macro Command Created
    ↓
Added to Undo Stack
    ↓
stacksUpdated() Signal Emitted
    ↓
UI Updates (enable/disable undo/redo buttons)

Usage Guide

Basic Usage

Enabling Undo/Redo

Undo/Redo is automatically enabled when you create a scene:

I_Scene {
    property UndoCore _undoCore: UndoCore {
        scene: scene
    }
}

Performing Undo

// Method 1: Direct call
scene._undoCore.undoStack.undo()

// Method 2: Check validity first
if (scene._undoCore.undoStack.isValidUndo) {
    scene._undoCore.undoStack.undo()
}

Performing Redo

// Method 1: Direct call
scene._undoCore.undoStack.redo()

// Method 2: Check validity first
if (scene._undoCore.undoStack.isValidRedo) {
    scene._undoCore.undoStack.redo()
}

Creating Custom Commands

To create a custom command for your application:

  1. Create Command File:
// MyCustomCommand.qml
import QtQuick
import NodeLink

I_Command {
    id: root

    property var myData: null

    function redo() {
        if (isValidScene()) {
            // Apply your operation
            scene.doSomething(myData)
        }
    }

    function undo() {
        if (isValidScene()) {
            // Reverse your operation
            scene.undoSomething(myData)
        }
    }
}
  1. Use in Scene:
function doSomething(data) {
    // ... perform operation ...

    // Create undo command
    if (!_undoCore.undoStack.isReplaying) {
        var cmd = Qt.createQmlObject(
            'import QtQuick; import NodeLink; import "Undo/Commands"; MyCustomCommand { }',
            _undoCore.undoStack
        )
        cmd.scene = scene
        cmd.myData = data
        _undoCore.undoStack.push(cmd)
    }
}

Monitoring Stack State

Listen to stack update signals:

Connections {
    target: scene._undoCore.undoStack
    function onStacksUpdated() {
        // Update UI (enable/disable buttons)
        undoButton.enabled = scene._undoCore.undoStack.isValidUndo
        redoButton.enabled = scene._undoCore.undoStack.isValidRedo
    }
}

Clearing Stacks

Reset undo/redo history:

scene._undoCore.undoStack.resetStacks()

When to Clear:

Advanced Topics

Batch Aggregation Details

Why Batch?

How It Works:

  1. Commands arrive rapidly (< 200ms apart)
  2. Added to _pendingCommands array
  3. Timer restarts on each new command
  4. When timer expires, all pending commands are grouped
  5. Macro command created with all sub-commands
  6. Single undo operation reverses all changes

Example:

User drags node from (100, 100) to (200, 200)
    ↓
50 position updates in 500ms
    ↓
All updates batched into one macro command
    ↓
One undo restores node to (100, 100)

Memory Optimization

Stack Size Management

// Default: 30 commands
property int maxStackSize: 30

// Unlimited (not recommended)
maxStackSize: 0

// Conservative (saves memory)
maxStackSize: 10

Command Cleanup

Commands are automatically cleaned up when:

Cleanup Process:

  1. Check if command holds scene objects
  2. Verify objects are not in scene
  3. Destroy QML objects
  4. Clear all references
  5. Destroy command object

Observer Performance

Optimization Strategies:

  1. Conditional Observation: Only observe properties that need undo
  2. Value Comparison: Skip commands if values haven't actually changed
  3. Batch Updates: Group related property changes
  4. Observer Blocking: Prevent unnecessary command creation

Performance Considerations:

Custom Property Tracking

To track custom properties:

  1. Create Observer:
// MyCustomObserver.qml
Item {
    property MyObject target
    property CommandStack undoStack
    property var _cache: ({})

    Component.onCompleted: {
        _cache.myProperty = target.myProperty
        _cache._init = true
    }

    Connections {
        target: target
        enabled: !NLSpec.undo.blockObservers

        function onMyPropertyChanged() {
            _ensureCache()
            let oldV = _cache.myProperty
            let newV = target.myProperty
            pushProp(target, "myProperty", oldV, newV)
            _cache.myProperty = newV
        }
    }

    function pushProp(targetObj, key, oldV, newV) {
        if (!undoStack || undoStack.isReplaying)
            return
        if (oldV === undefined || newV === undefined)
            return
        if (JSON.stringify(oldV) === JSON.stringify(newV))
            return

        var cmd = {
            undo: function() { targetObj[key] = oldV },
            redo: function() { targetObj[key] = newV }
        }
        undoStack.push(cmd)
    }
}
  1. Use in Scene Observer:
Repeater {
    model: Object.values(scene.myObjects)
    delegate: MyCustomObserver {
        target: modelData
        undoStack: root.undoStack
    }
}

Troubleshooting

Common Issues

1. Undo/Redo Not Working

Symptoms: Commands not being created or executed.

Possible Causes:

Solutions:

// Always check isReplaying before creating commands
if (!scene._undoCore.undoStack.isReplaying) {
    // Create command
}

// Ensure observers are enabled
Connections {
    enabled: !NLSpec.undo.blockObservers
    // ...
}

2. Infinite Loop During Undo/Redo

Symptoms: Application freezes or crashes during undo/redo.

Possible Causes:

Solutions:

// Always block observers
NLSpec.undo.blockObservers = true
try {
    cmd.undo()
} finally {
    NLSpec.undo.blockObservers = false
}

// Check isReplaying in observers
if (undoStack.isReplaying)
    return

3. Memory Leaks

Symptoms: Memory usage increases over time.

Possible Causes:

Solutions:

4. Commands Not Batched

Symptoms: Too many undo steps for single operation.

Possible Causes:

Solutions:

// Adjust batch timer interval (default: 200ms)
_batchTimer.interval: 300  // Increase for slower operations

// Ensure commands use push() method
undoStack.push(cmd)  // Will be batched

5. Properties Not Tracked

Symptoms: Property changes not undoable.

Possible Causes:

Solutions:

Debug Tips

Enable Logging

// In CommandStack
function push(cmd) {
    console.log("[Undo] Pushing command:", cmd)
    // ... rest of code
}

function undo() {
    console.log("[Undo] Undoing command:", undoStack[0])
    // ... rest of code
}

Monitor Stack State

Connections {
    target: scene._undoCore.undoStack
    function onStacksUpdated() {
        console.log("Undo stack size:", scene._undoCore.undoStack.undoStack.length)
        console.log("Redo stack size:", scene._undoCore.undoStack.redoStack.length)
    }
}

Check Observer State

// Check if observers are blocked
console.log("Observers blocked:", NLSpec.undo.blockObservers)
console.log("Is replaying:", scene._undoCore.undoStack.isReplaying)

Best Practices

1. Always Check isReplaying

// Good
if (!scene._undoCore.undoStack.isReplaying) {
    var cmd = createCommand()
    scene._undoCore.undoStack.push(cmd)
}

// Bad - creates commands during undo/redo
var cmd = createCommand()
scene._undoCore.undoStack.push(cmd)

2. Block Observers During Operations

// Good
NLSpec.undo.blockObservers = true
try {
    // Perform operation
} finally {
    NLSpec.undo.blockObservers = false
}

// Bad - observers create commands during operation
// Perform operation

3. Preserve Objects for Redo

// Good - preserve link object
function undo() {
    // Remove from scene but don't destroy
    delete scene.links[link._qsUuid]
    // link object still exists for redo
}

// Bad - destroys object
function undo() {
    link.destroy()  // Can't redo!
}

4. Use Batch Commands for Related Operations

// Good - single command for multiple nodes
var cmd = AddNodesCommand {
    nodes: [node1, node2, node3]
}

// Bad - separate commands
var cmd1 = AddNodeCommand { node: node1 }
var cmd2 = AddNodeCommand { node: node2 }
var cmd3 = AddNodeCommand { node: node3 }

5. Clean Up Properly

// Good - cleanup in command
function _cleanupCommand(cmd) {
    if (cmd.node && !cmd.scene.nodes[cmd.node._qsUuid]) {
        cmd.node.destroy()
    }
    cmd.node = null
    cmd.scene = null
}

// Bad - leave references
// Objects never destroyed, memory leak

Performance Considerations

Stack Size Impact

Batch Aggregation Impact

Observer Overhead

Command Creation Cost

Architecture Diagrams

Command Flow

User Action
    ↓
Scene Operation
    ↓
Command Created
    ↓
CommandStack.push()
    ↓
Batch Timer (200ms)
    ↓
Macro Command
    ↓
Undo Stack
    ↓
[Ready for Undo]

Undo Flow

User: Ctrl+Z
    ↓
CommandStack.undo()
    ↓
Get Command from Undo Stack
    ↓
Block Observers
    ↓
Execute command.undo()
    ↓
Properties Restored
    ↓
Unblock Observers
    ↓
Move to Redo Stack
    ↓
[Ready for Redo]

Observer Flow

Property Changes
    ↓
Observer Detects
    ↓
Check: !blockObservers && !isReplaying
    ↓
Create PropertyCommand
    ↓
CommandStack.push()
    ↓
[Added to Batch]

Conclusion

The NodeLink Undo/Redo system provides a robust, efficient, and extensible solution for tracking and reversing operations. Key features:

The system is designed to be:

For more information, see the source code in resources/Core/Undo/ directory.

Serialization Format

Overview

NodeLink uses QtQuickStream for serialization and deserialization of scenes. All scenes, nodes, links, containers, and their properties are saved to JSON files with the .QQS.json extension. This document explains the serialization format, how it works, and how to use it in your applications.

File Format

File Extension

NodeLink saves scenes as .QQS.json files (QtQuickStream JSON format).

Example: MyScene.QQS.json

File Type

File Dialog Configuration

FileDialog {
    id: saveDialog
    currentFile: "QtQuickStream.QQS.json"
    fileMode: FileDialog.SaveFile
    nameFilters: [ "QtQuickStream Files (*.QQS.json)" ]
    onAccepted: {
        NLCore.defaultRepo.saveToFile(saveDialog.currentFile);
    }
}

Serialization System

QtQuickStream

NodeLink uses QtQuickStream (QS) for serialization:

How It Works

  1. Save Process:
    1. All objects in the repository are serialized to JSON
    2. Object references are converted to URLs (qqs:/UUID)
    3. Properties are serialized based on their types
    4. JSON is written to file
  2. Load Process:
    1. JSON file is read and parsed
    2. Objects are created from their types
    3. Properties are restored from JSON
    4. Object references are resolved from URLs

Saving Scenes

Basic Save

// Save scene to file
NLCore.defaultRepo.saveToFile("MyScene.QQS.json");

Save with File Dialog

// examples/simpleNodeLink/main.qml
FileDialog {
    id: saveDialog
    currentFile: "QtQuickStream.QQS.json"
    fileMode: FileDialog.SaveFile
    nameFilters: [ "QtQuickStream Files (*.QQS.json)" ]
    onAccepted: {
        NLCore.defaultRepo.saveToFile(saveDialog.currentFile);
    }
}

Button {
    text: "Save"
    onClicked: saveDialog.visible = true
}

What Gets Saved

The following are saved to the file:

Serialization Type

NodeLink uses QSSerializer.SerialType.STORAGE for saving:

// In QSRepository.saveToFile()
let repoDump = dumpRepo(QSSerializer.SerialType.STORAGE);
return QSFileIO.write(fileName, JSON.stringify(repoDump, null, 4));

Storage Type:

Loading Scenes

Basic Load

// Load scene from file
NLCore.defaultRepo.loadFromFile("MyScene.QQS.json");

Load with File Dialog

// examples/simpleNodeLink/main.qml
FileDialog {
    id: loadDialog
    currentFile: "QtQuickStream.QQS.json"
    fileMode: FileDialog.OpenFile
    nameFilters: [ "QtQuickStream Files (*.QQS.json)" ]
    onAccepted: {
        NLCore.defaultRepo.clearObjects();
        NLCore.defaultRepo.loadFromFile(loadDialog.currentFile);
    }
}

Button {
    text: "Load"
    onClicked: loadDialog.visible = true
}

Load Process

  1. Clear Existing Objects (optional):
    NLCore.defaultRepo.clearObjects();
  2. Load from File:
    NLCore.defaultRepo.loadFromFile(fileName);
  3. Scene is Restored:
    • All objects are recreated
    • Properties are restored
    • References are resolved
    • Scene is ready to use

After Loading

After loading, the scene object is automatically restored:

// Scene is automatically available after load
window.scene = Qt.binding(function() {
    return NLCore.defaultRepo.qsRootObject;
});

File Structure

JSON Structure

The saved file has the following structure:

{
    "root": "qqs:/<root-object-uuid>",
    "<object-uuid-1>": {
        "qsType": "Scene",
        "title": "My Scene",
        "nodes": {
            "<node-uuid-1>": "qqs:/<node-uuid-1>",
            "<node-uuid-2>": "qqs:/<node-uuid-2>"
        },
        "links": {
            "<link-uuid-1>": "qqs:/<link-uuid-1>"
        },
        "containers": {},
        "nodeRegistry": "qqs:/<registry-uuid>",
        "sceneGuiConfig": "qqs:/<config-uuid>"
    },
    "<node-uuid-1>": {
        "qsType": "SourceNode",
        "type": 0,
        "title": "Source_1",
        "guiConfig": "qqs:/<gui-config-uuid>",
        "nodeData": "qqs:/<node-data-uuid>",
        "ports": {
            "<port-uuid-1>": "qqs:/<port-uuid-1>"
        }
    },
    "<gui-config-uuid>": {
        "qsType": "NodeGuiConfig",
        "position": { "x": 100.0, "y": 200.0 },
        "width": 200,
        "height": 150,
        "color": "#4A90E2",
        "opacity": 1.0,
        "autoSize": true
    },
    "<node-data-uuid>": {
        "qsType": "I_NodeData",
        "data": 42.0
    },
    "<port-uuid-1>": {
        "qsType": "Port",
        "portType": 1,
        "portSide": 3,
        "title": "value",
        "color": "white",
        "enable": true
    },
    "<link-uuid-1>": {
        "qsType": "Link",
        "inputPort": "qqs:/<port-uuid-1>",
        "outputPort": "qqs:/<port-uuid-2>",
        "direction": 1,
        "guiConfig": "qqs:/<link-gui-config-uuid>"
    }
}

Root Object

The "root" key contains the UUID reference to the root object (usually the Scene):

{
    "root": "qqs:/550e8400-e29b-41d4-a716-446655440000"
}

Object Entries

Each object is stored with its UUID as the key:

{
    "<uuid>": {
        "qsType": "ObjectType",
        "property1": "value1",
        "property2": "value2",
        "referenceProperty": "qqs:/<other-uuid>"
    }
}

Object Type

Each object has a "qsType" property indicating its QML component type:

{
    "qsType": "Scene"        // Scene object
    "qsType": "Node"         // Base Node
    "qsType": "SourceNode"   // Custom node type
    "qsType": "Port"         // Port object
    "qsType": "Link"         // Link object
    "qsType": "NodeGuiConfig" // GUI configuration
}

Object References

QtQuickStream URLs

Object references are stored as QtQuickStream URLs with the format:

qqs:/<UUID>

Example:

{
    "inputPort": "qqs:/550e8400-e29b-41d4-a716-446655440000",
    "outputPort": "qqs:/6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}

Reference Resolution

During loading, URLs are resolved to actual object references:

  1. URL is parsed: qqs:/<UUID>
  2. UUID is extracted
  3. Object is looked up in repository: repo._qsObjects[uuid]
  4. Reference is assigned to property

Nested References

References can be nested:

{
    "node": {
        "guiConfig": "qqs:/<gui-config-uuid>",
        "nodeData": "qqs:/<node-data-uuid>",
        "ports": {
            "<port-uuid>": "qqs:/<port-uuid>"
        }
    }
}

Data Types

Supported Types

QtQuickStream supports serialization of:

Primitive Types

{
    "width": 200,
    "height": 150.5,
    "title": "My Node",
    "locked": false,
    "data": null
}

Complex Types

{
    "position": { "x": 100.0, "y": 200.0 },
    "colors": ["#FF0000", "#00FF00", "#0000FF"],
    "timestamp": "2024-01-15T10:30:00.000Z"
}

QML Types

{
    "guiConfig": {
        "qsType": "NodeGuiConfig",
        "width": 200,
        "height": 150
    }
}

Type Conversion

Vector2D

vector2d is serialized as an object:

// In memory
position: Qt.vector2d(100, 200)

// In JSON
{
    "position": { "x": 100.0, "y": 200.0 }
}

Arrays

Arrays are serialized as JSON arrays:

// In memory
colors: ["#FF0000", "#00FF00"]

// In JSON
{
    "colors": ["#FF0000", "#00FF00"]
}

Maps/Objects

Maps are serialized as JSON objects:

// In memory
nodes: {
    "uuid1": node1,
    "uuid2": node2
}

// In JSON
{
    "nodes": {
        "uuid1": "qqs:/uuid1",
        "uuid2": "qqs:/uuid2"
    }
}

Blacklisted Properties

Some properties are not serialized (blacklisted):

Default Blacklist

Properties starting or ending with _ (underscore):

// These are NOT saved:
property var _internalProperty
property var property_

Explicit Blacklist

Properties in QSSerializer.blackListedPropNames:

Why Blacklist?

Complete Example

Saving a Scene

// main.qml
Window {
    property Scene scene: Scene { }

    Component.onCompleted: {
        // Initialize repository
        NLCore.defaultRepo = NLCore.createDefaultRepo([
            "QtQuickStream",
            "NodeLink",
            "MyApp"
        ]);
        NLCore.defaultRepo.initRootObject("Scene");
        window.scene = Qt.binding(function() {
            return NLCore.defaultRepo.qsRootObject;
        });
    }

    FileDialog {
        id: saveDialog
        fileMode: FileDialog.SaveFile
        nameFilters: [ "QtQuickStream Files (*.QQS.json)" ]
        onAccepted: {
            NLCore.defaultRepo.saveToFile(saveDialog.currentFile);
        }
    }

    Button {
        text: "Save"
        onClicked: saveDialog.visible = true
    }
}

Loading a Scene

// main.qml
Window {
    property Scene scene: Scene { }

    FileDialog {
        id: loadDialog
        fileMode: FileDialog.OpenFile
        nameFilters: [ "QtQuickStream Files (*.QQS.json)" ]
        onAccepted: {
            // Clear existing objects
            NLCore.defaultRepo.clearObjects();

            // Load from file
            NLCore.defaultRepo.loadFromFile(loadDialog.currentFile);

            // Scene is automatically restored
            window.scene = Qt.binding(function() {
                return NLCore.defaultRepo.qsRootObject;
            });
        }
    }

    Button {
        text: "Load"
        onClicked: loadDialog.visible = true
    }
}

Example JSON File

{
    "root": "qqs:/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "a1b2c3d4-e5f6-7890-abcd-ef1234567890": {
        "qsType": "Scene",
        "title": "My Calculator Scene",
        "nodes": {
            "b2c3d4e5-f6a7-8901-bcde-f12345678901": "qqs:/b2c3d4e5-f6a7-8901-bcde-f12345678901",
            "c3d4e5f6-a7b8-9012-cdef-123456789012": "qqs:/c3d4e5f6-a7b8-9012-cdef-123456789012"
        },
        "links": {
            "d4e5f6a7-b8c9-0123-def0-234567890123": "qqs:/d4e5f6a7-b8c9-0123-def0-234567890123"
        },
        "containers": {}
    },
    "b2c3d4e5-f6a7-8901-bcde-f12345678901": {
        "qsType": "SourceNode",
        "type": 0,
        "title": "Source_1",
        "guiConfig": "qqs:/e5f6a7b8-c9d0-1234-ef01-345678901234",
        "nodeData": "qqs:/f6a7b8c9-d0e1-2345-f012-456789012345",
        "ports": {
            "a7b8c9d0-e1f2-3456-0123-567890123456": "qqs:/a7b8c9d0-e1f2-3456-0123-567890123456"
        }
    },
    "e5f6a7b8-c9d0-1234-ef01-345678901234": {
        "qsType": "NodeGuiConfig",
        "position": { "x": 100.0, "y": 200.0 },
        "width": 200,
        "height": 150,
        "color": "#4A90E2",
        "opacity": 1.0,
        "autoSize": true,
        "minWidth": 120,
        "minHeight": 80
    },
    "f6a7b8c9-d0e1-2345-f012-456789012345": {
        "qsType": "I_NodeData",
        "data": 42.0
    },
    "a7b8c9d0-e1f2-3456-0123-567890123456": {
        "qsType": "Port",
        "portType": 1,
        "portSide": 3,
        "title": "value",
        "color": "white",
        "enable": true
    },
    "d4e5f6a7-b8c9-0123-def0-234567890123": {
        "qsType": "Link",
        "inputPort": "qqs:/a7b8c9d0-e1f2-3456-0123-567890123456",
        "outputPort": "qqs:/b8c9d0e1-f2a3-4567-1234-678901234567",
        "direction": 1,
        "guiConfig": "qqs:/c9d0e1f2-a3b4-5678-2345-789012345678"
    }
}

Best Practices

1. File Naming

2. Save Before Load

Always clear objects before loading:

NLCore.defaultRepo.clearObjects();
NLCore.defaultRepo.loadFromFile(fileName);

3. Error Handling

Handle save/load errors:

FileDialog {
    onAccepted: {
        var success = NLCore.defaultRepo.saveToFile(currentFile);
        if (!success) {
            console.error("Failed to save file:", currentFile);
            // Show error message to user
        }
    }
}

4. Backup Files

Create backups before overwriting:

function saveWithBackup(fileName) {
    // Create backup
    var backupName = fileName + ".backup";
    if (QFile.exists(fileName)) {
        QFile.copy(fileName, backupName);
    }

    // Save new file
    NLCore.defaultRepo.saveToFile(fileName);
}

5. Version Compatibility

Consider versioning for file format changes:

// Add version to scene
scene.version = "1.0";

// Check version on load
if (scene.version !== "1.0") {
    console.warn("File version mismatch:", scene.version);
    // Handle migration if needed
}

6. Large Files

For large scenes:

7. Custom Data Serialization

For custom data types:

// Custom node data
I_NodeData {
    property var data: null

    // Serialize complex data
    function serializeData() {
        return JSON.stringify(data);
    }

    // Deserialize complex data
    function deserializeData(jsonString) {
        data = JSON.parse(jsonString);
    }
}

Troubleshooting

File Not Saving

File Not Loading

Missing Properties

Broken References

Type Mismatches

Advanced Topics

Custom Serialization

You can implement custom serialization logic:

// In custom node
Node {
    function customSerialize() {
        return {
            type: type,
            title: title,
            customData: myCustomData
        };
    }

    function customDeserialize(data) {
        type = data.type;
        title = data.title;
        myCustomData = data.customData;
    }
}

Migration

Handle file format changes:

function migrateScene(scene, fromVersion, toVersion) {
    if (fromVersion === "1.0" && toVersion === "2.0") {
        // Migrate from v1.0 to v2.0
        Object.values(scene.nodes).forEach(node => {
            // Update node properties
            if (!node.newProperty) {
                node.newProperty = defaultValue;
            }
        });
    }
}

Export/Import

Convert to other formats:

function exportToJSON(scene) {
    var exportData = {
        version: "1.0",
        nodes: [],
        links: []
    };

    Object.values(scene.nodes).forEach(node => {
        exportData.nodes.push({
            type: node.type,
            title: node.title,
            position: {
                x: node.guiConfig.position.x,
                y: node.guiConfig.position.y
            }
        });
    });

    return JSON.stringify(exportData, null, 2);
}

Performance Optimization

Overview

NodeLink is designed to handle large scenes with thousands of nodes efficiently. This document covers performance optimization techniques, best practices, and patterns used throughout the framework to ensure smooth operation even with complex node graphs.

Performance Principles

Key Principles

  1. Batch Operations: Group multiple operations together to reduce overhead
  2. Lazy Evaluation: Defer expensive operations until necessary
  3. Incremental Updates: Update only what changed, not everything
  4. Caching: Cache expensive computations and component creation
  5. Efficient Lookups: Use maps/objects for O(1) lookups instead of arrays
  6. GPU Acceleration: Use hardware-accelerated rendering where possible

Batch Operations

Creating Multiple Nodes

Instead of creating nodes one by one, use batch creation:

// ❌ SLOW: Creating nodes individually
for (var i = 0; i < 100; i++) {
    var node = NLCore.createNode();
    node.type = nodeType;
    scene.addNode(node);
}

// βœ… FAST: Batch creation
var nodesToAdd = [];
for (var i = 0; i < 100; i++) {
    var node = NLCore.createNode();
    node.type = nodeType;
    nodesToAdd.push(node);
}
scene.addNodes(nodesToAdd, false);

Pre-allocating Arrays

Pre-allocate arrays when you know the size:

// βœ… Pre-allocate for better performance
var nodesToAdd = [];
nodesToAdd.length = pairs.length * 2;  // Pre-allocate
var nodeIndex = 0;

for (var i = 0; i < pairs.length; i++) {
    nodesToAdd[nodeIndex++] = startNode;
    nodesToAdd[nodeIndex++] = endNode;
}

Create multiple links at once:

// examples/PerformanceAnalyzer/resources/Core/PerformanceScene.qml
function createLinks(linkDataArray) {
    if (!linkDataArray || linkDataArray.length === 0) {
        return;
    }

    var addedLinks = [];

    for (var i = 0; i < linkDataArray.length; i++) {
        var linkData = linkDataArray[i];

        // Validate and create link
        if (!canLinkNodes(linkData.portA, linkData.portB)) {
            continue;
        }

        var obj = NLCore.createLink();
        obj.inputPort = findPort(linkData.portA);
        obj.outputPort = findPort(linkData.portB);
        links[obj._qsUuid] = obj;
        addedLinks.push(obj);
    }

    // Emit signals once after all links are created
    if (addedLinks.length > 0) {
        linksChanged();
        linksAdded(addedLinks);
    }
}

Complete Batch Example

// examples/PerformanceAnalyzer/resources/Core/PerformanceScene.qml
function createPairNodes(pairs) {
    var nodesToAdd = [];
    var linksToCreate = [];

    if (!pairs || pairs.length === 0) return;

    // Pre-allocate arrays
    nodesToAdd.length = pairs.length * 2;
    linksToCreate.length = pairs.length;

    var nodeIndex = 0;

    for (var i = 0; i < pairs.length; i++) {
        var pair = pairs[i];

        // Create start node
        var startNode = NLCore.createNode();
        startNode.type = CSpecs.NodeType.StartNode;
        startNode.title = pair.nodeName + "_start";
        // ... configure node ...

        // Create end node
        var endNode = NLCore.createNode();
        endNode.type = CSpecs.NodeType.EndNode;
        endNode.title = pair.nodeName + "_end";
        // ... configure node ...

        nodesToAdd[nodeIndex++] = startNode;
        nodesToAdd[nodeIndex++] = endNode;

        linksToCreate[i] = {
            nodeA: startNode,
            nodeB: endNode,
            portA: outputPort._qsUuid,
            portB: inputPort._qsUuid,
        };
    }

    // Add all nodes at once
    addNodes(nodesToAdd, false);

    // Create all links at once
    createLinks(linksToCreate);
}

Component Caching

ObjectCreator

NodeLink uses ObjectCreator (C++) to cache QML components for faster creation:

// resources/View/I_NodesRect.qml
function onNodesAdded(nodeArray: list) {
    var jsArray = [];
    for (var i = 0; i < nodeArray.length; i++) {
        jsArray.push(nodeArray[i]);
    }

    // ObjectCreator caches the component internally
    var result = ObjectCreator.createItems(
        "node",
        jsArray,
        root,
        nodeViewComponent.url,
        {
            "scene": root.scene,
            "sceneSession": root.sceneSession,
            "viewProperties": root.viewProperties
        }
    );

    // Set properties if needed
    if (result.needsPropertySet) {
        for (var i = 0; i < result.items.length; i++) {
            result.items[i].node = nodeArray[i];
        }
    }
}

How It Works

ObjectCreator caches QQmlComponent instances:

// Source/Core/objectcreator.cpp
QQmlComponent* ObjectCreator::getOrCreateComponent(const QString &componentUrl)
{
    if (m_componentCache.contains(componentUrl)) {
        return m_componentCache[componentUrl];
    }

    QQmlComponent *component = new QQmlComponent(m_engine, QUrl(componentUrl));
    m_componentCache[componentUrl] = component;
    return component;
}

Benefits:

Efficient Data Structures

Use Maps for Lookups

Use objects/maps for O(1) lookups instead of arrays:

// ❌ SLOW: Array lookup O(n)
var nodes = [];
function findNode(uuid) {
    for (var i = 0; i < nodes.length; i++) {
        if (nodes[i]._qsUuid === uuid) {
            return nodes[i];
        }
    }
}

// βœ… FAST: Map lookup O(1)
var nodes = {};  // Object/map
function findNode(uuid) {
    return nodes[uuid];
}

Scene Node Storage

// resources/Core/Scene.qml
property var nodes: ({})  // Object map, not array
property var links: ({})  // Object map, not array
property var containers: ({})  // Object map, not array

function findNode(uuid) {
    return nodes[uuid];  // O(1) lookup
}

View Maps

// resources/View/I_NodesRect.qml
property var _nodeViewMap: ({})  // UUID -> View mapping
property var _linkViewMap: ({})  // UUID -> View mapping

function onNodeAdded(nodeObj: Node) {
    // Fast lookup
    if (Object.keys(_nodeViewMap).includes(nodeObj._qsUuid)) {
        return;  // Already exists
    }

    // Create and store
    var view = createNodeView(nodeObj);
    _nodeViewMap[nodeObj._qsUuid] = view;
}

Rendering Optimizations

GPU-Accelerated Background Grid

NodeLink uses C++ Scene Graph for high-performance grid rendering:

// Source/View/BackgroundGridsCPP.cpp
QSGNode *BackgroundGridsCPP::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    QSGGeometryNode *node = static_cast(oldNode);

    // Reuse existing node if possible
    if (!node) {
        node = new QSGGeometryNode();
        // ... setup geometry and material ...
    }

    // Update geometry efficiently
    const int verticesCount = cols * rows * 6;
    QSGGeometry *geometry = node->geometry();
    geometry->allocate(verticesCount);

    // Fill vertices
    QSGGeometry::Point2D *v = geometry->vertexDataAsPoint2D();
    // ... fill vertices ...

    node->markDirty(QSGNode::DirtyGeometry);
    return node;
}

Benefits:

Canvas Optimization

Links use Canvas with efficient repainting:

// resources/View/I_LinkView.qml
Canvas {
    id: canvas

    // Only repaint when necessary
    onOutputPosChanged: preparePainter();
    onInputPosChanged: preparePainter();
    onIsSelectedChanged: preparePainter();
    onLinkColorChanged: preparePainter();

    onPaint: {
        // Efficient painting logic
        var context = canvas.getContext("2d");
        LinkPainter.createLink(context, ...);
    }
}

Update Strategies

Incremental Updates

Update only affected nodes, not the entire graph:

// examples/visionLink/resources/Core/VisionLinkScene.qml
function updateDataFromNode(startingNode: Node) {
    // Update only the starting node
    if (startingNode.type === CSpecs.NodeType.Blur ||
        startingNode.type === CSpecs.NodeType.Brightness ||
        startingNode.type === CSpecs.NodeType.Contrast) {
        startingNode.updataData();
    }

    // Find and update only downstream nodes
    var nodesToUpdate = [startingNode];
    var processedNodes = [];
    var maxIterations = 100;
    var iteration = 0;

    while (nodesToUpdate.length > 0 && iteration < maxIterations) {
        iteration++;
        var currentNode = nodesToUpdate.shift();
        processedNodes.push(currentNode._qsUuid);

        // Find downstream links
        var downstreamLinks = Object.values(links).filter(function(link) {
            var upstreamNodeId = findNodeId(link.inputPort._qsUuid);
            return upstreamNodeId === currentNode._qsUuid;
        });

        // Update downstream nodes
        downstreamLinks.forEach(function(link) {
            var downStreamNode = findNode(link.outputPort._qsUuid);
            upadateNodeData(currentNode, downStreamNode);

            // Add to queue if not processed
            if (processedNodes.indexOf(downStreamNode._qsUuid) === -1) {
                nodesToUpdate.push(downStreamNode);
            }
        });
    }
}

Iterative Propagation

Use iterative algorithms for data propagation:

// examples/logicCircuit/resources/Core/LogicCircuitScene.qml
function updateLogic() {
    // Reset all operation nodes
    Object.values(nodes).forEach(node => {
        if (node.type === LSpecs.NodeType.AND ||
            node.type === LSpecs.NodeType.OR ||
            node.type === LSpecs.NodeType.NOT) {
            node.nodeData.inputA = null;
            node.nodeData.inputB = null;
            node.nodeData.output = null;
        }
    });

    // Iterative propagation with max iterations
    var maxIterations = 999;
    var changed = true;

    for (var i = 0; i < maxIterations && changed; i++) {
        changed = false;
        // ... propagation logic ...
    }
}

Topological Processing

// examples/visionLink/resources/Core/VisionLinkScene.qml
function updateData() {
    var allLinks = Object.values(links);
    var processedLinks = [];
    var remainingLinks = allLinks.slice();

    var maxIterations = 100;
    var iteration = 0;

    // Process in iterations until all links are processed
    while (remainingLinks.length > 0 && iteration < maxIterations) {
        iteration++;

        var linksProcessedThisIteration = [];
        var linksStillWaiting = [];

        remainingLinks.forEach(function(link) {
            var upstreamNode = findNode(link.inputPort._qsUuid);
            var downStreamNode = findNode(link.outputPort._qsUuid);

            // Check if upstream has data
            var upstreamHasData = upstreamNode.nodeData.data !== null &&
                                 upstreamNode.nodeData.data !== undefined;

            if (upstreamHasData) {
                // Process now
                upadateNodeData(upstreamNode, downStreamNode);
                linksProcessedThisIteration.push(link);
            } else {
                // Wait for next iteration
                linksStillWaiting.push(link);
            }
        });

        remainingLinks = linksStillWaiting;
        processedLinks = processedLinks.concat(linksProcessedThisIteration);
    }
}

Memory Management

Garbage Collection

Explicitly trigger GC when clearing large scenes:

// examples/PerformanceAnalyzer/resources/Core/PerformanceScene.qml
function clearScene() {
    console.time("Scene_clear");
    gc();  // Trigger garbage collection
    scene.selectionModel.clear();
    var nodeIds = Object.keys(nodes);
    scene.deleteNodes(nodeIds);
    links = [];
    console.timeEnd("Scene_clear");
}

Object Destruction

Properly destroy objects to free memory:

// resources/View/I_NodesRect.qml
function onNodesRemoved(nodeArray: list) {
    for (var i = 0; i < nodeArray.length; i++) {
        var nodeObj = nodeArray[i];
        var nodeObjId = nodeObj._qsUuid;

        // Delete ports
        let nodePorts = nodeObj.ports;
        Object.entries(nodePorts).forEach(([portId, port]) => {
            nodeObj.deletePort(port);
        });

        // Destroy node
        nodeObj.destroy();

        // Destroy and remove view
        if (_nodeViewMap[nodeObjId]) {
            _nodeViewMap[nodeObjId].destroy();
            delete _nodeViewMap[nodeObjId];
        }
    }
}

String Comparison Optimization

HashCompareString

Use MD5 hashing for efficient string comparison:

// resources/Core/Scene.qml
function canLinkNodes(portA, portB) {
    // Use hash comparison for efficiency
    if (HashCompareString.compareStringModels(nodeA, nodeB)) {
        return false;  // Same node
    }
    // ... rest of logic ...
}

How It Works:

// Source/Core/HashCompareStringCPP.cpp
bool HashCompareStringCPP::compareStringModels(QString strModelFirst, QString strModelSecound)
{
    // Compare MD5 hashes instead of full strings
    QByteArray hash1 = QCryptographicHash::hash(
        strModelFirst.toUtf8(),
        QCryptographicHash::Md5
    );
    QByteArray hash2 = QCryptographicHash::hash(
        strModelSecound.toUtf8(),
        QCryptographicHash::Md5
    );
    return hash1 == hash2;
}

Benefits:

Timer-Based Operations

Debouncing Updates

Use timers to debounce frequent updates:

// examples/logicCircuit/resources/Core/LogicCircuitScene.qml
property Timer _upateDataTimer: Timer {
    repeat: false
    running: false
    interval: 1  // 1ms delay
    onTriggered: scene.updateLogic();
}

// Trigger update with debounce
onLinkRemoved: _upateDataTimer.restart();
onNodeRemoved: _upateDataTimer.restart();
onLinkAdded: _upateDataTimer.restart();

Async Operations

Use Qt.callLater for async operations:

// examples/PerformanceAnalyzer/Main.qml
function selectAll() {
    busyIndicator.running = true;
    statusText.text = "Selecting...";

    // Defer to next event loop
    Qt.callLater(function () {
        const startTime = Date.now();
        scene.selectionModel.selectAll(scene.nodes, [], scene.containers);
        const elapsed = Date.now() - startTime;
        statusText.text = "Selected all items (" + elapsed + "ms)";
        busyIndicator.running = false;
    });
}

Selection Timer

Debounce selection events:

// resources/View/NodeView.qml
Timer {
    id: _selectionTimer
    interval: 200  // 200ms delay
    repeat: false
    onTriggered: {
        // Handle selection after delay
    }
}

MouseArea {
    onPressed: _selectionTimer.start();
    onClicked: _selectionTimer.start();
}

Best Practices

1. Batch Operations

Always batch operations when possible:

// βœ… Good
scene.addNodes([node1, node2, node3], false);
scene.createLinks([link1, link2, link3]);

// ❌ Bad
scene.addNode(node1);
scene.addNode(node2);
scene.addNode(node3);

2. Minimize Signal Emissions

Emit signals after batch operations:

// βœ… Good
var addedNodes = [];
for (var i = 0; i < 100; i++) {
    addedNodes.push(createNode(i));
}
nodesChanged();  // Emit once
nodesAdded(addedNodes);  // Emit once

// ❌ Bad
for (var i = 0; i < 100; i++) {
    var node = createNode(i);
    nodesChanged();  // Emit 100 times!
    nodesAdded([node]);  // Emit 100 times!
}

3. Use Efficient Lookups

Prefer maps over arrays for lookups:

// βœ… Good
var nodeMap = {};  // Not []
nodeMap[uuid] = node;
var found = nodeMap[uuid];  // O(1)

// ❌ Bad
var nodeArray = [];
nodeArray.push(node);
var found = nodeArray.find(n => n._qsUuid === uuid);  // O(n)

4. Limit Iterations

Always set max iterations for loops:

// βœ… Good
var maxIterations = 100;
var iteration = 0;
while (condition && iteration < maxIterations) {
    iteration++;
    // ... logic ...
}

// ❌ Bad
while (condition) {  // Could loop forever
    // ... logic ...
}

5. Cache Expensive Computations

Cache results of expensive operations:

// βœ… Good
property var _cachedResult: null;

function expensiveOperation() {
    if (_cachedResult !== null) {
        return _cachedResult;
    }
    _cachedResult = computeExpensiveResult();
    return _cachedResult;
}

6. Use ObjectCreator for Views

Always use ObjectCreator for creating views:

// βœ… Good
var result = ObjectCreator.createItems("node", nodes, parent, componentUrl, props);

// ❌ Bad
for (var i = 0; i < nodes.length; i++) {
    var view = Qt.createQmlObject(componentUrl, parent);
    // ... setup ...
}

7. Incremental Updates

Update only what changed:

// βœ… Good
function updateDataFromNode(startingNode) {
    // Update only downstream nodes
    var nodesToUpdate = [startingNode];
    // ... process incrementally ...
}

// ❌ Bad
function updateAllData() {
    // Update everything
    Object.values(nodes).forEach(node => {
        updateNode(node);
    });
}

8. Pre-allocate Arrays

Pre-allocate when size is known:

// βœ… Good
var array = [];
array.length = 1000;  // Pre-allocate
for (var i = 0; i < 1000; i++) {
    array[i] = createItem(i);
}

// ❌ Bad
var array = [];
for (var i = 0; i < 1000; i++) {
    array.push(createItem(i));  // Reallocates multiple times
}

Performance Testing

Performance Analyzer Example

NodeLink includes a Performance Analyzer example for testing:

// examples/PerformanceAnalyzer/Main.qml
function selectAll() {
    const startTime = Date.now();
    scene.selectionModel.selectAll(scene.nodes, [], scene.containers);
    const elapsed = Date.now() - startTime;
    console.log("Time elapsed:", elapsed, "ms");
}

Timer {
    id: timer
    onTriggered: {
        const startTime = Date.now();
        scene.createPairNodes(pairs);
        const elapsed = Date.now() - startTime;
        console.log("Elapsed time:", elapsed, "ms");
    }
}

Benchmarking Tips

  1. Use console.time() and console.timeEnd():
    console.time("Operation");
    // ... operation ...
    console.timeEnd("Operation");
  2. Measure in milliseconds:
    const start = Date.now();
    // ... operation ...
    const elapsed = Date.now() - start;
  3. Test with realistic data sizes:
    • Small: 10-100 nodes
    • Medium: 100-1000 nodes
    • Large: 1000-10000 nodes
  4. Profile memory usage:
    function clearScene() {
        console.time("Scene_clear");
        gc();
        // ... clear operations ...
        console.timeEnd("Scene_clear");
    }

Performance Targets

Common Performance Issues

Issue: Slow Node Creation

Symptoms: Creating nodes one by one is slow

Solution: Use batch creation

scene.addNodes(nodeArray, false);

Issue: Frequent Signal Emissions

Symptoms: UI freezes during operations

Solution: Batch operations and emit signals once

// Create all nodes first
var nodes = [];
for (var i = 0; i < count; i++) {
    nodes.push(createNode(i));
}
// Then add all at once
scene.addNodes(nodes, false);

Issue: Slow Lookups

Symptoms: Finding nodes/links is slow

Solution: Use maps instead of arrays

var nodeMap = {};  // Not []
nodeMap[uuid] = node;
var found = nodeMap[uuid];

Issue: Memory Leaks

Symptoms: Memory usage grows over time

Solution: Properly destroy objects

node.destroy();
delete _viewMap[uuid];
gc();  // Trigger GC if needed

Issue: Slow Rendering

Symptoms: Low FPS with many nodes

Solution:

Advanced Optimizations

Viewport Culling

Only render visible nodes:

function isNodeVisible(node, viewport) {
    var nodeRect = Qt.rect(
        node.guiConfig.position.x,
        node.guiConfig.position.y,
        node.guiConfig.width,
        node.guiConfig.height
    );
    return nodeRect.intersects(viewport);
}

function updateVisibleNodes() {
    var viewport = getViewport();
    Object.values(nodes).forEach(node => {
        var view = _nodeViewMap[node._qsUuid];
        view.visible = isNodeVisible(node, viewport);
    });
}

Lazy Loading

Load nodes on demand:

function loadNodesInViewport() {
    var viewport = getViewport();
    var nodesToLoad = Object.values(nodes).filter(node => {
        return isNodeVisible(node, viewport) &&
               !_nodeViewMap[node._qsUuid];
    });

    if (nodesToLoad.length > 0) {
        createNodeViews(nodesToLoad);
    }
}

Connection Graph Caching

Cache connection graphs:

property var _connectionGraphCache: null;

function buildConnectionGraph(nodes) {
    if (_connectionGraphCache !== null) {
        return _connectionGraphCache;
    }

    var graph = {};
    // ... build graph ...
    _connectionGraphCache = graph;
    return graph;
}

function invalidateConnectionGraph() {
    _connectionGraphCache = null;
}

Lasso Selection

Overview

Lasso Selection is an alternative selection mode in NodeLink that allows users to select nodes, containers, and links using a freeform (lasso-style) path instead of a rectangular marquee. This feature enables more flexible and precise selection in dense or irregular node layouts.

The lasso selection integrates seamlessly with the existing selection system, selection tools, and rubber band visualization.

Lasso selection Overview 1

Lasso selection Overview 2

Lasso selection Overview 3

Lasso selection Overview 4

Motivation

Rectangular (marquee) selection is not always ideal when:

  1. Nodes are arranged in non-rectangular patterns
  2. The scene is dense and overlapping
  3. Users need fine-grained control over which objects are selected`

Lasso Selection solves these problems by allowing users to draw an arbitrary shape around objects, selecting only those fully or partially enclosed by the lasso path.

Selection Modes

NodeLink supports multiple selection modes managed by SceneSession:

The active selection mode determines how pointer drag interactions are interpreted during selection.

High-Level Flow

  1. User activates Lasso Selection mode
  2. Pointer drag creates a freehand path
  3. Path points are collected in real time
  4. Path is optionally auto-closed when endpoints are close
  5. Objects inside the lasso polygon are detected
  6. Selection model is updated
  7. Selection tools and rubber band visuals are refreshed

Lasso Selection Algorithm

The lasso selection feature is implemented using a free-form polygon selection algorithm combined with a point-in-polygon containment test to accurately determine which scene items fall inside the user-defined lasso area.

Lasso Path Construction

While the user is drawing the lasso, mouse positions are sampled continuously and stored as an ordered list of 2D points. To improve visual smoothness and reduce noise caused by high-frequency mouse movement, the raw input points are rendered using quadratic BΓ©zier curve interpolation, resulting in a smooth and visually consistent lasso outline without altering the actual selection geometry.

Once the user completes the lasso gesture, the path is automatically closed, forming a valid polygon.

Selection Algorithm

After the lasso polygon is finalized, each selectable item in the scene is tested against the lasso area using a Point-in-Polygon (PIP) algorithm. For nodes and containers, the algorithm typically evaluates one or more representative points (such as the node’s center point or bounding box corners) to determine whether the item lies inside the lasso polygon.

The implementation relies on a well-known and robust approach such as the ray-casting algorithm, which determines containment by counting edge intersections between a horizontal ray and the polygon edges.

Accuracy and Performance Considerations

Accuracy:

The lasso selection provides pixel-level precision, allowing users to select complex and irregular groups of nodes that cannot be efficiently captured using rectangular (marquee) selection.

Performance:

The algorithm operates in linear time relative to the number of polygon edges and selectable items, which is suitable for real-time interaction. Selection evaluation is only performed after the lasso gesture is completed, ensuring that scene interaction remains responsive during drawing.

Design Rationale

The lasso-based approach was chosen to:

  1. Support non-rectangular, free-form selection shapes
  2. Improve usability in dense node layouts
  3. Provide predictable and visually intuitive selection behavior

This algorithm integrates seamlessly with the existing selection model and complements traditional rectangle-based selection without introducing breaking changes.

Lasso Path Handling

Path Construction

The lasso path is built incrementally from mouse/touch move events:

Shape Auto-Closing

To improve usability, the lasso automatically closes when the last point is close enough to the first point:

function closeShapeIfNeeded() {
    lassoSelection.isShapeClosed = false;

    if (lassoSelection.pathPoints.length < 3)
        return;

    var first = lassoSelection.pathPoints[0];
    var last = lassoSelection.pathPoints[lassoSelection.pathPoints.length - 1];

    var dx = first.x - last.x;
    var dy = first.y - last.y;
    var dist = Math.sqrt(dx * dx + dy * dy);

    var threshold = 25;

    if (dist < threshold) {
        lassoSelection.pathPoints[lassoSelection.pathPoints.length - 1] =
            Qt.point(first.x, first.y);
        lassoSelection.isShapeClosed = true;
    }

    freehandCanvas.requestPaint();
}

This avoids forcing users to manually close the lasso precisely.

Object Detection

Once the lasso is closed (or selection ends):

Only objects inside the lasso area are added to the SelectionModel.

Integration with SelectionModel

Lasso selection does not introduce a new selection system.

Instead, it reuses the existing SelectionModel:

This ensures full compatibility with:

Visual Feedback

Lasso Overlay

Rubber Band Rectangle

After selection:

Used for:

Selection Tools Behavior

The SelectionToolsRect (delete, color, duplicate, etc.):

Correctly follows:

A fix was applied to ensure the tools update correctly for single selection, not only multi-selection, by reacting to selection changes consistently.

Performance Considerations

This keeps lasso selection responsive even in large scenes.

Best Practices

Result

With Lasso Selection, NodeLink now provides:

This feature significantly improves usability without adding complexity to the core selection architecture.