RFDT Solver and WiTwin Simulator are undergoing internal testing as we prepare for public release. The functions are limited. Sign up to receive updates.

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