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 Components

Create custom components with fields, buttons, and groups

Overview#

Components are the building blocks of scene objects in WiTwin. They define properties that can be edited in the Properties panel and behaviors that can be triggered via buttons.

Basic Component#

Use the @component decorator to create a component:

from witwin.components import Component, component, float_field, bool_field
 
@component(name="MyComponent")
class MyComponent(Component):
    intensity = float_field(1.0, min=0.0, max=10.0)
    enabled = bool_field(True)

Component Decorator#

@component(
    name="ComponentName",        # Internal name (required)
    display_name="Display Name"  # UI display name (optional)
)
class MyComponent(Component):
    pass

Field Types#

Numeric Fields#

from witwin.components import float_field, int_field
 
# Float field with range and step
brightness = float_field(
    1.0,                    # Default value
    min=0.0,                # Minimum value
    max=2.0,                # Maximum value
    step=0.1,               # Step increment
    description="Brightness level"
)
 
# Float field with units
frequency = float_field(
    2.4e9,
    units=[
        {"label": "Hz", "multiplier": 1},
        {"label": "MHz", "multiplier": 1e6},
        {"label": "GHz", "multiplier": 1e9},
    ],
    default_unit="GHz"
)
 
# Integer field
count = int_field(10, min=0, max=100, description="Number of samples")

Boolean Field#

from witwin.components import bool_field
 
enabled = bool_field(True, description="Enable this feature")
visible = bool_field(False)

String Field#

from witwin.components import string_field
 
# Text input
name = string_field("Untitled", description="Object name")
 
# Dropdown selection
mode = string_field("auto", options=["auto", "manual", "advanced"])
 
# Toggle buttons (enum_toggle)
quality = string_field("medium", options=["low", "medium", "high"], enum_toggle=True)

Vector Fields#

from witwin.components import vector2_field, vector3_field, vector4_field
 
uv_offset = vector2_field([0.0, 0.0])
position = vector3_field([0.0, 0.0, 0.0])
quaternion = vector4_field([0.0, 0.0, 0.0, 1.0])

Color Field#

from witwin.components import color_field
 
# RGBA color (0-1 range)
base_color = color_field([0.5, 0.5, 0.5, 1.0])
highlight = color_field([1.0, 0.0, 0.0, 1.0])

Component Reference Field#

from witwin.components import component_field
 
# Reference to another component
target_mesh = component_field("Mesh", description="Target mesh to render")
material_ref = component_field("Material")

Image Field#

from witwin.components import image_field
import numpy as np
 
preview = image_field(hide_label=True, title="Render Preview")
 
# Assign image data directly
# Supports: numpy array, torch tensor, PIL Image
# self.preview = np.random.rand(256, 256, 3)

List Field#

from witwin.components import list_field, float_field, vector3_field
 
# List of floats
weights = list_field(float_field(1.0, min=0, max=1))
 
# List of waypoints
waypoints = list_field(
    vector3_field(),
    default=[[0, 0, 0], [1, 0, 0]],
    description="Path waypoints"
)

Plot Field#

from witwin.components import plot_field
from witwin.components.plot import PlotBuilder
 
output = plot_field(title="Visualization")
 
def on_value_change(self, field_name, old_val, new_val):
    if field_name == 'scale':
        x = [1, 2, 3, 4, 5]
        y = [i * new_val for i in x]
        self.output = PlotBuilder.line(x, y, title="Scaled Data")

PlotBuilder Methods

from witwin.components.plot import PlotBuilder
 
# Line chart
PlotBuilder.line(x, y, title=None, xlabel=None, ylabel=None)
 
# Scatter plot
PlotBuilder.scatter(x, y, title=None, size=None, color=None)
 
# Bar chart
PlotBuilder.bar(x, heights, title=None)
 
# Heatmap/Image
PlotBuilder.imshow(data, title=None)
 
# Histogram
PlotBuilder.hist(data, bins=30, title=None)

Common Field Parameters#

All fields support these common parameters:

field(
    default,                 # Default value
    description=None,        # Tooltip description
    readonly=False,          # Prevent editing
    title=None,              # Override label text
    tooltip=None,            # Additional tooltip
    hide_label=False,        # Hide the field label
    group=None,              # Assign to a group
    horizontal_group=None,   # Horizontal layout group
    no_sync=False,           # Don't sync to frontend
)

Buttons#

Use the @button decorator to add clickable actions:

from witwin.components import button
 
@component(name="Physics")
class PhysicsComponent(Component):
    velocity = vector3_field([0, 0, 0])
 
    @button(display_name="Reset Velocity")
    def reset(self):
        self.velocity = [0, 0, 0]
        return "Velocity reset"
 
    @button(display_name="Apply Force", description="Apply upward force")
    def apply_force(self):
        self.velocity[1] += 10
        return "Force applied"
 
    @button(display_name="Save", group="Settings")
    def save_settings(self):
        # Button rendered at end of "Settings" group
        pass

Button Decorator Options#

@button(
    display_name="Button Text",  # Button label
    description=None,            # Tooltip
    icon=None,                   # Lucide icon name
    style=None,                  # Button style
    group=None                   # Place in group
)
def method_name(self, params=None):
    # params contains additional data from frontend
    return "Result message"

Field Groups#

Groups organize fields visually in the Properties panel.

Foldout Group (Collapsible)#

from witwin.components import foldout_group, define_group
 
@component(name="MyComponent")
class MyComponent(Component):
    define_group(foldout_group("Settings", collapsed=False))
    define_group(foldout_group("Advanced", collapsed=True))
 
    mode = string_field("auto", group="Settings")
    quality = float_field(0.5, group="Settings")
    debug = bool_field(False, group="Advanced")

Tab Group#

from witwin.components import tab_group, define_group
 
@component(name="Material")
class MaterialComponent(Component):
    define_group(tab_group("Appearance", "Colors", "Textures"))
 
    # Fields in tabs use dot notation: "GroupName.TabName"
    base_color = color_field([1, 1, 1, 1], group="Appearance.Colors")
    emission = color_field([0, 0, 0, 1], group="Appearance.Colors")
    albedo = string_field("", group="Appearance.Textures")
    normal = string_field("", group="Appearance.Textures")

Box Group (Simple Container)#

from witwin.components import box_group, define_group
 
@component(name="Info")
class InfoComponent(Component):
    define_group(box_group("Status", display_name="Current Status"))
 
    state = string_field("Ready", group="Status", readonly=True)
    progress = float_field(0.0, group="Status", readonly=True)

Toggle Group#

from witwin.components import toggle_group, define_group
 
@component(name="Debug")
class DebugComponent(Component):
    define_group(toggle_group("Debug", enabled=False, display_name="Debug Options"))
 
    log_level = string_field("INFO", group="Debug")
    show_bounds = bool_field(False, group="Debug")

Conditional Display#

Control field visibility based on other field values:

@component(name="Renderer")
class RendererComponent(Component):
    show_advanced = bool_field(False, description="Show advanced options")
 
    # Show when checkbox is True
    advanced_option = float_field(
        0.5,
        show_if="show_advanced",
        description="Only visible when Show Advanced is checked"
    )
 
    # Hide when checkbox is True
    basic_option = string_field(
        "simple",
        hide_if="show_advanced"
    )
 
    # Enable based on condition
    quality = string_field("low", options=["low", "medium", "high"])
    hdr_enabled = bool_field(
        False,
        enable_if={"field_name": "quality", "operator": "eq", "value": "high"}
    )

Condition Operators#

OperatorDescription
eqEquals
neqNot equals
gtGreater than
ltLess than
gteGreater or equal
lteLess or equal
is_trueField is truthy
is_falseField is falsy

Value Change Callbacks#

React to field value changes:

@component(name="Processor")
class ProcessorComponent(Component):
    input_value = float_field(0.0)
    output_value = float_field(0.0, readonly=True)
 
    def on_value_change(self, field_name, old_value, new_value):
        if field_name == 'input_value':
            result = new_value * 2.0
            self.set_field('output_value', result)

Component Methods#

class Component:
    # Set field value
    def set_field(self, field_name, value, silent=False, force=False):
        """Set field value. Use silent=True to skip callbacks."""
        pass
 
    # Manual sync to frontend
    def update(self, field_name=None):
        """Trigger sync. If field_name is None, syncs all fields."""
        pass
 
    # Get referenced component
    def get_referenced_component(self, field_name):
        """Get component instance from a component_field."""
        pass
 
    # Toggle group state
    def is_group_enabled(self, group_name):
        """Check if toggle group is enabled."""
        pass
 
    def set_group_enabled(self, group_name, enabled, notify=True):
        """Enable/disable a toggle group."""
        pass
 
    # Properties
    @property
    def owner(self):
        """Get the SceneObject that owns this component."""
        pass
 
    @property
    def scene(self):
        """Get the Scene this component belongs to."""
        pass

Complete Example#

from witwin.components import (
    Component, component, button,
    float_field, bool_field, string_field, vector3_field, color_field,
    foldout_group, define_group, plot_field
)
from witwin.components.plot import PlotBuilder
from witwin import Console
import numpy as np
 
@component(name="WaveGenerator")
class WaveGeneratorComponent(Component):
    # Define groups
    define_group(foldout_group("Parameters"))
    define_group(foldout_group("Output", collapsed=True))
 
    # Parameters group
    frequency = float_field(1.0, min=0.1, max=10.0, group="Parameters")
    amplitude = float_field(1.0, min=0.0, max=5.0, group="Parameters")
    wave_type = string_field("sine", options=["sine", "square", "sawtooth"], group="Parameters")
 
    # Output group
    preview = plot_field(title="Waveform", group="Output")
    current_value = float_field(0.0, readonly=True, group="Output")
 
    @button(display_name="Generate Preview", group="Parameters")
    def generate(self):
        x = np.linspace(0, 2 * np.pi, 100)
        if self.wave_type == "sine":
            y = self.amplitude * np.sin(self.frequency * x)
        elif self.wave_type == "square":
            y = self.amplitude * np.sign(np.sin(self.frequency * x))
        else:
            y = self.amplitude * (x % (2 * np.pi / self.frequency))
 
        self.preview = PlotBuilder.line(x.tolist(), y.tolist(), title=f"{self.wave_type} wave")
        Console.log(f"Generated {self.wave_type} wave")
        return "Preview generated"
 
    def on_value_change(self, field_name, old_val, new_val):
        if field_name in ['frequency', 'amplitude', 'wave_type']:
            Console.log(f"{field_name} changed to {new_val}")