Creating dynamic component inputs based on presets
Have you ever built a Grasshopper component that needs different inputs depending on what mode it's in? You might start by adding every possible parameter upfront, but before long, your component interface becomes cluttered with options that don't make sense for the current configuration. Users see vegetation presets when working with solid materials, or refinement controls when they're dealing with porous surfaces.
There's a better way. Instead of showing everything at once, you can create components that adapt their interface on the fly, showing only the inputs that matter for the selected preset. This approach transforms your component from a static collection of parameters into a dynamic, context-aware tool that guides users through the right options at the right time.
The Problem
As your Grasshopper components grow more sophisticated, you'll inevitably face a common challenge: different modes or configurations need completely different sets of inputs. The straightforward solution (adding every possible parameter to the component) quickly becomes a problem.
Take a material configuration component as an example. When working with solid materials, you need refinement levels and surface roughness controls. Switch to vegetation, and suddenly you're dealing with tree type presets and density settings. Porous materials? That's a whole different set of parameters involving permeability coefficients and flow characteristics.
The traditional approach of showing everything at once creates a confusing experience. Users working with solid materials see irrelevant vegetation presets. Those configuring porous materials are distracted by refinement controls they'll never use. The interface becomes noisy, and the component feels harder to understand than it actually is.
This isn't just an aesthetic issue. It actively makes your components less usable. Users spend mental energy filtering out irrelevant options, and the risk of misconfiguration increases when too many parameters are visible at once.
The Solution: Dynamic Input Management
The solution is elegant: instead of showing everything, monitor a primary input (like "Type" or "Mode") and dynamically manage the component's interface based on that selection. When a user switches from "solid" to "vegetation", the component automatically shows only the relevant parameters for vegetation, hiding everything else.
This dynamic approach requires four key pieces working together:
- Monitor a primary input that determines the configuration mode. This is your trigger for interface changes.
- Define preset configurations that specify which parameters each mode requires. Think of this as a recipe book for each mode.
- Dynamically add and remove inputs as the user switches between presets. The component adapts in real-time.
- Maintain state across regeneration cycles so the component remembers the current configuration. No surprises when the component regenerates.
The result? A component that feels intelligent, showing users exactly what they need, when they need it. The interface becomes a reflection of the current workflow rather than a static collection of every possible option.
Example Implementation Pattern
Let's walk through a practical example that demonstrates how this pattern works. We'll build a material configuration component that adapts its inputs based on the selected material type.
import Grasshopper as gh
import Grasshopper.Kernel as ghk
import scriptcontext as sc
# Define preset configurations
# Each preset specifies which inputs should be available
PRESET_CONFIGS = {
"solid": {
"inputs": [
("Refine", "num", "Refinement level"),
("Levels", "text", "Refinement boxes"),
("Roughness", "text", "Surface roughness preset")
]
},
"vegetation": {
"inputs": [
("VegetationPreset", "text", "Select vegetation type")
]
},
"porous": {
"inputs": [
("PorousPreset", "text", "Select porous material type")
]
}
}
# Track which parameters we've created dynamically
# This helps us identify what to remove when switching presets
DYNAMIC_PARAMS = set()
def ensure_inputs(selected_type):
"""Dynamically manage inputs based on selected type"""
comp = ghenv.Component
# Validate the selected type exists
if selected_type not in PRESET_CONFIGS:
print(f"Warning: Unknown preset type '{selected_type}'")
return
# Get current input parameter names
existing_inputs = {p.NickName for p in comp.Params.Input}
# Determine what inputs we need for this type
required_inputs = PRESET_CONFIGS[selected_type]["inputs"]
required_names = {name for name, _, _ in required_inputs}
# Add missing inputs
for name, kind, desc in required_inputs:
if name not in existing_inputs:
add_parameter(kind, name, desc)
DYNAMIC_PARAMS.add(name)
# Remove inputs not needed for this type
# Only remove parameters we created dynamically (not static ones)
for param in list(comp.Params.Input):
if param.NickName in DYNAMIC_PARAMS and param.NickName not in required_names:
remove_parameter(param)
DYNAMIC_PARAMS.discard(param.NickName)
# Update component UI to reflect changes
comp.Params.OnParametersChanged()
comp.ExpireSolution(True)This example shows the basic structure, but there are several important concepts to understand before implementing this pattern yourself. Let's break down each piece:
Key Concepts
Now that we've seen the pattern in action, let's break down the core concepts that make dynamic inputs work. Each piece plays a specific role in creating a responsive, adaptive component interface.
1. Preset Definition
Everything starts with defining your preset configurations. Think of this as creating a blueprint for each mode. You're specifying exactly which inputs should be available when that preset is active. Each preset is a dictionary that maps to a list of input specifications.
Here's the structure:
PRESETS = {
"type_a": {
"inputs": [("Param1", "num", "Description"), ...],
"defaults": {"Param1": 10} # Optional: default values
},
"type_b": {
"inputs": [("Param2", "text", "Description"), ...],
"defaults": {"Param2": "default_value"}
}
}Each input tuple contains three elements: the parameter name, its type (like "num" or "text"), and a description that helps users understand what it does. The optional defaults dictionary lets you set initial values when parameters are first created.
2. Input Registration
When a preset type is selected, the system needs to register only the inputs relevant to that configuration. This is where the magic happens: parameters appear dynamically based on the current selection. Here's how to create and register parameters:
import Grasshopper.Kernel.Parameters as ghparams
def add_parameter(kind, name, description):
"""Create and register a new input parameter"""
comp = ghenv.Component
param = None
# Create the appropriate parameter type
if kind == "num":
param = ghparams.Param_Number()
elif kind == "text":
param = ghparams.Param_String()
elif kind == "bool":
param = ghparams.Param_Boolean()
elif kind == "int":
param = ghparams.Param_Integer()
else:
print(f"Warning: Unknown parameter type '{kind}'")
return
# Configure the parameter
param.NickName = name
param.Name = name
param.Description = description
param.Access = ghparams.GH_ParamAccess.item # or .list for multiple items
# Register the parameter with the component
comp.Params.RegisterInputParam(param)
# Add it to the component's input list
comp.Params.OnParametersChanged()3. Input Cleanup
When switching presets, you need to remove inputs that aren't needed for the newly selected type. This cleanup step is crucial: it keeps the interface clean and prevents confusion. However, it's important to handle this carefully to avoid breaking connections or losing data:
def remove_parameter(param):
"""Safely remove a parameter from the component"""
comp = ghenv.Component
if param is None:
return
try:
# First, disconnect any sources (upstream components)
# We iterate over a list copy to avoid modification during iteration
sources = list(param.Sources)
for source in sources:
param.RemoveSource(source)
# Unregister the parameter from the component
# The second argument (True) indicates to remove from the UI as well
comp.Params.UnregisterInputParameter(param, True)
# Update the component to reflect the change
comp.Params.OnParametersChanged()
except Exception as e:
print(f"Error removing parameter {param.NickName}: {e}")4. State Persistence
One of the trickiest aspects of dynamic inputs is ensuring the component remembers its configuration across regeneration cycles. Without persistence, switching presets would reset every time the component regenerates. The solution is to use Grasshopper's sticky storage: a persistent dictionary that survives regeneration.
Here's how to implement it:
STORAGE_KEY = "dynamic_inputs_config"
def save_state(config):
"""Save current configuration to sticky storage"""
sc.sticky[STORAGE_KEY] = config
def load_state():
"""Load last saved configuration from sticky storage"""
return sc.sticky.get(STORAGE_KEY, {})
# Usage: Load state when component initializes
def initialize_component():
saved_state = load_state()
if saved_state:
selected_type = saved_state.get("type", "solid")
ensure_inputs(selected_type)This ensures that when your component regenerates (which happens frequently in Grasshopper), it remembers which preset was active and restores the appropriate inputs automatically.
Understanding these four concepts gives you the foundation to build dynamic input systems. But you might be wondering: is all this effort worth it? Let's look at the benefits:
Benefits
So why go through the effort of implementing dynamic inputs? The benefits extend beyond just a cleaner interface. They fundamentally change how users interact with your components.
Cleaner UI: By showing only relevant inputs, you eliminate visual clutter. Users aren't distracted by parameters that don't apply to their current workflow, which makes the component feel more focused and professional.
Better User Experience: When users aren't overwhelmed with unused parameters, they can focus on what matters. This makes components more intuitive to use, especially for complex tools with multiple modes. New users can learn the component more quickly because they're not trying to figure out which parameters are relevant.
Type Safety: Each preset ensures the correct parameter types are available. This reduces the chance of configuration errors and makes it clear what inputs are expected for each mode. The component guides users toward correct usage.
Maintainability: Adding new presets or modifying existing ones is straightforward. You simply update the preset configuration dictionary. No need to manually add or remove parameters in the component definition. This makes your code more maintainable and easier to extend.
Reduced Cognitive Load: Users don't have to mentally filter out irrelevant options. The component does the filtering for them, allowing them to focus on the task at hand rather than understanding which parameters apply to their current mode.
These benefits make dynamic inputs a powerful pattern, but implementing them successfully requires following some key practices:
Best Practices
As you implement dynamic inputs, these practices will help you avoid common pitfalls and create robust, user-friendly components:
-
Always provide a default preset: When the component first loads, it needs to know which preset to use. Without a default, you risk the component having no inputs or an invalid state. Set a sensible default that represents the most common use case.
-
Handle parameter removal gracefully: Before removing a parameter, always disconnect any sources (upstream connections). This prevents broken connections and ensures a smooth transition when switching presets. The
remove_parameterfunction we showed earlier handles this. -
Validate preset names: Users might mistype preset names or pass invalid values. Always validate that the selected preset exists in your configuration before trying to use it. A simple check prevents crashes and provides helpful error messages.
-
Cache the current state: Use sticky storage to persist the current configuration. This ensures the component remembers its state across regeneration cycles, which is essential for a smooth user experience.
-
Provide clear descriptions: Each dynamic input should have a clear, helpful description. When parameters appear and disappear dynamically, users need to quickly understand what each one does. Good descriptions reduce confusion and support requests.
-
Track dynamic parameters separately: Keep a set or list of parameters you've created dynamically. This helps you distinguish between static parameters (that should never be removed) and dynamic ones (that can be safely removed when switching presets).
-
Test preset switching thoroughly: Switching between presets is a critical path. Make sure it works smoothly in all scenarios. Test with connected parameters, empty presets, and rapid switching to ensure robustness.
Following these practices will help you build robust components. But where does this pattern actually make sense? Let's look at some real-world scenarios:
Real-World Applications
This pattern shines in several common scenarios where different modes require fundamentally different inputs. Here are some practical examples where dynamic inputs make a significant difference:
Material systems: When building components that handle different material types, each category has its own unique properties. Solid materials need refinement and roughness parameters, while vegetation requires preset selections and density settings. Dynamic inputs let you show only the relevant properties for the selected material type.
Analysis tools: Components that offer multiple calculation modes (like different CFD analysis types or structural analysis methods) each require unique parameters. Dynamic inputs ensure users only see the options relevant to their chosen analysis mode, reducing confusion and potential misconfiguration.
Export components: When supporting multiple file formats, each format often has format-specific options. A component that exports to both OBJ and IFC can show OBJ-specific parameters only when OBJ is selected, and IFC parameters when IFC is selected.
Geometry generators: Components that create geometry using different methods (extrusion, lofting, sweeping) can show method-specific inputs. When a user selects "extrude", they see extrusion distance and direction. Switch to "loft", and they see guide curves and section parameters instead.
Simulation components: Different simulation types require different input parameters. A wind analysis might need wind speed and direction, while a thermal analysis needs temperature and material properties. Dynamic inputs keep the interface focused on what's actually needed.
By implementing dynamic inputs in these scenarios, you create more intuitive and user-friendly components that adapt seamlessly to the task at hand. The interface becomes a reflection of the current workflow rather than a static collection of every possible option.
Now that we've covered the concepts, benefits, and best practices, let's put it all together with a complete, production-ready example:
Complete Working Example
Here's a complete, production-ready implementation that ties all the concepts together. This example creates a material configuration component with three presets: solid, vegetation, and porous materials.
import Grasshopper as gh
import Grasshopper.Kernel as ghk
import Grasshopper.Kernel.Parameters as ghparams
import scriptcontext as sc
# Storage key for persisting state
STORAGE_KEY = "material_config_state"
# Track dynamically created parameters
DYNAMIC_PARAMS = set()
# Define preset configurations
PRESET_CONFIGS = {
"solid": {
"inputs": [
("Refine", "num", "Refinement level (0-10)"),
("Levels", "text", "Refinement boxes (comma-separated)"),
("Roughness", "text", "Surface roughness preset")
],
"defaults": {"Refine": 5.0, "Roughness": "smooth"}
},
"vegetation": {
"inputs": [
("VegetationPreset", "text", "Select vegetation type"),
("Density", "num", "Vegetation density (0-1)")
],
"defaults": {"Density": 0.5}
},
"porous": {
"inputs": [
("PorousPreset", "text", "Select porous material type"),
("Permeability", "num", "Permeability coefficient")
],
"defaults": {"Permeability": 0.1}
}
}
def add_parameter(kind, name, description, default_value=None):
"""Create and register a new input parameter"""
comp = ghenv.Component
param = None
# Create the appropriate parameter type
if kind == "num":
param = ghparams.Param_Number()
if default_value is not None:
param.AddPersistentData(ghk.Types.GH_ObjectWrapper(default_value))
elif kind == "text":
param = ghparams.Param_String()
if default_value is not None:
param.AddPersistentData(ghk.Types.GH_ObjectWrapper(str(default_value)))
elif kind == "bool":
param = ghparams.Param_Boolean()
if default_value is not None:
param.AddPersistentData(ghk.Types.GH_ObjectWrapper(bool(default_value)))
else:
print(f"Warning: Unknown parameter type '{kind}'")
return None
# Configure the parameter
param.NickName = name
param.Name = name
param.Description = description
param.Access = ghparams.GH_ParamAccess.item
# Register the parameter
comp.Params.RegisterInputParam(param)
comp.Params.OnParametersChanged()
return param
def remove_parameter(param):
"""Safely remove a parameter from the component"""
comp = ghenv.Component
if param is None:
return
try:
# Disconnect any sources
sources = list(param.Sources)
for source in sources:
param.RemoveSource(source)
# Unregister the parameter
comp.Params.UnregisterInputParameter(param, True)
comp.Params.OnParametersChanged()
except Exception as e:
print(f"Error removing parameter {param.NickName}: {e}")
def ensure_inputs(selected_type):
"""Dynamically manage inputs based on selected type"""
comp = ghenv.Component
# Validate the selected type
if selected_type not in PRESET_CONFIGS:
print(f"Warning: Unknown preset type '{selected_type}'. Using default 'solid'.")
selected_type = "solid"
# Get current input parameter names
existing_inputs = {p.NickName for p in comp.Params.Input}
# Determine what inputs we need for this type
config = PRESET_CONFIGS[selected_type]
required_inputs = config["inputs"]
required_names = {name for name, _, _ in required_inputs}
defaults = config.get("defaults", {})
# Add missing inputs
for name, kind, desc in required_inputs:
if name not in existing_inputs:
default_val = defaults.get(name)
add_parameter(kind, name, desc, default_val)
DYNAMIC_PARAMS.add(name)
# Remove inputs not needed for this type
for param in list(comp.Params.Input):
if param.NickName in DYNAMIC_PARAMS and param.NickName not in required_names:
remove_parameter(param)
DYNAMIC_PARAMS.discard(param.NickName)
# Update component UI
comp.Params.OnParametersChanged()
comp.ExpireSolution(True)
def save_state(config):
"""Save current configuration to sticky storage"""
sc.sticky[STORAGE_KEY] = config
def load_state():
"""Load last saved configuration from sticky storage"""
return sc.sticky.get(STORAGE_KEY, {"type": "solid"})
# Main execution
# Assume 'Type' is a static input parameter that contains the selected preset
try:
# Get the selected type from the 'Type' input
# This assumes you have a static input named 'Type'
type_param = None
for param in ghenv.Component.Params.Input:
if param.NickName == "Type":
type_param = param
break
if type_param and type_param.VolatileDataCount > 0:
selected_type = str(type_param.VolatileData[0]).strip().lower()
else:
# Load from saved state or use default
state = load_state()
selected_type = state.get("type", "solid")
# Ensure inputs match the selected type
ensure_inputs(selected_type)
# Save state for next regeneration
save_state({"type": selected_type})
except Exception as e:
print(f"Error in dynamic input management: {e}")
# Fallback to default
ensure_inputs("solid")This complete example includes error handling, state persistence, default values, and proper parameter management. You can adapt it to your specific use case by modifying the PRESET_CONFIGS dictionary and adjusting the parameter types as needed.