简单节点编辑器

CI Release Swift Compatibility Platform Compatibility License

英文 / 日文

仍处于开发的 Alpha 阶段!

EasyNodeEditor 是一个使用 SwiftUI 创建节点编辑器的库。我开发这个库的目标是尽可能少地展示内部复杂代码,以便开发者可以尽可能专注于节点创建。

演示

(v0.1.0)
GIF 2022-09-17 at 8 27 39 PM

(v0.1.4)
GIF 2022-09-28 at 3 41 15 PM

用法 1 - 标准输出节点

步骤 1. 创建你的节点

创建一个类,并从 NodeModelBase 类继承。

class YourOutputNode: NodeModelBase {
}

步骤 2. 创建输出

创建输出变量。
为输出变量添加 @Output
确保为所有输出变量添加 @objc 包装器。
对变量的命名没有限制。你可以随意命名变量,库将自动使用该名称在节点中显示。

class YourOutputNode: NodeModelBase {
    @objc @Output var output: Int = 3
}

步骤 3. 注册你的节点

在实例化 EasyNodeEditor View 时注册你的节点。

struct ContentView: View {
    var body: some View {
        EasyNodeEditor(nodeTypes: [YourOutputNode.self])
    }
}

就是这样!
EasyNodeEditor 库将创建如下所示的节点。

image

用法 2 - 标准输入和输出节点

步骤 1. 创建你的节点

创建一个类,并从 NodeModelBase 类继承。

class YourIONode: NodeModelBase {
}

步骤 2. 创建输入和输出

创建输入和/或输出。
为输入变量添加 @Input,为输出变量添加 @Output
确保为所有输入和输出变量添加 @objc 包装器。
对变量的命名没有限制。你可以随意命名变量,库将自动使用该名称在节点中显示。

class YourIONode: NodeModelBase {
    @objc @Input var input: Int = 0
    @objc @Output var output: Int = 0
}

步骤 3. 定义输入值更改时发生的情况

重写 processOnChange() 函数,并定义你的处理过程。
不要在 processOnChange() 内部更改输入值。这会启动无限循环。

class YourIONode: NodeModelBase {
    @objc @Input var input: Int = 0
    @objc @Output var output: Int = 0
    override func processOnChange() {
        output = input * 5
    }
}

步骤 4. 注册你的节点

在实例化 EasyNodeEditor View 时注册你的节点。

struct ContentView: View {
    var body: some View {
        EasyNodeEditor(nodeTypes: [YourOutputNode.self, YoutIONode.self])
    }
}

非常简单!!
EasyNodeEditor 库将创建如下所示的节点。

image

用法 3 - 标准显示节点

步骤 1. 创建你的节点

创建一个类,并从 NodeModelBase 类继承。

class YourDisplayNode: NodeModelBase {
}

步骤 2. 创建输入

创建输入。
为输入变量添加 @Input
确保为所有输入变量添加 @objc 包装器。
对变量的命名没有限制。你可以随意命名变量,库将自动使用该名称在节点中显示。

class YourDisplayNode: NodeModelBase {
    @objc @Input var input: Int = 0
}

步骤 3. 创建 View

重写 middleContent() 函数,并定义你的 View。

class YourDisplayNode: NodeModelBase {
    @objc @Input var input: Int = 0
    override func middleContent() -> any View {
        Text("number is now -> \(input)")
    }
}

步骤 4. 注册你的节点

在实例化 EasyNodeEditor View 时注册你的节点。

struct ContentView: View {
    var body: some View {
        EasyNodeEditor(nodeTypes: [YourOutputNode.self, YoutIONode.self, YourDisplayNode.self])
    }
}

太棒了!!
EasyNodeEditor 库将创建如下所示的节点。

image

用法 4 - 标准交互式节点

我假设你已经阅读了这里的用法 1 ~ 3。
对于交互式节点,EasyNodeEditor 提供了 @Middle 属性包装器。
每当带有 @Input@Middle 的变量值更改时,processOnChange() 函数将被触发。
如果你需要用于交互的绑定对象,请使用 binding 方法并传递 KeyPath 以获取绑定对象。
完成制作后,像往常一样注册你的节点。

class YourInteractiveNode: NodeModelBase {
    @objc @Input var input: Int = 0
    @objc @Middle var count: Int = 0
    @objc @Output var output: Int = 0
    override func processOnChange() {
        output = input * count
    }
    override func middleContent() -> any View {
        Group {
            Slider(value: binding(\YourInteractiveNode.count), in: 0...100)
        }
        .frame(minWidth: 200, maxWidth: 200)
        .fixedSize()
    }
}

简单!!
EasyNodeEditor 库将创建如下所示的节点。

GIF 2022-09-17 at 8 21 30 PM

完整示例代码

import SwiftUI
import EasyNodeEditor

struct ContentView: View {
    var body: some View {
        EasyNodeEditor(nodeTypes: [YourOutputNode.self, YourIONode.self, YourDisplayNode.self, YourInteractiveNode.self])
    }
}

class YourOutputNode: NodeModelBase {
    @objc @Output var output: Int = 3
}

class YourIONode: NodeModelBase {
    @objc @Input var input: Int = 0
    @objc @Output var output: Int = 0
    override func processOnChange() {
        output = input * 5
    }
}

class YourDisplayNode: NodeModelBase {
    @objc @Input var input: Int = 0
    override func middleContent() -> any View {
        Text("number is now -> \(input)")
    }
}

class YourInteractiveNode: NodeModelBase {
    @objc @Input var input: Int = 0
    @objc @Middle var count: Int = 0
    @objc @Output var output: Int = 0
    override func processOnChange() {
        output = input * count
    }
    override func middleContent() -> any View {
        Group {
            Slider(value: binding(\YourInteractiveNode.count), in: 0...100)
        }
        .frame(minWidth: 200, maxWidth: 200)
        .fixedSize()
    }
}