Customize Nodes

Create custom nodes with input and output ports for the Node Editor

Overview#

Nodes are visual programming blocks in the Node Editor. They process inputs and produce outputs, allowing users to create complex data flows without writing code.

Basic Node#

from witwin.node_editor import Node, input_port, output_port
 
class AddNode(Node):
    node_type = "add"
    display_name = "Add"
    category = "Math"
 
    # Input ports
    a = input_port("float", default=0.0, description="First operand")
    b = input_port("float", default=0.0, description="Second operand")
 
    # Output port
    result = output_port("float", description="Sum of a and b")
 
    def execute(self, a: float, b: float) -> float:
        return a + b

Node Class Attributes#

AttributeDescription
node_typeUnique identifier (e.g., "math/add")
display_nameName shown in UI
categoryCategory in node library

Input Ports#

Input ports receive data from connected nodes or default values.

input_port(
    port_type: str,      # Data type
    default=None,        # Default value when unconnected
    description=""       # Tooltip description
)

Port Types#

TypeDescriptionDefault
floatFloating point number0.0
intInteger number0
boolBooleanFalse
stringText string""
vector22D vector [x, y][0, 0]
vector33D vector [x, y, z][0, 0, 0]
vector44D vector [x, y, z, w][0, 0, 0, 0]
colorRGBA color[1, 1, 1, 1]
texture2D2D texture referenceNone
tensorN-dimensional arrayNone
matrixMatrix dataNone

Examples#

class MathNode(Node):
    # Numeric inputs
    value = input_port("float", default=1.0, description="Input value")
    count = input_port("int", default=10)
 
    # Boolean input
    enabled = input_port("bool", default=True)
 
    # Vector inputs
    position = input_port("vector3", default=[0, 0, 0])
    uv = input_port("vector2", default=[0, 0])
 
    # Color input
    color = input_port("color", default=[1, 1, 1, 1])

Output Ports#

Output ports produce data that can be connected to other nodes.

output_port(
    port_type: str,      # Data type
    description=""       # Tooltip description
)

Examples#

class ProcessorNode(Node):
    # Single output
    result = output_port("float", description="Processed result")
 
    # Multiple outputs
    position = output_port("vector3", description="World position")
    normal = output_port("vector3", description="Surface normal")
    distance = output_port("float", description="Distance from origin")

Execute Method#

The execute method processes inputs and returns outputs:

class MultiplyNode(Node):
    node_type = "multiply"
    display_name = "Multiply"
    category = "Math"
 
    a = input_port("float", default=1.0)
    b = input_port("float", default=1.0)
    result = output_port("float")
 
    def execute(self, a: float, b: float) -> float:
        return a * b

Multiple Outputs#

Return a tuple matching the order of output port definitions:

class SplitVectorNode(Node):
    node_type = "split_vector"
    display_name = "Split Vector"
    category = "Vector"
 
    vector = input_port("vector3", default=[0, 0, 0])
    x = output_port("float")
    y = output_port("float")
    z = output_port("float")
 
    def execute(self, vector: list) -> tuple:
        return vector[0], vector[1], vector[2]

Nodes with Fields#

Add editable parameters using component fields:

from witwin.node_editor import Node, input_port, output_port
from witwin.components import float_field, string_field, int_field, bool_field
 
class NoiseNode(Node):
    node_type = "noise_generator"
    display_name = "Noise Generator"
    category = "Generators"
 
    # Output
    noise = output_port("float", description="Generated noise value")
 
    # Editable fields (shown in node body)
    noise_type = string_field(
        "perlin",
        options=["perlin", "simplex", "worley", "value"],
        description="Noise algorithm"
    )
    scale = float_field(1.0, min=0.1, max=10.0)
    octaves = int_field(4, min=1, max=8)
    seed = int_field(0, min=0, max=9999)
 
    def execute(self) -> float:
        # Use self.noise_type, self.scale, etc.
        return self.generate_noise()

Nodes with Buttons#

Add action buttons to nodes:

from witwin.components import button, image_field
 
class PreviewNode(Node):
    node_type = "preview"
    display_name = "Preview"
    category = "Output"
 
    data = input_port("tensor")
    preview = image_field(description="Preview image")
 
    @button(display_name="Render Preview")
    def render(self):
        if self.data is not None:
            self.preview = self.data
        return "Preview rendered"

Port Colors#

Ports are color-coded by type in the Node Editor:

TypeColor
floatGray
intGray
vector2Green
vector3Yellow
vector4Pink
boolPurple
colorGradient
texture2DBlue
tensorOrange

Node Categories#

Organize nodes into categories for the library:

class AddNode(Node):
    category = "Math"        # Shows under Math category
 
class SineNode(Node):
    category = "Math/Trigonometry"  # Subcategory

Common categories:

  • Math - Mathematical operations
  • Vector - Vector operations
  • Color - Color manipulation
  • Generators - Value generators
  • Input - Scene inputs
  • Output - Results and visualization
  • Logic - Boolean operations

Complete Examples#

Math Node#

from witwin.node_editor import Node, input_port, output_port
from witwin.components import string_field
 
class MathOperationNode(Node):
    node_type = "math_operation"
    display_name = "Math"
    category = "Math"
 
    a = input_port("float", default=0.0)
    b = input_port("float", default=0.0)
    result = output_port("float")
 
    operation = string_field(
        "add",
        options=["add", "subtract", "multiply", "divide", "power"],
        enum_toggle=True
    )
 
    def execute(self, a: float, b: float) -> float:
        if self.operation == "add":
            return a + b
        elif self.operation == "subtract":
            return a - b
        elif self.operation == "multiply":
            return a * b
        elif self.operation == "divide":
            return a / b if b != 0 else 0
        elif self.operation == "power":
            return a ** b
        return 0

Vector Node#

class CombineVectorNode(Node):
    node_type = "combine_vector"
    display_name = "Combine XYZ"
    category = "Vector"
 
    x = input_port("float", default=0.0, description="X component")
    y = input_port("float", default=0.0, description="Y component")
    z = input_port("float", default=0.0, description="Z component")
    vector = output_port("vector3", description="Combined vector")
 
    def execute(self, x: float, y: float, z: float) -> list:
        return [x, y, z]

Color Node#

class ColorMixNode(Node):
    node_type = "color_mix"
    display_name = "Mix Colors"
    category = "Color"
 
    color_a = input_port("color", default=[1, 0, 0, 1])
    color_b = input_port("color", default=[0, 0, 1, 1])
    factor = input_port("float", default=0.5)
    result = output_port("color")
 
    def execute(self, color_a: list, color_b: list, factor: float) -> list:
        factor = max(0, min(1, factor))
        return [
            color_a[0] * (1 - factor) + color_b[0] * factor,
            color_a[1] * (1 - factor) + color_b[1] * factor,
            color_a[2] * (1 - factor) + color_b[2] * factor,
            color_a[3] * (1 - factor) + color_b[3] * factor,
        ]

Generator Node with Preview#

from witwin.components import button, image_field, float_field, int_field
import numpy as np
 
class GradientNode(Node):
    node_type = "gradient_generator"
    display_name = "Gradient"
    category = "Generators"
 
    texture = output_port("texture2D")
 
    width = int_field(256, min=16, max=2048)
    height = int_field(256, min=16, max=2048)
    angle = float_field(0.0, min=0, max=360)
    preview = image_field(hide_label=True)
 
    @button(display_name="Generate")
    def generate(self):
        w, h = self.width, self.height
        gradient = np.zeros((h, w, 3), dtype=np.uint8)
        for x in range(w):
            gradient[:, x] = [int(255 * x / w)] * 3
        self.preview = gradient
        return "Gradient generated"
 
    def execute(self):
        return self.preview

Differentiable Nodes#

For optimization workflows, nodes can be marked as differentiable. Connect nodes through differentiable edges (blue dashed lines) from the Hyperparameters panel to enable automatic differentiation.

class DifferentiableNode(Node):
    node_type = "differentiable_op"
    display_name = "Differentiable"
    category = "Optimization"
 
    # Inputs can receive gradients
    param = input_port("float", default=1.0)
    result = output_port("float")
 
    def execute(self, param: float) -> float:
        # This operation will be tracked for differentiation
        return param ** 2